diff --git a/src/lang/eval/error.ts b/src/lang/eval/error.ts index 018120a..fea86b2 100644 --- a/src/lang/eval/error.ts +++ b/src/lang/eval/error.ts @@ -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 = | { tag: "ok", value: T } diff --git a/src/lang/eval/signal.ts b/src/lang/eval/signal.ts index 22a6095..f0c74bd 100644 --- a/src/lang/eval/signal.ts +++ b/src/lang/eval/signal.ts @@ -230,7 +230,6 @@ function tupleThen(Xs: Signal[], f: (values: A[]) => B): Signal { // === Primitive Signals === - export function makeTickSignal(intervalMs: number): Signal { const s = signal(Value.number(0)); diff --git a/src/lang/eval/signalValue.ts b/src/lang/eval/signalValue.ts index 7588629..d8f3ac2 100644 --- a/src/lang/eval/signalValue.ts +++ b/src/lang/eval/signalValue.ts @@ -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; +} + diff --git a/src/lang/eval/value.ts b/src/lang/eval/value.ts index b675413..239ca93 100644 --- a/src/lang/eval/value.ts +++ b/src/lang/eval/value.ts @@ -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; + case "string": + return v1.value === (v2 as Extract).value; + case "tag": + return v1.tag_name === (v2 as Extract).tag_name; + case "tagged": { + const other = v2 as Extract; + return v1.tag_name === other.tag_name && equals(v1.value, other.value); + } + case "tuple": { + const other = v2 as Extract; + 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; + 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 + }); +} +