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: "TypeMismatch", expected: ValueTag, received: Value }
|
||||||
| { tag: "DuplicateVariableNamesInPattern", pattern: Pattern, duplicates: VariableName[] }
|
| { tag: "DuplicateVariableNamesInPattern", pattern: Pattern, duplicates: VariableName[] }
|
||||||
// | { tag: "DuplicateVariableNamesInProductPattern", pattern: ProductPattern, 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> =
|
export type Result<T> =
|
||||||
| { tag: "ok", value: T }
|
| { tag: "ok", value: T }
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,6 @@ function tupleThen<A, B>(Xs: Signal<A>[], f: (values: A[]) => B): Signal<B> {
|
||||||
|
|
||||||
|
|
||||||
// === Primitive Signals ===
|
// === Primitive Signals ===
|
||||||
|
|
||||||
export function makeTickSignal(intervalMs: number): Signal<Value> {
|
export function makeTickSignal(intervalMs: number): Signal<Value> {
|
||||||
const s = signal(Value.number(0));
|
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 { Program } from "../program";
|
||||||
import { Result, RuntimeError, ThrownRuntimeError } from "./error";
|
import { Result, RuntimeError, ThrownRuntimeError } from "./error";
|
||||||
import { eval_expr } from "./evaluator";
|
import { eval_expr } from "./evaluator";
|
||||||
import { match_product_pattern } from "./pattern_match";
|
import { match_product_pattern } from "./pattern_match";
|
||||||
import { Env, Value } from "./value";
|
import { Env, equals, forceBool, Value } from "./value";
|
||||||
import { PriorityQueue } from "./priority_queue"
|
import { PriorityQueue } from "./priority_queue"
|
||||||
|
|
||||||
// === Reactive DAG ===
|
// === 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 {
|
export function spawnSignal(program: Program, signalName: SignalName, expr: SignalExpr): SignalRuntime.DAGNode {
|
||||||
const maybeNode = program.signal_runtimeNew.store.get(signalName);
|
const maybeNode = program.signal_runtimeNew.store.get(signalName);
|
||||||
if (maybeNode !== undefined) {
|
if (maybeNode !== undefined) {
|
||||||
|
|
@ -82,6 +74,33 @@ export function spawnSignal(program: Program, signalName: SignalName, expr: Sign
|
||||||
return 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);
|
||||||
|
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
|
// TODO: Should take in a `SignalName`, and find `oldNode` first
|
||||||
export function hotSwapSignal(program: Program, oldNode: SignalRuntime.DAGNode, expr: SignalExpr) {
|
export function hotSwapSignal(program: Program, oldNode: SignalRuntime.DAGNode, expr: SignalExpr) {
|
||||||
reeval_signal_expression(program, Env.nil(), oldNode, expr)
|
reeval_signal_expression(program, Env.nil(), oldNode, expr)
|
||||||
|
|
@ -166,13 +185,26 @@ function wouldCreateCycle(node: SignalRuntime.DAGNode, newParents: SignalRuntime
|
||||||
|
|
||||||
type SignalValue =
|
type SignalValue =
|
||||||
| { tag: "closure", closure: SignalClosure }
|
| { 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 }
|
| { tag: "proxy", parent: SignalRuntime.DAGNode }
|
||||||
|
|
||||||
type SignalClosure = {
|
type SignalClosure = {
|
||||||
bindings: SignalBinding[],
|
bindings: SignalBinding[],
|
||||||
env: Env,
|
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 = {
|
type SignalBinding = {
|
||||||
|
|
@ -363,7 +395,7 @@ function eval_signal_closure(program: Program, closure: SignalClosure): Value {
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace SignalValue {
|
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 closure = (closure: SignalClosure): SignalValue => ({ tag: "closure", closure });
|
||||||
export const proxy = (parent: SignalRuntime.DAGNode): SignalValue => ({ tag: "proxy", parent });
|
export const proxy = (parent: SignalRuntime.DAGNode): SignalValue => ({ tag: "proxy", parent });
|
||||||
}
|
}
|
||||||
|
|
@ -385,10 +417,9 @@ function propagate(program: Program, rootNode: SignalRuntime.DAGNode) {
|
||||||
while (!heap.isEmpty()) {
|
while (!heap.isEmpty()) {
|
||||||
const node = heap.pop()!;
|
const node = heap.pop()!;
|
||||||
switch (node.signal.tag) {
|
switch (node.signal.tag) {
|
||||||
case "const":
|
case "source":
|
||||||
break;
|
break;
|
||||||
case "proxy": {
|
case "proxy": {
|
||||||
// TODO: new PROXY code: Is this correct?
|
|
||||||
const value = node.internalInputs[0].currentValue;
|
const value = node.internalInputs[0].currentValue;
|
||||||
node.currentValue = value;
|
node.currentValue = value;
|
||||||
descendantsWithExternalEffects.push(node);
|
descendantsWithExternalEffects.push(node);
|
||||||
|
|
@ -398,8 +429,34 @@ function propagate(program: Program, rootNode: SignalRuntime.DAGNode) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "closure": {
|
case "closure": {
|
||||||
// TODO: This may throw an exception!
|
const closure = node.signal.closure;
|
||||||
const value = eval_signal_closure(program, 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;
|
node.currentValue = value;
|
||||||
descendantsWithExternalEffects.push(node);
|
descendantsWithExternalEffects.push(node);
|
||||||
for (const childNode of node.internalOutputs) {
|
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