126 lines
3.4 KiB
JavaScript
126 lines
3.4 KiB
JavaScript
/**
|
|
* tree.js — Runtime tree representation.
|
|
*
|
|
* The JS tree uses a simple array representation matching the
|
|
* TypeScript reference evaluator:
|
|
*
|
|
* Leaf = []
|
|
* Stem = [child] (array length === 1)
|
|
* Fork = [right, left] (array length === 2)
|
|
*
|
|
* This is a "flattened stack" representation: when reduced, terms
|
|
* become arrays and the evaluator pops three elements at a time.
|
|
*/
|
|
|
|
/**
|
|
* Check if a value is a Leaf (empty array).
|
|
*/
|
|
export function isLeaf(t) {
|
|
return Array.isArray(t) && t.length === 0;
|
|
}
|
|
|
|
/**
|
|
* Check if a value is a Stem (single element array).
|
|
*/
|
|
export function isStem(t) {
|
|
return Array.isArray(t) && t.length === 1;
|
|
}
|
|
|
|
/**
|
|
* Check if a value is a Fork (two element array).
|
|
*/
|
|
export function isFork(t) {
|
|
return Array.isArray(t) && t.length === 2;
|
|
}
|
|
|
|
/**
|
|
* Check if a value is a valid tree calculus value (Leaf, Stem, or Fork).
|
|
*/
|
|
export function isTree(t) {
|
|
return isLeaf(t) || isStem(t) || isFork(t);
|
|
}
|
|
|
|
/**
|
|
* Triage a tree: classify it as Leaf/Stem/Fork.
|
|
* The tree must be in normal form (no reducible redexes).
|
|
*
|
|
* Returns { kind: "leaf"|"stem"|"fork", ...rest }
|
|
*/
|
|
export function triage(t) {
|
|
if (!Array.isArray(t)) {
|
|
throw new Error("not a tree (not an array)");
|
|
}
|
|
if (t.length === 0) return { kind: "leaf" };
|
|
if (t.length === 1) return { kind: "stem", child: t[0] };
|
|
if (t.length === 2) return { kind: "fork", right: t[0], left: t[1] };
|
|
throw new Error(`not a value/binary tree: length ${t.length}`);
|
|
}
|
|
|
|
/**
|
|
* Apply the Tree Calculus apply rules.
|
|
*
|
|
* apply(a, b) computes the application of term a to term b.
|
|
*
|
|
* Rules:
|
|
* apply(Fork(Leaf, a), _) = a
|
|
* apply(Fork(Stem(a), b), c) = apply(apply(a, c), apply(b, c))
|
|
* apply(Fork(Fork, _, _), Leaf) = left of inner Fork
|
|
* apply(Fork(Fork, _, _), Stem) = right of inner Fork
|
|
* apply(Fork(Fork, _, _), Fork) = apply(apply(c, u), v) where c=Fork(u,v)
|
|
* apply(Leaf, b) = Stem(b)
|
|
* apply(Stem(a), b) = Fork(a, b)
|
|
*
|
|
* For Fork, the inner structure is [right, left], so:
|
|
* a = right, b = left
|
|
*/
|
|
export function apply(a, b) {
|
|
// apply(Fork(Leaf, a), _) = a
|
|
// Fork = [right, left] = [Leaf, a] → left child is Leaf
|
|
if (isFork(a) && isLeaf(a[1])) {
|
|
return a[0]; // return right child
|
|
}
|
|
|
|
// apply(Fork(Stem(a), b), c)
|
|
if (isFork(a) && isStem(a[1])) {
|
|
const stemChild = a[1][0]; // left child of fork
|
|
const right = a[0]; // right child of fork
|
|
const innerA = stemChild;
|
|
const innerB = right;
|
|
const appliedA = apply(innerA, b);
|
|
const appliedB = apply(innerB, b);
|
|
return apply(appliedA, appliedB);
|
|
}
|
|
|
|
// apply(Fork(Fork, _, _), Leaf)
|
|
if (isFork(a) && isFork(a[1]) && isLeaf(b)) {
|
|
return a[1][0]; // right child of inner fork (which is left child)
|
|
}
|
|
|
|
// apply(Fork(Fork, _, _), Stem)
|
|
if (isFork(a) && isFork(a[1]) && isStem(b)) {
|
|
return a[1][1]; // left child of inner fork
|
|
}
|
|
|
|
// apply(Fork(Fork, _, _), Fork)
|
|
if (isFork(a) && isFork(a[1]) && isFork(b)) {
|
|
// b = Fork(u, v) = [v, u]
|
|
const u = b[0];
|
|
const v = b[1];
|
|
// apply(apply(c, u), v) where c = inner fork
|
|
const applied = apply(apply(a[1], u), v);
|
|
return applied;
|
|
}
|
|
|
|
// apply(Leaf, b) = Stem(b)
|
|
if (isLeaf(a)) {
|
|
return [b];
|
|
}
|
|
|
|
// apply(Stem(a), b) = Fork(a, b)
|
|
if (isStem(a)) {
|
|
return [b, a[0]]; // [right, left]
|
|
}
|
|
|
|
throw new Error("apply: undefined reduction for terms");
|
|
}
|