Start preparing primitives for UI

This commit is contained in:
Yura Dupyn 2026-02-09 18:08:22 +01:00
parent 5e7578c4a3
commit 1ad1c8c442
2 changed files with 153 additions and 4 deletions

View file

@ -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<Value>, e: Expr): Value {
const value = eval_expr(program, Env.nil(), e);
sig.set(() => value);
return value;
}
export interface Signal<T> {
state: T,
observers: Observer<T>[],

View file

@ -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<T> =
| { 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<void> {
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<void> {
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<void> {
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);
}
}