From 94cb3bd721ae354021b1375751b6d27ded6eb14b Mon Sep 17 00:00:00 2001 From: Yura Dupyn <2153100+omedusyo@users.noreply.github.com> Date: Sun, 8 Feb 2026 21:01:43 +0100 Subject: [PATCH] Initial signal runtime --- src/lang/SIGNAL-EXPERIMENT.md | 86 ++++++ src/lang/SIGNAL.md | 485 ++++++++++++++++++++++++++++++++++ src/lang/eval/error.ts | 5 +- src/lang/eval/evaluator.ts | 2 +- src/lang/eval/signal.ts | 154 +++++++++++ src/lang/expr.ts | 18 +- src/lang/program.ts | 185 ++++++++++--- 7 files changed, 900 insertions(+), 35 deletions(-) create mode 100644 src/lang/SIGNAL-EXPERIMENT.md create mode 100644 src/lang/SIGNAL.md create mode 100644 src/lang/eval/signal.ts diff --git a/src/lang/SIGNAL-EXPERIMENT.md b/src/lang/SIGNAL-EXPERIMENT.md new file mode 100644 index 0000000..b39f6d9 --- /dev/null +++ b/src/lang/SIGNAL-EXPERIMENT.md @@ -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); +``` diff --git a/src/lang/SIGNAL.md b/src/lang/SIGNAL.md new file mode 100644 index 0000000..abbfaf6 --- /dev/null +++ b/src/lang/SIGNAL.md @@ -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 + . +
+ { $n } +
+ } +} +``` + +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 +
+

My App

+ { $main-counter } +
+ } +} +``` + +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 ( + + ); +} + +let-signal foo = x0; +@foo // read +@foo := x1 // write + +@foo { x . body } + + +function Counter() { + let-signal count = 0; + + +} + + + +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 ( +
+ ... +
+); +``` + + +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 +} +``` + diff --git a/src/lang/eval/error.ts b/src/lang/eval/error.ts index bbbc574..018120a 100644 --- a/src/lang/eval/error.ts +++ b/src/lang/eval/error.ts @@ -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 } diff --git a/src/lang/eval/evaluator.ts b/src/lang/eval/evaluator.ts index 9cc109d..77f1735 100644 --- a/src/lang/eval/evaluator.ts +++ b/src/lang/eval/evaluator.ts @@ -17,7 +17,7 @@ export function eval_start(program: Program, e: Expr): Result { } // 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) { diff --git a/src/lang/eval/signal.ts b/src/lang/eval/signal.ts new file mode 100644 index 0000000..926d568 --- /dev/null +++ b/src/lang/eval/signal.ts @@ -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>, +}; + +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 | 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 { + 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 { + state: T, + observers: Observer[], + set(transform: (state: T) => T): void, + read(): T, + subscribe(observer: Observer): void, + map(transform: (state: T) => S): Signal, +}; + +export type Observer = (state: T) => void; + +export function signal(initState: T): Signal { + return { + state: initState, + observers: [], + set(transform: (state: T) => T) { + const state = transform(this.state); + this.state = state; + this.observers.forEach((observer: Observer) => { + observer(state); + }); + }, + read(): T { + return this.state; + }, + subscribe(observer: Observer) { + // TODO: This needs to return `cancellation` + this.observers.push(observer); + }, + map(transform: (state: T) => S): Signal { + const Y = signal(transform(this.state)); + this.subscribe((state: T) => { + Y.set(() => transform(state)); + }); + return Y; + } + }; +} + +function pair(X: Signal, Y: Signal): 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 { + 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; +} + diff --git a/src/lang/expr.ts b/src/lang/expr.ts index 492ad0f..cee8b5d 100644 --- a/src/lang/expr.ts +++ b/src/lang/expr.ts @@ -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 }); diff --git a/src/lang/program.ts b/src/lang/program.ts index 24fe0ee..8fdfa70 100644 --- a/src/lang/program.ts +++ b/src/lang/program.ts @@ -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, - 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 + function_definitions: Map, + function_definition_order: FunctionName[], + + signal_definitions: Map, + 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, -// } - -// 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) { + 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 { + 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): 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 { + 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,