Initial signal runtime

This commit is contained in:
Yura Dupyn 2026-02-08 21:01:43 +01:00
parent 8b02e3e7d1
commit 94cb3bd721
7 changed files with 900 additions and 35 deletions

View file

@ -0,0 +1,86 @@
```javascript
const Thing = (initState) => ({
state: initState,
subscribers: [],
set(transform) {
const state = transform(this.state);
this.state = state;
this.subscribers.forEach(f => {
f(state);
});
},
get() {
return this.state;
},
subscribe(f) {
this.subscribers.push(f);
},
map(transform) {
const Y = Thing(this.state);
this.subscribe(x => {
Y.set(() => transform(x));
});
return Y;
},
});
function pair(X, Y) {
const Z = Thing([X.get(), Y.get()]);
X.subscribe(x => {
Z.set(() => [x, Y.get()]);
});
Y.subscribe(y => {
Z.set(() => [X.get(), y]);
});
return Z;
}
// Comonad lift
// Signal(a), (Signal(a) -> b) -> Signal(b)
// X: Signal(A), f: Signal(A) -> B
// Y: Signal(B)
// function extend(X, f) {
// const y0 = f(X);
// const Y = Thing(y0);
// X.subscribe(x => {
// Y.set(f(???)); I need to somehow feed it a new signal...
// // TODO: Ofcourse I can feed it `X` again, but that feels wrong... I thought
// });
// return Y;
// }
const count = Thing(0);
console.log("COUNT EXISTS");
console.log(count.get())
count.subscribe(x => {
console.log("count is now", x);
});
count.set(() => 1)
count.set(() => 2)
const double = count.map(x => 2*x);
console.log("DOUBLE EXISTS");
double.subscribe(x => {
console.log("double is now", x);
});
count.set(() => 3);
count.set(() => 9);
const WTF = pair(count, double)
console.log("-> WTF EXISTS");
WTF.subscribe(([x, y]) => {
console.log("WTF is now ", [x, y]);
});
count.set(() => 13);
```

485
src/lang/SIGNAL.md Normal file
View file

@ -0,0 +1,485 @@
# Language Design: Two Worlds
The language into two fragments: Normal Expressions vs Signal Expressions.
- The Normal (Values) world is the standard (call-by-value) functional programming.
- The Signal (Reactive) world declares connections between time-varying signals.
The basic constraint is: Normal world has no conception of Signals. We can't name in a normal world a signal, we can't create signals, we can't pass them to functions or return signals from functions.
# Applicative Structure
Basic Signal expression is `@foo` which we can think of as reading (and being dependent on) a signal `foo`.
Then we have another Signal expression `let-signal` which is the applicative structure. It is the only construct that connects the Normal world to the Signal world.
It allows embedding of Normal Expressions into Signal Expressions. Recall applicative structure
```
Signal(A), Signal(B), (A, B -> C) -> Signal(C)
```
we directly adopt this into
```
let-signal {
// SIGNAL WORLD (RHS of :=)
// We extract values from the graph.
x := signal-expr-1,
y := signal-expr-2
.
// NORMAL WORLD (Body)
// We compute a new value using the extracted data.
// $x and $y are plain values here.
normal-expression($x, $y)
}
```
For example given a number signal `count`, we can create a signal that doubles it by
```
let-signal {
x := @count
. *($x, 2)
}
```
We also have a constant signal expression `const e` where `e` is a normal expression.
Note that a normal expression can't contain `@` nor `let-expr`.
# Top-Level Constructs
We introduce two new top-level constructs, via keywords `signal` and `fn-signal`.
The following
```
signal double {
let-signal {
x := @count
. *($x, 2)
}
}
```
defines a new top-level signal named `double` and starts it up.
General syntax
```
signal sigName {
signal-expression
}
```
We also have parametrised top-level signal expressions. These can be used to define UI components
```
fn-signal Counter(color) {
let-signal {
// We must hardwire to global signals or internal state
n := @global-count
.
<div style={ :( color = $color ) }>
{ $n }
</div>
}
}
```
More generally
```
fn-signal ParametrizedSignalName(x1, x2, x3) {
signal-expression // $x1, $x2, $x3 are normal values
}
```
Here is how we can define App Root
```
signal App {
let-signal {
// SIGNAL WORLD: Instantiate the component
// We pass "red" (Normal Expr), not a signal.
main-counter := Counter("red")
.
// NORMAL WORLD: Compose the HTML
<div id="app">
<h1>My App</h1>
{ $main-counter }
</div>
}
}
```
TODO: Now that I'm thinking about it, I think we should separate Views from Components.
Basically a `View` is a pure function that happens to return UI. While a Component is a View together with all the signals feds into it.
# Runtime Constraints & Implementation
- Static Topology? We should be able to know the dependency graph between signals at compile-time.
- Rank-based Update, topological-srot on graph once at startup. Each node gets a rank (how close it is to the root signal).
Updates ar eprocessed via priority-queue - low-rank dependencies are processed earlier
# TODO: Linear Types
What exactly is the meaning of the below?
Seems a bit weird that the signal `state` can be accessed multiple times like this.
```
let-signal {
x0 := @state,
x1 := @state,
. ...
}
It would make sense to me if we sampled once, then duplicated the value we got...
let-signal {
x := @state,
x0 := $x,
x1 := $x,
. ...
}
```
But then again, I guess it does make sense to sample twice - but there's no guarantee of getting the same result... e.g.
```
@ : Signal(A) -> Signal(A), A
```
the signal read operation may potentially change the signal itself.
-----------------------
This is Solidjs. How does it work?
```
function Counter() {
const [ count, setCount ] = createSignal(0);
return (
<button onClick={ () => setCount(n => n + 1) }>
Count: { count() }
</button>
);
}
let-signal foo = x0;
@foo // read
@foo := x1 // write
@foo { x . body }
function Counter() {
let-signal count = 0;
<button onClick={ e => @count { c . c + 1 } } >
Count: { @count } // how can only this part be re-rendered? How is this communicated?
</button>
}
let count = 0
let double = count * 2
count = 11
console.log(double) // 0
// ===Count===
let-signal count = 0;
//asignment
@count := 1
@count := 2
// subscription
@count { x .
console.log(x);
}
// POSSIBILITY 1: manual...
let-signal double = @count
@count { x .
@double := 2*x
}
// POSSIBILITY 2: automatic
let-signal??? double = 2*@count
// but where' sthe dependency on count? Is it just auto?
// I guess you can just analyze the code and see that this deopends on the count...
// but what if I wanted to just initialize `double` with the current value of the @count signal...
//
// maybe that's not how signals should be used...
// if a signal is used in a thing - that thing must change according to the signal - no other way?
```
What can we do?
- Creation: We can introduce a new signal
- Reading/Subsribing: We can read the current value of a signal
- should we then automatically change with that signal?
Are we dependent on it?
This is like we can read only by subscription.
- Updating: If we have the capability, we can update the signal to new value (can be a function of the current value)
- ???Subscription: We can subscribe to changes in the signal
- Map: We can create new signals that are a function of the original signal:
But this is a bit weird... in this output signal we can still set it...
Maybe we should be able to create only read-only signals?
Read/Write Signal
ReadOnly Signal (from map?)
What about stuff like `andThen`?
```
Signal(A)
```
```
type Model
create initState: Model
type Msg
create update : Msg, Model -> Model
const { dispatchMsg: (Msg) -> Void } = useElm(initState, update)
```
how to call the combination of a signal together with the events?
=== Tracking Scopes ===
When are tracking-scopes created?
```
// effects
createEffect(() => {
...
})
// returning jsx from a component
// would be better if we had to actually wrap it explicitely into something.
return (
<div>
...
</div>
);
```
Nesting tracking-scopes seem complex.
We need some sort of a stack for tracking effects.
And when a child effect is pushed, it needs to say to its parent (current top-of-stack),
that, "hey, I'm yo kid, please clean me up when u refresh"
TODO:
```
createEffect(() => {
if (showDetails()) {
console.log(userID());
} else {
console.log("Hidden");
}
});
```
Async is gonna be a pain in the ass too.
Basically anything to do with continuations.
Then I need to worry about `Scheduler`.
=== Store ===
How do you call a combination of a signal together with event-source that governs it together with a capability to dispatch it. For example imagine fictional:
```
type Model = ... // probably-big-record
type Msg = ... // probably-big-sum-type
let initModel: Model = ...
fn update : Model, Msg -> Model
// now we can make the thing ???
let X = Store(initModel, update)
```
Now from `X` we should be able to get `Signal(Model)`, but we should also get a capability to trigger events - i.e. some sort of dispatch mechanism.
So I'm wondering, how this `X` should be called (its type depends both on `Model` and `Msg`). Or should that just be a pair? Like `(Signal(Model), EventSourceDIspatchCapability(Msg))`?
Apparently this is called a `Store(Model, Msg)`
Other people call it `Subject` - something that can both be read and written to
Other people call it `Atom/Agent`
signals, managing state
storing/updating/reading values reactively
- current-user
- current-page
- current-theme
createSignal(initState)
tracking-signal
signals are reactive
they auto-update when their value changes
- this is stupid. Ofcourse...
when a signal is called within a tracking scope,
signal adds a dependency
=== Reactive Runtime ===
- Signal Dependency Graph (or DAG)
- Effect Ownership Tree
- Effect Stack
for async
- Scheduler
- Context Restoration (when a thing in Scheduler is ready and needs to be resumed)
=== Syntax ===
Wait a sec... @count returns a Signal object?
I'm not sure that I like that...
+(@count, 1) would be error?
hmm, to read from a signal, we need to be in a tracking block...
```
track {
@count
}
```
```
let {
double = track { *(@count, 2) } // this returns a signal... interesting!
.
track {
+(@double) // need different syntax I think... `double` is a regular variable...
}
}
```
What about
```
let-signal {
double = *(@count, 2),
.
+(@double, 5)
}
```
the w
`track` expression returns a signal, interesting.
Actually the let-signal could be like a sequential pairing mechanism.
Signal(A) Signal(B), (A, B -> C) -> Signal(C)
let-signal {
a := signal-expression,
b := signal-expression
.
pure-expression that has a, b as local variables...
}
Mapping:
we could have a sing
this is like a map
```
let-signal {
x := @count
.
*($x, 2)
} // the following is like the double signal
```
```
let {
double = let-signal { x := @count . *($x, 2) }
.
let-signal {
y := $double // fucking shit. `double` here is a local var...
. +($y, 1)
}
}
```
hmm, maybe I could somehow syntactically distinguish Signal expressions from normal expressions?
We can also do the same sort of thing with effects, I guess...
```
effect {
x := @count
.
log("The count is now: ", x)
}
```
The following is basically what I would have to put into the tiddler's code...
```
let-signal {
x := $count
.
+($x, 1)
}
```
not exactly simple, damn.
The only way to sample a signal is by using this let-signal thing...
$count { x . +($x, 1) }
// This one is simple... but not exactly with clear semantics
+($count, 1)
Maybe we could somehow auto-transform these into the let-signal things?
Yeah, but evaluation rules need to be strict and that's what let-signal thing allows...
mhm, it also would be nice if we had a syntax for constant signals..., right?
```
let-signal {
divisor = 2, // note that this is a pure expression...
x := @mouse-x,
y := let-signal {
a := @mouse-y,
b := @offset
. +($a, $b)
}
.
/(+(*), $divisor)
}
```
// === parametrised signals ===
```
// like a top-level function of type (A, B, C) -> Signal(D)
fn-signal Foo(x1, x2, x3) {
// signal-expression
}
```

View file

@ -1,9 +1,12 @@
import { FunctionName, Pattern, VariableName } from "../expr"
import { FunctionName, Pattern, SignalName, VariableName } from "../expr"
import { Closure, Value, ValueTag } from "./value"
export type RuntimeError =
| { tag: "FunctionLookupFailure", name: FunctionName }
| { tag: "FunctionCallArityMismatch", name: FunctionName, expected: number, actual: number }
| { tag: "SignalLookupFailure", name: SignalName }
| { tag: "SignalDefinitionHasSignalIdWithoutSignal", name: SignalName } // runtime corruption
| { tag: "SignalHasCyclicDependency", name: SignalName }
| { tag: "ClosureApplicationArityMismatch", closure: Closure, expected: number, actual: number }
| { tag: "VariableLookupFailure", name: VariableName }
// | { tag: "CellLookupFailure", name: CellName }

View file

@ -17,7 +17,7 @@ export function eval_start(program: Program, e: Expr): Result<Value> {
}
// may throw `ThrownRuntimeError`
function eval_expr(program: Program, env: Env, e: Expr): Value {
export function eval_expr(program: Program, env: Env, e: Expr): Value {
switch (e.tag) {
case "literal":
switch (e.literal.tag) {

154
src/lang/eval/signal.ts Normal file
View file

@ -0,0 +1,154 @@
import { SignalExpr } from "../expr";
import { Program } from "../program";
import { Result, RuntimeError, ThrownRuntimeError } from "./error";
import { eval_expr } from "./evaluator";
import { Env, Value } from "./value";
export type SignalId = number;
export type SignalRuntime = {
next_signal_id: SignalId,
store: Map<SignalId, Signal<Value>>,
};
export namespace SignalRuntime {
export function make(): SignalRuntime {
return {
next_signal_id: 0,
store: new Map(),
};
}
export function generateSignalId(runtime: SignalRuntime): SignalId {
const id = runtime.next_signal_id;
runtime.next_signal_id += 1;
return id;
}
export function getSignal(runtime: SignalRuntime, signalId: SignalId): Signal<Value> | undefined {
return runtime.store.get(signalId);
}
}
// TODO
// 1. initialization phase where we compile top-level signal expressions into a graph
// 2. runtime, subscriptions, derived signals etc.
// may throw `ThrownRuntimeError`
export function eval_signal_expression(program: Program, e: SignalExpr): Signal<Value> {
switch (e.tag) {
case "read":
return Program.get_or_create_signal(program, e.name);
case "const":
const val = eval_expr(program, Env.nil(), e.arg);
return signal(val);
case "let":
// TODO
throw new Error("TODO: Let-signals are hard, skipping for now.");
}
}
export interface Signal<T> {
state: T,
observers: Observer<T>[],
set(transform: (state: T) => T): void,
read(): T,
subscribe(observer: Observer<T>): void,
map<S>(transform: (state: T) => S): Signal<S>,
};
export type Observer<T> = (state: T) => void;
export function signal<T>(initState: T): Signal<T> {
return {
state: initState,
observers: [],
set(transform: (state: T) => T) {
const state = transform(this.state);
this.state = state;
this.observers.forEach((observer: Observer<T>) => {
observer(state);
});
},
read(): T {
return this.state;
},
subscribe(observer: Observer<T>) {
// TODO: This needs to return `cancellation`
this.observers.push(observer);
},
map<S>(transform: (state: T) => S): Signal<S> {
const Y = signal(transform(this.state));
this.subscribe((state: T) => {
Y.set(() => transform(state));
});
return Y;
}
};
}
function pair<A, B>(X: Signal<A>, Y: Signal<B>): Signal<[A, B]> {
const Z = signal([X.read(), Y.read()] as [A, B]);
X.subscribe(x => {
Z.set(() => [x, Y.read()]);
});
Y.subscribe(y => {
Z.set(() => [X.read(), y]);
});
return Z;
}
// === example ===
// const count = signal(0);
// console.log("COUNT EXISTS");
// console.log(count.read())
// count.subscribe(x => {
// console.log("count is now", x);
// });
// count.set(() => 1)
// count.set(() => 2)
// const double = count.map(x => 2*x);
// console.log("DOUBLE EXISTS");
// double.subscribe(x => {
// console.log("double is now", x);
// });
// count.set(() => 3);
// count.set(() => 9);
// const WTF = pair(count, double)
// console.log("-> WTF EXISTS");
// WTF.subscribe(([x, y]) => {
// console.log("WTF is now ", [x, y]);
// });
// count.set(() => 13);
// === Primitive Signals ===
export function makeTickSignal(intervalMs: number): Signal<Value> {
const s = signal(Value.number(0));
setInterval(() => {
const state = s.read();
if (state.tag === "number"){
s.set(() => Value.number(state.value + 1));
} else {
console.log("Something is really wrong with the state of tick-signal", state);
}
}, intervalMs);
return s;
}

View file

@ -3,7 +3,7 @@ import { Span } from "./parser/source_text"
// === Identifiers ===
export type VariableName = string
export type FunctionName = string
// type CellName = string
export type SignalName = string
export type Tag = string
export type FieldName = string
@ -13,7 +13,6 @@ export type Meta = { span: Span };
export type Expr =
| { tag: "literal", literal: Literal} & Meta
| { tag: "var_use", name: VariableName } & Meta
// | { tag: "cell_ref", name: CellName }
| { tag: "call", name: FunctionName, args: Expr[] } & Meta
| { tag: "let", bindings: ExprBinding[], body: Expr } & Meta
| { tag: "tag", tag_name: Tag } & Meta
@ -24,6 +23,11 @@ export type Expr =
| { tag: "lambda", parameters: ProductPattern[], body: Expr } & Meta
| { tag: "apply", callee: Expr, args: Expr[] } & Meta
export type SignalExpr =
| { tag: "read", name: SignalName } & Meta
| { tag: "const", arg: Expr } & Meta
| { tag: "let", bindings: SignalBinding[], body: Expr } & Meta
export type Literal =
| { tag: "number", value: number }
| { tag: "string", value: string }
@ -33,6 +37,11 @@ export type ExprBinding = {
expr: Expr,
} & Meta
export type SignalBinding = {
pattern: ProductPattern,
expr: SignalExpr,
} & Meta
export type MatchBranch = {
pattern: Pattern,
body: Expr,
@ -75,6 +84,11 @@ export namespace Expr {
export const fieldAssignment = (name: FieldName, expr: Expr, span: Span): FieldAssignment => ({ name, expr, span });
}
export namespace SignalExpr {
export const read = (name: SignalName, span: Span): SignalExpr => ({ tag: "read", name, span });
export const signalBinding = (pattern: ProductPattern, expr: SignalExpr, span: Span): SignalBinding => ({ pattern, expr, span });
}
export namespace ProductPattern {
export const any = (name: VariableName, span: Span): ProductPattern => ({ tag: "any", name, span });
export const tuple = (patterns: ProductPattern[], span: Span): ProductPattern => ({ tag: "tuple", patterns, span });

View file

@ -1,35 +1,22 @@
import { eval_signal_expression, makeTickSignal, Signal, SignalId, SignalRuntime } from "./eval/signal";
import { ThrownRuntimeError } from "./eval/error";
import { Value } from "./eval/value";
import { Expr, FunctionName, ProductPattern } from "./expr";
import { Expr, FunctionName, SignalName, ProductPattern, SignalExpr } from "./expr";
import { installPrimitives } from "./primitive";
export type Timestamp = number;
export type Program = {
function_definitions: Map<FunctionName, FunctionDefinition>,
function_definition_order: FunctionName[],
// TODO: Perhaps include the story and the environment?
// story should be a list of currently viewed bindings
// environment should be like the store... maybe call it store! It should map names to values and perhaps expressions that generated the value...
// like a reactive cell. This is the analogue of the tiddler.
// store: Map<CellName, Cell>
function_definitions: Map<FunctionName, FunctionDefinition>,
function_definition_order: FunctionName[],
signal_definitions: Map<SignalName, SignalDefinition>,
signal_definition_order: SignalName[],
signal_runtime: SignalRuntime,
};
// type Cell = {
// name: CellName,
// expression: Expr,
// cached_value?: Value,
// status: CellStatus
// // TODO: Dependencies? Not sure about this yet...
// // Operational Semantics of Cells is gonna be thought up much later.
// // dependencies?: Set<CellName>,
// }
// type CellStatus =
// | "clean"
// | "dirty"
// | "error"
// === Functions ===
export type FunctionDefinition =
| { tag: "user", def: UserFunctionDefinition }
| { tag: "primitive", def: PrimitiveFunctionDefinition }
@ -37,8 +24,8 @@ export type FunctionDefinition =
export type UserFunctionDefinition = {
// Raw user input (authoritative)
name: FunctionName,
raw_parameters: string;
raw_body: string;
raw_parameters: string,
raw_body: string,
// parsed
parameters: ProductPattern[],
@ -46,8 +33,8 @@ export type UserFunctionDefinition = {
// metadata
created_at: Timestamp;
last_modified_at: Timestamp;
created_at: Timestamp,
last_modified_at: Timestamp,
}
export type PrimitiveFunctionDefinition = {
@ -57,6 +44,29 @@ export type PrimitiveFunctionDefinition = {
export type Implementation = (args: Value[]) => Value
// === Signals ===
export type SignalDefinition =
| { tag: "user", def: UserSignalDefinition }
| { tag: "primitive", def: PrimitiveSignalDefinition }
type UserSignalDefinition = {
name: SignalName,
raw_body: string,
body: SignalExpr,
is_initializing: boolean,
signalId?: SignalId,
// metadata
createdAt: Timestamp,
lastModifiedAt: Timestamp,
}
type PrimitiveSignalDefinition = {
name: FunctionName,
signalId: SignalId,
}
export namespace Program {
type Error =
@ -92,9 +102,15 @@ export namespace Program {
const program: Program = {
function_definitions: new Map(),
function_definition_order: [],
signal_runtime: SignalRuntime.make(),
signal_definitions: new Map(),
signal_definition_order: [],
};
installPrimitives(program);
initialize_signal_runtime(program);
return program;
}
@ -102,13 +118,120 @@ export namespace Program {
export function lookup_function(program: Program, name: FunctionName): FunctionDefinition {
const fn = program.function_definitions.get(name);
if (fn === undefined) {
throw ThrownRuntimeError.error({
tag: "FunctionLookupFailure",
name,
});
throw ThrownRuntimeError.error({ tag: "FunctionLookupFailure", name, });
}
return fn;
}
// may throw `ThrownRuntimeError`. This is used by evaluator.
export function lookup_signal_definition(program: Program, name: SignalName): SignalDefinition {
const sigDef = program.signal_definitions.get(name);
if (sigDef === undefined) {
throw ThrownRuntimeError.error({ tag: "SignalLookupFailure", name, });
}
return sigDef;
}
export function initialize_signal_runtime(program: Program) {
// TODO: Do I really need to initalize everything from the start?
install_primitive_signals(program);
for (const name of program.signal_definition_order) {
const _ = get_or_create_signal(program, name);
}
}
function install_primitive_signals(program: Program) {
install_primitive_signal(program, "tick",makeTickSignal(1000));
}
function install_primitive_signal(program: Program, name: SignalName, signal: Signal<Value>) {
const signalId = attachNewSignal(program, signal);
const def: SignalDefinition = {
tag: "primitive",
def: { name, signalId }
};
program.signal_definitions.set(name, def);
}
// may throw `ThrownRuntimeError`. This is used during initialization.
export function get_or_create_signal(program: Program, name: SignalName): Signal<Value> {
const sigDef = lookup_signal_definition(program, name);
switch (sigDef.tag) {
case "user": {
const def = sigDef.def;
if (def.signalId !== undefined) {
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
if (signal === undefined) {
throw ThrownRuntimeError.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name, });
} else {
return signal;
}
}
// We need to create the signal for the first time.
if (def.is_initializing) {
throw ThrownRuntimeError.error({ tag: "SignalHasCyclicDependency", name, });
}
def.is_initializing = true;
try {
const newSignal = eval_signal_expression(program, def.body);
const newId = attachNewSignal(program, newSignal);
program.signal_runtime.store.set(newId, newSignal);
def.signalId = newId;
return newSignal;
} finally {
def.is_initializing = false;
}
}
case "primitive": {
const def = sigDef.def;
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
if (signal === undefined) {
throw ThrownRuntimeError.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name, });
} else {
return signal;
}
}
}
}
function attachNewSignal(program: Program, signal: Signal<Value>): SignalId {
const newId = SignalRuntime.generateSignalId(program.signal_runtime);
program.signal_runtime.store.set(newId, signal);
return newId;
}
// TODO: Is this necessary? Maybe `get_or_create_signal` is sufficient for all cases.
// may throw `ThrownRuntimeError`. This is used by evaluator.
export function lookup_signal(program: Program, name: SignalName): Signal<Value> {
const sigDef = lookup_signal_definition(program, name);
switch (sigDef.tag) {
case "user": {
const def = sigDef.def;
if (def.signalId === undefined) {
throw ThrownRuntimeError.error({ tag: "SignalLookupFailure", name, });
}
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
if (signal === undefined){
throw ThrownRuntimeError.error({ tag: "SignalLookupFailure", name, });
} else {
return signal;
}
}
case "primitive": {
const def = sigDef.def;
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
if (signal === undefined) {
throw ThrownRuntimeError.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name, });
} else {
return signal;
}
}
}
}
export type CreateFunction = {
name: FunctionName,