structural equality, booleans, relational barriers for propagation
This commit is contained in:
parent
6cca0d17a1
commit
115b457173
4 changed files with 167 additions and 18 deletions
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
// TODO: This may throw an exception!
|
||||
const value = eval_signal_closure(program, node.signal.closure);
|
||||
const closure = node.signal.closure;
|
||||
// TODO: This may throw an exception!
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue