From 1ad1c8c4422a101965bbe511e4ffbf9eb76d92a3 Mon Sep 17 00:00:00 2001 From: Yura Dupyn <2153100+omedusyo@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:08:22 +0100 Subject: [PATCH] Start preparing primitives for UI --- src/lang/eval/signal.ts | 11 ++- src/lang/program.ts | 146 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 4 deletions(-) diff --git a/src/lang/eval/signal.ts b/src/lang/eval/signal.ts index 926d568..a250f3d 100644 --- a/src/lang/eval/signal.ts +++ b/src/lang/eval/signal.ts @@ -1,7 +1,7 @@ -import { SignalExpr } from "../expr"; +import { Expr, SignalExpr, SignalName } from "../expr"; import { Program } from "../program"; import { Result, RuntimeError, ThrownRuntimeError } from "./error"; -import { eval_expr } from "./evaluator"; +import { eval_expr, eval_start } from "./evaluator"; import { Env, Value } from "./value"; @@ -50,6 +50,13 @@ export function eval_signal_expression(program: Program, e: SignalExpr): Signal< } } +// may throw `ThrownRuntimeError` +export function signal_set_value(program: Program, sig: Signal, e: Expr): Value { + const value = eval_expr(program, Env.nil(), e); + sig.set(() => value); + return value; +} + export interface Signal { state: T, observers: Observer[], diff --git a/src/lang/program.ts b/src/lang/program.ts index 8fdfa70..48a203f 100644 --- a/src/lang/program.ts +++ b/src/lang/program.ts @@ -1,8 +1,9 @@ -import { eval_signal_expression, makeTickSignal, Signal, SignalId, SignalRuntime } from "./eval/signal"; +import { eval_signal_expression, makeTickSignal, signal, Signal, SignalId, SignalRuntime } from "./eval/signal"; import { ThrownRuntimeError } from "./eval/error"; -import { Value } from "./eval/value"; +import { Env, Value } from "./eval/value"; import { Expr, FunctionName, SignalName, ProductPattern, SignalExpr } from "./expr"; import { installPrimitives } from "./primitive"; +import { eval_expr } from "./eval/evaluator"; export type Timestamp = number; @@ -47,6 +48,7 @@ export type Implementation = (args: Value[]) => Value // === Signals === export type SignalDefinition = | { tag: "user", def: UserSignalDefinition } + | { tag: "cell", def: CellDefinition } | { tag: "primitive", def: PrimitiveSignalDefinition } type UserSignalDefinition = { @@ -62,6 +64,19 @@ type UserSignalDefinition = { lastModifiedAt: Timestamp, } +type CellDefinition = { + name: SignalName, + raw_body: string, + + body: Expr, + + signalId?: SignalId, + + // Metadata + createdAt: Timestamp, + lastModifiedAt: Timestamp, +} + type PrimitiveSignalDefinition = { name: FunctionName, signalId: SignalId, @@ -76,6 +91,12 @@ export namespace Program { | { tag: "CannotDeletePrimitiveFunction", name: FunctionName } | { tag: "PrimitiveFunctionAlreadyExists", name: FunctionName } + | { tag: "DuplicateSignalName", name: SignalName } + | { tag: "SignalNotFound", name: SignalName } + | { tag: "CannotEditPrimitiveSignal", name: SignalName } + | { tag: "CannotDeletePrimitiveSignal", name: SignalName } + | { tag: "PrimitiveSignalAlreadyExists", name: SignalName } + type Result = | { tag: "ok", value: T } | { tag: "error", error: Error } @@ -185,6 +206,24 @@ export namespace Program { 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; + } + } + + // 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; + } case "primitive": { const def = sigDef.def; const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId); @@ -233,6 +272,7 @@ export namespace Program { } } + // === Functions === export type CreateFunction = { name: FunctionName, parameters: ProductPattern[], @@ -316,5 +356,107 @@ export namespace Program { return Result.ok(undefined); } + // === Signals === + export type CreateSignal = { + name: SignalName, + body: SignalExpr, + raw_body: string, + } + + export function registerSignal( + program: Program, + { name, body, raw_body }: CreateSignal + ): Result { + if (program.signal_definitions.has(name)) { + return Result.error({ tag: "DuplicateSignalName", name }); + } + + const now: Timestamp = Date.now(); + + const newSignal: UserSignalDefinition = { + name, + raw_body, + body, + is_initializing: false, + signalId: undefined, // Start uncompiled + + createdAt: now, + lastModifiedAt: now, + }; + + program.signal_definitions.set(name, { tag: "user", def: newSignal }); + program.signal_definition_order.push(name); + + // TODO: Note that this doesn't actually evaluate the signal and doesn't insert it into signal-runtime. + // For that we will use `get_or_create_signal` + + return Result.ok(undefined); + } + + export type UpdateSignal = { + body: SignalExpr, + raw_body: string, + } + +export function updateSignal( + program: Program, + name: SignalName, + { body, raw_body }: UpdateSignal + ): Result { + const existingEntry = program.signal_definitions.get(name); + + if (existingEntry === undefined) { + return Result.error({ tag: "SignalNotFound", name } as any); + } + + if (existingEntry.tag === "primitive") { + return Result.error({ tag: "CannotEditPrimitiveSignal", name } as any); + } + + const def = existingEntry.def; + def.body = body; + def.raw_body = raw_body; + def.lastModifiedAt = Date.now(); + + // TODO: When to recompile? + // 2. CRITICAL: Invalidate the Runtime Cache + // We clear the ID so the next 'read' forces a re-compile. + // Note: This does NOT automatically update other signals that + // are currently holding a reference to the *old* signal ID. + // That requires a more complex hot-reload strategy, but this + // is the correct first step. + def.signalId = undefined; + def.is_initializing = false; + + return Result.ok(undefined); + } + + 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.tag === "primitive") { + return Result.error({ tag: "CannotDeletePrimitiveSignal", name } as any); + } + + program.signal_definitions.delete(name); + + 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. + + return Result.ok(undefined); + } + }