Finally can run the UI

This commit is contained in:
Yura Dupyn 2026-02-18 01:20:16 +01:00
parent 115b457173
commit 49ef33f113
3 changed files with 95 additions and 159 deletions

View file

@ -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<SignalName, SignalRuntime.DAGNode>,
// 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) {

View file

@ -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<FunctionName, FunctionDefinition>,
function_definition_order: FunctionName[],
@ -15,9 +16,6 @@ export type Program = {
signal_definitions: Map<SignalName, SignalDefinition>,
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<Value>) {
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<Value> {
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<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 "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;
}
}
}
@ -468,32 +398,33 @@ export function updateSignal(
return Result.ok(undefined);
}
export function deleteSignal(program: Program, name: SignalName): Result<void> {
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<void> {
// 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<void> {
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<void> {
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);
}

View file

@ -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);