diff --git a/src/lang/eval/signalValue.ts b/src/lang/eval/signalValue.ts index d8f3ac2..50255b7 100644 --- a/src/lang/eval/signalValue.ts +++ b/src/lang/eval/signalValue.ts @@ -11,6 +11,7 @@ import { PriorityQueue } from "./priority_queue" export type SignalRuntime = { // Named signals (there are also nameless ones - and the distinction matters for garbage collection) store: Map, + // TODO: consider tracking root nodes (and even the signal-closures with 0 bindings). Would be useful to build a global visualization of the DAG. }; export type ExternalObserver = (state: Value) => void; @@ -18,6 +19,7 @@ export type UnsubscribeCapability = () => void; export namespace SignalRuntime { // TODO: This is a terrible name. Looking for a new name... Just don't call it `Node`, which clashes with the builtin ts `Node` type all the time. + // TODO: consider naming this `SignalId` actually... export type DAGNode = { signalName?: SignalName, signal: SignalValue, @@ -53,7 +55,7 @@ export function externalSubscribe(node: SignalRuntime.DAGNode, observer: Externa } export function getNode(program: Program, signalName: SignalName): SignalRuntime.DAGNode { - const maybeNode = program.signal_runtimeNew.store.get(signalName); + const maybeNode = program.signal_runtime.store.get(signalName); if (maybeNode === undefined) { // TODO: Make this into a proper error throw Error(`Signal '${signalName}' not found!`); @@ -63,19 +65,19 @@ export function getNode(program: Program, signalName: SignalName): SignalRuntime } export function spawnSignal(program: Program, signalName: SignalName, expr: SignalExpr): SignalRuntime.DAGNode { - const maybeNode = program.signal_runtimeNew.store.get(signalName); + const maybeNode = program.signal_runtime.store.get(signalName); if (maybeNode !== undefined) { // TODO: Make this into a proper error throw Error(`Attempt to spawn a signal '${signalName}' that already exists`); } const node = eval_signal_expression(program, Env.nil(), expr); node.signalName = signalName; - program.signal_runtimeNew.store.set(signalName, node); + program.signal_runtime.store.set(signalName, node); return node; } export function spawnSource(program: Program, signalName: SignalName, initValue: Value): [SignalRuntime.DAGNode, (value: Value) => void] { - const maybeNode = program.signal_runtimeNew.store.get(signalName); + const maybeNode = program.signal_runtime.store.get(signalName); if (maybeNode !== undefined) { // TODO: Make this into a proper error throw Error(`Attempt to spawn a signal '${signalName}' that already exists`); @@ -90,7 +92,7 @@ export function spawnSource(program: Program, signalName: SignalName, initValue: externalOutputs: [], currentValue: initValue, }; - program.signal_runtimeNew.store.set(signalName, node); + program.signal_runtime.store.set(signalName, node); function setValue(value: Value) { diff --git a/src/lang/program.ts b/src/lang/program.ts index d348a58..61d9ed0 100644 --- a/src/lang/program.ts +++ b/src/lang/program.ts @@ -1,13 +1,14 @@ -import { eval_signal_expression, makeTickSignal, signal, Signal, SignalId, SignalRuntime } from "./eval/signal"; import { ThrownRuntimeError } from "./eval/error"; import { Env, Value } from "./eval/value"; import { Expr, FunctionName, SignalName, ProductPattern, SignalExpr } from "./expr"; import { installPrimitives } from "./primitive"; import { eval_expr } from "./eval/evaluator"; -import { SignalRuntime as SignalRuntimeNew} from "./eval/signalValue" +import { eval_signal_expression, spawnTick, spawnSignal, SignalRuntime, getNode, spawnSource } from "./eval/signalValue" export type Timestamp = number; +type SignalId = SignalRuntime.DAGNode; + export type Program = { function_definitions: Map, function_definition_order: FunctionName[], @@ -15,9 +16,6 @@ export type Program = { signal_definitions: Map, signal_definition_order: SignalName[], - signal_runtimeNew: SignalRuntimeNew, - - // TODO: Get rid of the old Runtime signal_runtime: SignalRuntime, }; @@ -74,7 +72,7 @@ type CellDefinition = { body: Expr, - signalId?: SignalId, + cell?: [SignalId, (value: Value) => void], // Metadata createdAt: Timestamp, @@ -82,7 +80,7 @@ type CellDefinition = { } type PrimitiveSignalDefinition = { - name: FunctionName, + name: SignalName, signalId: SignalId, } @@ -129,7 +127,6 @@ export namespace Program { function_definitions: new Map(), function_definition_order: [], - signal_runtimeNew: SignalRuntimeNew.make(), signal_runtime: SignalRuntime.make(), signal_definitions: new Map(), signal_definition_order: [], @@ -167,11 +164,11 @@ export namespace Program { } function install_primitive_signals(program: Program) { - install_primitive_signal(program, "tick",makeTickSignal(1000)); + const signalId = spawnTick(program, "tick", 1000); + install_primitive_signal(program, "tick", signalId); } - function install_primitive_signal(program: Program, name: SignalName, signal: Signal) { - const signalId = attachNewSignal(program, signal); + function install_primitive_signal(program: Program, name: SignalName, signalId: SignalId) { const def: SignalDefinition = { tag: "primitive", def: { name, signalId } @@ -180,19 +177,14 @@ export namespace Program { } // may throw `ThrownRuntimeError`. This is used during initialization. - export function get_or_create_signal(program: Program, name: SignalName): Signal { + export function get_or_create_signal(program: Program, name: SignalName): SignalId { 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; - } + return def.signalId; } // We need to create the signal for the first time. @@ -202,90 +194,28 @@ export namespace Program { def.is_initializing = true; try { - const newSignal = eval_signal_expression(program, Env.nil(), def.body); - const newId = attachNewSignal(program, newSignal); - program.signal_runtime.store.set(newId, newSignal); + const newId = spawnSignal(program, name, def.body); def.signalId = newId; - return newSignal; + return newId; } finally { def.is_initializing = false; } } case "cell": { 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; - } + if (def.cell !== undefined) { + return def.cell[0]; } // We need to create the cell-signal for the first time. const initialValue = eval_expr(program, Env.nil(), def.body); - const sig = signal(initialValue); - const id = attachNewSignal(program, sig); - def.signalId = id; - return sig; + const cell = spawnSource(program, name, initialValue) + def.cell = cell; + return cell[0]; } 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 "cell": { - 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; - } + return sigDef.def.signalId; } } } @@ -431,7 +361,7 @@ export namespace Program { raw_body: string, } -export function updateSignal( + export function updateSignal( program: Program, name: SignalName, { body, raw_body }: UpdateSignal @@ -468,32 +398,33 @@ export function updateSignal( return Result.ok(undefined); } - export function deleteSignal(program: Program, name: SignalName): Result { - const existingEntry = program.signal_definitions.get(name); + // TODO: This needs careful thought about transitive dependencies of a signal + // export function deleteSignal(program: Program, name: SignalName): Result { + // const existingEntry = program.signal_definitions.get(name); - if (!existingEntry) { - return Result.error({ tag: "SignalNotFound", name } as any); - } + // if (!existingEntry) { + // return Result.error({ tag: "SignalNotFound", name } as any); + // } - if (existingEntry.tag === "primitive") { - return Result.error({ tag: "CannotDeletePrimitiveSignal", name } as any); - } + // if (existingEntry.tag === "primitive") { + // return Result.error({ tag: "CannotDeletePrimitiveSignal", name } as any); + // } - program.signal_definitions.delete(name); + // program.signal_definitions.delete(name); - const orderIndex = program.signal_definition_order.indexOf(name); - if (orderIndex !== -1) { - program.signal_definition_order.splice(orderIndex, 1); - } + // const orderIndex = program.signal_definition_order.indexOf(name); + // if (orderIndex !== -1) { + // program.signal_definition_order.splice(orderIndex, 1); + // } - // TODO: - // Note: The old signal instance still exists in program.signal_runtime.store - // We technically leak memory here unless we also remove it from the runtime store. - // However, since other signals might still depend on that ID, - // leaving it is actually safer for now to prevent crashes. + // // TODO: + // // Note: The old signal instance still exists in program.signal_runtime.store + // // We technically leak memory here unless we also remove it from the runtime store. + // // However, since other signals might still depend on that ID, + // // leaving it is actually safer for now to prevent crashes. - return Result.ok(undefined); - } + // return Result.ok(undefined); + // } // === Cells === export type CreateCell = { @@ -502,34 +433,35 @@ export function updateSignal( raw_body: string, } + // TODO: refactor pending export function registerCell( program: Program, { name, body, raw_body }: CreateCell ): Result { - if (program.signal_definitions.has(name)) { - return Result.error({ tag: "DuplicateSignalName", name }); - } + // if (program.signal_definitions.has(name)) { + // return Result.error({ tag: "DuplicateSignalName", name }); + // } - const now: Timestamp = Date.now(); + // const now: Timestamp = Date.now(); - // TODO: MAY THROW RuntimeError. Should probably switch to `eval_start` - and extend the `Program.Error` with runtime errors. - const initialValue = eval_expr(program, Env.nil(), body); + // // TODO: MAY THROW RuntimeError. Should probably switch to `eval_start` - and extend the `Program.Error` with runtime errors. + // const initialValue = eval_expr(program, Env.nil(), body); - const sig = signal(initialValue); - const signalId = attachNewSignal(program, sig); + // const sig = signal(initialValue); + // const signalId = attachNewSignal(program, sig); - const newCell: CellDefinition = { - name, - raw_body, - body, - signalId, - createdAt: now, - lastModifiedAt: now, - }; + // const newCell: CellDefinition = { + // name, + // raw_body, + // body, + // signalId, + // createdAt: now, + // lastModifiedAt: now, + // }; - program.signal_definitions.set(name, { tag: "cell", def: newCell }); - program.signal_definition_order.push(name); + // program.signal_definitions.set(name, { tag: "cell", def: newCell }); + // program.signal_definition_order.push(name); return Result.ok(undefined); } @@ -539,44 +471,45 @@ export function updateSignal( raw_body: string, } + // TODO: refactor pending export function updateCell( program: Program, name: SignalName, { body, raw_body }: UpdateCell ): Result { - const existingEntry = program.signal_definitions.get(name); + // const existingEntry = program.signal_definitions.get(name); - if (!existingEntry) { - return Result.error({ tag: "SignalNotFound", name } as any); - } + // if (!existingEntry) { + // return Result.error({ tag: "SignalNotFound", name } as any); + // } - // Ensure we are editing a Cell - if (existingEntry.tag !== "cell") { - return Result.error({ tag: "CannotEditCell", name } as any); - } + // // Ensure we are editing a Cell + // if (existingEntry.tag !== "cell") { + // return Result.error({ tag: "CannotEditCell", name } as any); + // } - const def = existingEntry.def; + // const def = existingEntry.def; - // TODO: MAY THROW RuntimeError. Should probably switch to `eval_start` - and extend the `Program.Error` with runtime errors. - const newValue = eval_expr(program, Env.nil(), body); + // // TODO: MAY THROW RuntimeError. Should probably switch to `eval_start` - and extend the `Program.Error` with runtime errors. + // const newValue = eval_expr(program, Env.nil(), body); - // Find the existing runtime signal - if (def.signalId === undefined) { - // This should theoretically not happen for cells since we initialize them eagerly, - // but good to be safe. - return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any); - } + // // Find the existing runtime signal + // if (def.signalId === undefined) { + // // This should theoretically not happen for cells since we initialize them eagerly, + // // but good to be safe. + // return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any); + // } - const sig = SignalRuntime.getSignal(program.signal_runtime, def.signalId); - if (!sig) { - return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any); - } + // const sig = SignalRuntime.getSignal(program.signal_runtime, def.signalId); + // if (!sig) { + // return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any); + // } - sig.set(() => newValue); + // sig.set(() => newValue); - def.body = body; - def.raw_body = raw_body; - def.lastModifiedAt = Date.now(); + // def.body = body; + // def.raw_body = raw_body; + // def.lastModifiedAt = Date.now(); return Result.ok(undefined); } diff --git a/src/ui/Digith/Signal/SignalDigith.tsx b/src/ui/Digith/Signal/SignalDigith.tsx index 6f1f105..d323ebe 100644 --- a/src/ui/Digith/Signal/SignalDigith.tsx +++ b/src/ui/Digith/Signal/SignalDigith.tsx @@ -10,6 +10,7 @@ import { updateDigith } from "src/ui/Scrowl/scrowlStore"; import { DigithError } from "src/ui/Digith/DigithError"; import { Value } from "src/lang/eval/value"; import { Val } from "src/ui/Component/Value"; +import { externalSubscribe } from "src/lang/eval/signalValue"; type Input = { raw_body: string, @@ -50,9 +51,9 @@ export function SignalDigith(props: { signal: Digith.Signal }) { const signal = Program.get_or_create_signal(program, props.signal.name); // TODO: Not sure about this one. Setting a signal in `setValue` is discouraged. - setTimeout(() => setValue(signal.read()), 0); + setTimeout(() => setValue(signal.currentValue), 0); - const cancel = signal.subscribe((newValue) => { + const cancel = externalSubscribe(signal, (newValue) => { setValue(newValue); }); onCleanup(cancel);