structural equality, booleans, relational barriers for propagation

This commit is contained in:
Yura Dupyn 2026-02-18 00:29:17 +01:00
parent 6cca0d17a1
commit 115b457173
4 changed files with 167 additions and 18 deletions

View file

@ -14,6 +14,8 @@ export type RuntimeError =
| { tag: "TypeMismatch", expected: ValueTag, received: Value }
| { tag: "DuplicateVariableNamesInPattern", pattern: Pattern, duplicates: VariableName[] }
// | { tag: "DuplicateVariableNamesInProductPattern", pattern: ProductPattern, duplicates: VariableName[] }
| { tag: "ClosureEqualityComparison", value0: Closure, value1: Value } // Closures cannot be compared for equality
| { tag: "NotABoolean", value: Value } // Attempt to use a non-boolean when a boolean is expected
export type Result<T> =
| { tag: "ok", value: T }

View file

@ -230,7 +230,6 @@ function tupleThen<A, B>(Xs: Signal<A>[], f: (values: A[]) => B): Signal<B> {
// === Primitive Signals ===
export function makeTickSignal(intervalMs: number): Signal<Value> {
const s = signal(Value.number(0));

View file

@ -1,9 +1,9 @@
import { Expr, ProductPattern, SignalExpr, SignalName } from "../expr";
import { Expr, ProductPattern, SignalExpr, SignalName, VariableName } from "../expr";
import { Program } from "../program";
import { Result, RuntimeError, ThrownRuntimeError } from "./error";
import { eval_expr } from "./evaluator";
import { match_product_pattern } from "./pattern_match";
import { Env, Value } from "./value";
import { Env, equals, forceBool, Value } from "./value";
import { PriorityQueue } from "./priority_queue"
// === Reactive DAG ===
@ -62,14 +62,6 @@ export function getNode(program: Program, signalName: SignalName): SignalRuntime
}
}
// TODO: This feels wrong. We shouldn't allow setting of arbitrary signals... only special signals that are "cells",
// and are meant to be modified by the user...
export function setSignal(program: Program, name: SignalName, value: Value) {
const node = getNode(program, name);
node.currentValue = value;
propagate(program, node);
}
export function spawnSignal(program: Program, signalName: SignalName, expr: SignalExpr): SignalRuntime.DAGNode {
const maybeNode = program.signal_runtimeNew.store.get(signalName);
if (maybeNode !== undefined) {
@ -82,6 +74,33 @@ export function spawnSignal(program: Program, signalName: SignalName, expr: Sign
return node;
}
export function spawnSource(program: Program, signalName: SignalName, initValue: Value): [SignalRuntime.DAGNode, (value: Value) => void] {
const maybeNode = program.signal_runtimeNew.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: SignalRuntime.DAGNode = {
signal: SignalValue.const_(initValue),
signalName,
rank: 0,
internalInputs: [],
internalOutputs: [],
externalOutputs: [],
currentValue: initValue,
};
program.signal_runtimeNew.store.set(signalName, node);
function setValue(value: Value) {
node.currentValue = value;
propagate(program, node);
}
return [node, setValue];
}
// TODO: Should take in a `SignalName`, and find `oldNode` first
export function hotSwapSignal(program: Program, oldNode: SignalRuntime.DAGNode, expr: SignalExpr) {
reeval_signal_expression(program, Env.nil(), oldNode, expr)
@ -166,13 +185,26 @@ function wouldCreateCycle(node: SignalRuntime.DAGNode, newParents: SignalRuntime
type SignalValue =
| { tag: "closure", closure: SignalClosure }
| { tag: "const", value: Value } // Is this really necessary? Yes... its not a special case of SignalClosure - it has a value as a body, not an expression...
// "source" is either a constant signal, or an external-input signal. The point is that these have no internal-inputs, i.e. rank-0 nodes.
| { tag: "source", value: Value } // Is this really necessary? Yes... its not a special case of SignalClosure - it has a value as a body, not an expression...
| { tag: "proxy", parent: SignalRuntime.DAGNode }
type SignalClosure = {
bindings: SignalBinding[],
env: Env,
body: Expr
body: Expr,
barrier?: Barrier, // Affects how propagation is done
}
type Barrier =
| { tag: "eq" }
// User-defined predicate (old, new) -> bool. What value is considered to be the canonical bool?
| { tag: "relation", relation: Relation }
type Relation = {
oldVar: VariableName,
newVar: VariableName,
body: Expr,
}
type SignalBinding = {
@ -363,7 +395,7 @@ function eval_signal_closure(program: Program, closure: SignalClosure): Value {
}
export namespace SignalValue {
export const const_ = (value: Value): SignalValue => ({ tag: "const", value });
export const const_ = (value: Value): SignalValue => ({ tag: "source", value });
export const closure = (closure: SignalClosure): SignalValue => ({ tag: "closure", closure });
export const proxy = (parent: SignalRuntime.DAGNode): SignalValue => ({ tag: "proxy", parent });
}
@ -385,10 +417,9 @@ function propagate(program: Program, rootNode: SignalRuntime.DAGNode) {
while (!heap.isEmpty()) {
const node = heap.pop()!;
switch (node.signal.tag) {
case "const":
case "source":
break;
case "proxy": {
// TODO: new PROXY code: Is this correct?
const value = node.internalInputs[0].currentValue;
node.currentValue = value;
descendantsWithExternalEffects.push(node);
@ -398,8 +429,34 @@ function propagate(program: Program, rootNode: SignalRuntime.DAGNode) {
break;
}
case "closure": {
const closure = node.signal.closure;
// TODO: This may throw an exception!
const value = eval_signal_closure(program, node.signal.closure);
const value = eval_signal_closure(program, closure);
if (closure.barrier !== undefined) {
switch (closure.barrier.tag) {
case "eq":
// TODO: This may throw an exception!
if (equals(value, node.currentValue)) {
continue
} else {
break;
}
case "relation":
// TODO: This may throw an exception!
const should_propagate = eval_relation(
program,
closure.env,
node.currentValue,
value,
closure.barrier.relation
);
if (!should_propagate) {
continue
} else {
break;
}
}
}
node.currentValue = value;
descendantsWithExternalEffects.push(node);
for (const childNode of node.internalOutputs) {
@ -419,3 +476,38 @@ function propagate(program: Program, rootNode: SignalRuntime.DAGNode) {
}
function eval_relation(
program: Program,
env: Env,
old_value: Value,
new_value: Value,
relation: Relation
): boolean {
const frame = Env.nil_frame();
Env.frame_insert_mut(frame, relation.oldVar, old_value);
Env.frame_insert_mut(frame, relation.newVar, new_value);
const extendedEnv = Env.push_frame(env, frame);
// TODO: This may throw an exception!
const result = eval_expr(program, extendedEnv, relation.body);
// TODO: This may throw an exception!
return forceBool(result);
}
// ===External Input Sources===
export function spawnTick(program: Program, signalName: SignalName, intervalMs: number): SignalRuntime.DAGNode {
const [node, setValue] = spawnSource(program, signalName, Value.number(0));
setInterval(() => {
const state = node.currentValue;
if (state.tag === "number"){
setValue(Value.number(state.value + 1))
} else {
console.log(`Something is really wrong with the state of tick-signal '${signalName}'`, state);
}
}, intervalMs);
return node;
}

View file

@ -75,3 +75,59 @@ export namespace Env {
}
}
export function equals(v1: Value, v2: Value): boolean {
if (v1 === v2) return true; // Reference equality optimization
if (v1.tag !== v2.tag) return false;
switch (v1.tag) {
case "number":
return v1.value === (v2 as Extract<Value, { tag: "number" }>).value;
case "string":
return v1.value === (v2 as Extract<Value, { tag: "string" }>).value;
case "tag":
return v1.tag_name === (v2 as Extract<Value, { tag: "tag" }>).tag_name;
case "tagged": {
const other = v2 as Extract<Value, { tag: "tagged" }>;
return v1.tag_name === other.tag_name && equals(v1.value, other.value);
}
case "tuple": {
const other = v2 as Extract<Value, { tag: "tuple" }>;
if (v1.values.length !== other.values.length) return false;
for (let i = 0; i < v1.values.length; i++) {
if (!equals(v1.values[i], other.values[i])) return false;
}
return true;
}
case "record": {
const other = v2 as Extract<Value, { tag: "record" }>;
if (v1.fields.size !== other.fields.size) return false;
for (const [key, val1] of v1.fields) {
const val2 = other.fields.get(key);
if (val2 === undefined || !equals(val1, val2)) return false;
}
return true;
}
case "closure":
// Philosophical/Mathematical barrier: throw error as requested
throw ThrownRuntimeError.error({
tag: "ClosureEqualityComparison",
value0: v1.closure,
value1: v2,
});
}
}
// Canonical bools are:
// - True is `#T`
// - False is `#F`
// TODO: This is not a great design. Probably introducing completely new values would be better.
export function forceBool(value: Value): boolean {
if (value.tag === "tag") {
if (value.tag_name === "T") return true;
if (value.tag_name === "F") return false;
}
throw ThrownRuntimeError.error({
tag: "NotABoolean",
value
});
}