From 4e0eb0996b9f298086ad4b4a45334d254cdb386a Mon Sep 17 00:00:00 2001 From: Max DeLiso Date: Sun, 27 Apr 2025 16:11:49 -0400 Subject: [PATCH 1/4] add arena evaluator as symbolic alternative, test --- bin/ski.ts | 7 +- lib/evaluator/arenaEvaluator.ts | 183 ++++++++++++++++++++++++++ lib/evaluator/evaluator.ts | 7 + lib/evaluator/skiEvaluator.ts | 16 ++- lib/index.ts | 2 +- lib/ski/arena.ts | 11 ++ lib/ski/church.ts | 4 +- test/conversion/converter.test.ts | 8 +- test/evaluator/arenaEvaluator.test.ts | 94 +++++++++++++ test/evaluator/skiEvaluator.test.ts | 65 +++++++-- test/performance.test.ts | 4 +- test/ski/church.test.ts | 38 +++--- 12 files changed, 396 insertions(+), 43 deletions(-) create mode 100644 lib/evaluator/arenaEvaluator.ts create mode 100644 lib/evaluator/evaluator.ts create mode 100644 lib/ski/arena.ts create mode 100644 test/evaluator/arenaEvaluator.test.ts diff --git a/bin/ski.ts b/bin/ski.ts index 30dca96..03b5438 100755 --- a/bin/ski.ts +++ b/bin/ski.ts @@ -7,7 +7,7 @@ const { terminal } = tkexport; import { // SKI evaluator - stepOnce, + symbolicEvaluator, // SKI expressions prettyPrintSKI, type SKIExpression, @@ -36,7 +36,6 @@ import { // Types prettyPrintTy, inferType, - reduce } from '../lib/index.js'; import { randExpression } from '../lib/ski/generator.js'; @@ -144,7 +143,7 @@ function printCurrentTerm(): void { } function skiStepOnce(): void { - const result = stepOnce(currentSKI); + const result = symbolicEvaluator.stepOnce(currentSKI); if (result.altered) { currentSKI = result.expr; printGreen('stepped: ' + prettyPrintSKI(currentSKI)); @@ -155,7 +154,7 @@ function skiStepOnce(): void { function skiStepMany(): void { const MAX_ITER = 100; - const result = reduce(currentSKI, MAX_ITER); + const result = symbolicEvaluator.reduce(currentSKI, MAX_ITER); currentSKI = result; printGreen(`stepped many (with max of ${MAX_ITER}): ` + prettyPrintSKI(result)); } diff --git a/lib/evaluator/arenaEvaluator.ts b/lib/evaluator/arenaEvaluator.ts new file mode 100644 index 0000000..9924867 --- /dev/null +++ b/lib/evaluator/arenaEvaluator.ts @@ -0,0 +1,183 @@ +import { cons } from '../cons.js'; +import { EMPTY, ArenaKind, ArenaNodeId as ArenaNodeId, ArenaSym } from '../ski/arena.js'; +import { SKIExpression } from '../ski/expression.js'; +import { I, K, S, SKITerminalSymbol } from '../ski/terminal.js'; +import { Evaluator } from './evaluator.js'; + +const CAP = 1 << 22; +const kind = new Uint8Array(CAP); +const sym = new Uint8Array(CAP); +const leftId = new Uint32Array(CAP); +const rightId = new Uint32Array(CAP); +const hash32 = new Uint32Array(CAP); +const nextIdx = new Uint32Array(CAP); +let top = 0; // bump pointer + +const bucketShift = 16; // 65 536 buckets +const buckets = new Uint32Array(1 << bucketShift).fill(EMPTY); +const mask = (1 << bucketShift) - 1; + +// see https://github.com/aappleby/smhasher +// this is a fast integer scrambler with nice distribution properties +function avalanche32(x: number): number { + x = (x ^ (x >>> 16)) >>> 0; + x = (x * 0x7feb352d) >>> 0; + x = (x ^ (x >>> 15)) >>> 0; + x = (x * 0x846ca68b) >>> 0; + x = (x ^ (x >>> 16)) >>> 0; + return x >>> 0; +} + +const isTerminal = (n: ArenaNodeId) => (kind[n] as ArenaKind) === ArenaKind.Terminal; +const symOf = (n: ArenaNodeId) => sym[n] as ArenaSym; +const leftOf = (n: ArenaNodeId) => leftId[n]; +const rightOf = (n: ArenaNodeId) => rightId[n]; + +// Donald Knuth’s multiplicative-hash suggestion in The Art of Computer Programming, Vol 3 (section 6.4, 2nd ed., §3.2). +const GOLD = 0x9e3779b9; +const mix = (a: number, b: number) => + avalanche32((a + GOLD + ((b << 6) >>> 0) + (b >>> 2)) >>> 0); + +// make identical leaves pointer-equal +const termIds: Partial> = {}; + +function arenaTerminal(symVal: ArenaSym): ArenaNodeId { + const cached = termIds[symVal]; + if (cached !== undefined) return cached; // ← reuse + + const id = top++; + kind[id] = ArenaKind.Terminal; + sym[id] = symVal; + hash32[id] = symVal; // injective over {1,2,3} + termIds[symVal] = id; // remember for next time + return id; +} + +function arenaCons(l: ArenaNodeId, r: ArenaNodeId): ArenaNodeId { + const h = mix(hash32[l], hash32[r]); + const b = h & mask; + + /* lookup */ + for (let i = buckets[b]; i !== EMPTY; i = nextIdx[i]) { + if (hash32[i] === h && leftId[i] === l && rightId[i] === r) return i; + } + + /* miss → allocate */ + const id = top++; + kind[id] = ArenaKind.NonTerm; + leftId[id] = l; + rightId[id] = r; + hash32[id] = h; + nextIdx[id] = buckets[b]; + buckets[b] = id; + return id; +} + +function arenaKernelStep(expr: ArenaNodeId): { altered: boolean; expr: ArenaNodeId } { + if(isTerminal(expr)) { + return { + altered: false, + expr + }; + } + + if (isTerminal(leftOf(expr)) && symOf(leftOf(expr)) === ArenaSym.I) { + return { + altered: true, + expr: rightOf(expr) + }; + } + + if (!isTerminal(leftOf(expr)) && + isTerminal(leftOf(leftOf(expr))) && + symOf(leftOf(leftOf(expr))) === ArenaSym.K) { + + return { + altered: true, + expr: rightOf(leftOf(expr)) + }; + } + + if (!isTerminal(leftOf(expr)) && + !isTerminal(leftOf(leftOf(expr))) && + isTerminal(leftOf(leftOf(leftOf(expr)))) && + symOf(leftOf(leftOf(leftOf(expr)))) === ArenaSym.S) { + + const x = rightOf(leftOf(leftOf(expr))); + const y = rightOf(leftOf(expr)); + const z = rightOf(expr); + return { + altered: true, + expr: arenaCons(arenaCons(x, z), arenaCons(y, z)), + }; + } + + const leftRes = arenaKernelStep(leftOf(expr)); + + if (leftRes.altered) { + return { + altered: true, + expr: arenaCons(leftRes.expr, rightOf(expr)), + }; + } + + const rightRes = arenaKernelStep(rightOf(expr)); + + if (rightRes.altered) { + return { + altered: true, + expr: arenaCons(leftOf(expr), rightRes.expr), + }; + } + + return { altered: false, expr }; +} + +export const arenaEvaluator: Evaluator = { + stepOnce: arenaKernelStep, + reduce(expr: ArenaNodeId, max = Infinity) { + let cur = expr; + for (let i = 0; i < max; i++) { + const r = arenaKernelStep(cur); + if (!r.altered) return r.expr; + cur = r.expr; + } + return cur; + }, +}; + +export function toArena(exp: SKIExpression): ArenaNodeId { + switch(exp.kind) { + case 'terminal': + switch (exp.sym) { + case SKITerminalSymbol.S: + return arenaTerminal(ArenaSym.S); + case SKITerminalSymbol.K: + return arenaTerminal(ArenaSym.K); + case SKITerminalSymbol.I: + return arenaTerminal(ArenaSym.I); + default: + throw new Error('unrecognized terminal'); + } + + case 'non-terminal': + return arenaCons(toArena(exp.lft), toArena(exp.rgt)); + } +} + +export function fromArena(ni: ArenaNodeId): SKIExpression { + if (isTerminal(ni)) { + switch(symOf(ni)) { + case ArenaSym.S: + return S; + case ArenaSym.K: + return K; + case ArenaSym.I: + return I; + default: + throw new Error('corrupt symbol'); + } + } + + return cons(fromArena(leftOf(ni)), fromArena(rightOf(ni))); +} diff --git a/lib/evaluator/evaluator.ts b/lib/evaluator/evaluator.ts new file mode 100644 index 0000000..587c1b8 --- /dev/null +++ b/lib/evaluator/evaluator.ts @@ -0,0 +1,7 @@ +export interface Evaluator { + /** Apply exactly one β-step (or return unchanged). */ + stepOnce(expr: E): { altered: boolean; expr: E }; + + /** Keep stepping until fix-point or maxIterations. */ + reduce(expr: E, maxIterations?: number): E; +} diff --git a/lib/evaluator/skiEvaluator.ts b/lib/evaluator/skiEvaluator.ts index 0f36095..49ac99f 100644 --- a/lib/evaluator/skiEvaluator.ts +++ b/lib/evaluator/skiEvaluator.ts @@ -2,6 +2,7 @@ import { cons, ConsCell } from '../cons.js'; import { expressionEquivalent, SKIExpression, toSKIKey } from '../ski/expression.js'; import { SKITerminalSymbol } from '../ski/terminal.js'; import { createMap, searchMap, insertMap, SKIMap } from '../data/map/skiMap.js'; +import { Evaluator } from './evaluator.js'; /** * The internal shape of an evaluation result. @@ -176,7 +177,7 @@ const stepOnceMemoized = (expr: SKIExpression): SKIResult => { * @param maxIterations (optional) the maximum number of reduction iterations. * @returns the reduced SKI expression. */ -export const reduce = (exp: SKIExpression, maxIterations?: number): SKIExpression => { +const reduce = (exp: SKIExpression, maxIterations?: number): SKIExpression => { let current = exp; const maxIter = maxIterations ?? Infinity; for (let i = 0; i < maxIter; i++) { @@ -189,7 +190,13 @@ export const reduce = (exp: SKIExpression, maxIterations?: number): SKIExpressio return current; }; -export const stepOnce = (expr: SKIExpression): SKIResult => { +/** + * Performs exactly one symbolic reduction step. + * + * @param expr the input SKI expression + * @returns whether the reduction step changed the input, and the result + */ +const stepOnce = (expr: SKIExpression): SKIResult => { if (expr.kind === 'terminal') return { altered: false, expr }; let result = stepI(expr); if (result.altered) return result; @@ -203,3 +210,8 @@ export const stepOnce = (expr: SKIExpression): SKIResult => { if (result.altered) return { altered: true, expr: cons(expr.lft, result.expr) }; return { altered: false, expr }; }; + +export const symbolicEvaluator: Evaluator = { + stepOnce, + reduce, +}; diff --git a/lib/index.ts b/lib/index.ts index 9833785..90b96b4 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,5 +1,5 @@ // Core evaluator exports -export { stepOnce, reduce } from './evaluator/skiEvaluator.js'; +export { symbolicEvaluator } from './evaluator/skiEvaluator.js'; // SKI expression exports export { diff --git a/lib/ski/arena.ts b/lib/ski/arena.ts new file mode 100644 index 0000000..a3ee46a --- /dev/null +++ b/lib/ski/arena.ts @@ -0,0 +1,11 @@ +/** + * In the arena evaluator, every SKI expression gets its own numeric index. + * This is used to accelerate term evaluation. + */ +export type ArenaNodeId = number; + +export const enum ArenaKind { Terminal = 0, NonTerm = 1 } + +export const enum ArenaSym { S = 1, K = 2, I = 3 } + +export const EMPTY = 0xffffffff; diff --git a/lib/ski/church.ts b/lib/ski/church.ts index 6fa2ef8..e2dbed8 100644 --- a/lib/ski/church.ts +++ b/lib/ski/church.ts @@ -1,7 +1,7 @@ import { Zero, One, Succ, True, False } from '../consts/combinators.js'; import { apply, SKIExpression } from './expression.js'; import { SKITerminalSymbol } from './terminal.js'; -import { reduce } from '../evaluator/skiEvaluator.js'; +import { symbolicEvaluator } from '../evaluator/skiEvaluator.js'; /** * @see https://en.wikipedia.org/wiki/Church_encoding @@ -45,7 +45,7 @@ export const UnChurchNumber = (exp: SKIExpression): number => { */ export const UnChurchBoolean = (expr: SKIExpression): boolean => { // Apply the Church boolean to ChurchN(1) (for true) and ChurchN(0) (for false) - const testExpr = reduce(apply(expr, ChurchN(1), ChurchN(0))); + const testExpr = symbolicEvaluator.reduce(apply(expr, ChurchN(1), ChurchN(0))); return UnChurchNumber(testExpr) === 1; }; diff --git a/test/conversion/converter.test.ts b/test/conversion/converter.test.ts index d96aad9..6568875 100644 --- a/test/conversion/converter.test.ts +++ b/test/conversion/converter.test.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import { cons } from '../../lib/cons.js'; import { predLambda } from '../../lib/consts/lambdas.js'; -import { reduce } from '../../lib/evaluator/skiEvaluator.js'; +import { symbolicEvaluator } from '../../lib/evaluator/skiEvaluator.js'; import { UnChurchNumber, ChurchN } from '../../lib/ski/church.js'; import { apply } from '../../lib/ski/expression.js'; import { I } from '../../lib/ski/terminal.js'; @@ -25,7 +25,7 @@ describe('Lambda conversion', () => { for (let a = 0; a < N; a++) { for (let b = 0; b < N; b++) { const result = UnChurchNumber( - reduce(apply(convertLambda(konst), ChurchN(a), ChurchN(b))) + symbolicEvaluator.reduce(apply(convertLambda(konst), ChurchN(a), ChurchN(b))) ); expect(result).to.equal(a); } @@ -51,7 +51,7 @@ describe('Lambda conversion', () => { for (let b = 0; b < N; b++) { const expected = a ** b; // exponentiation: a^b const result = UnChurchNumber( - reduce(apply(convertLambda(flip), ChurchN(a), ChurchN(b))) + symbolicEvaluator.reduce(apply(convertLambda(flip), ChurchN(a), ChurchN(b))) ); expect(result).to.equal(expected); } @@ -62,7 +62,7 @@ describe('Lambda conversion', () => { for (let n = 0; n < N; n++) { const expected = Math.max(n - 1, 0); // pred(0) is defined as 0. const result = UnChurchNumber( - reduce(apply(convertLambda(predLambda), ChurchN(n))) + symbolicEvaluator.reduce(apply(convertLambda(predLambda), ChurchN(n))) ); expect(result).to.equal(expected); } diff --git a/test/evaluator/arenaEvaluator.test.ts b/test/evaluator/arenaEvaluator.test.ts new file mode 100644 index 0000000..e3ceb2f --- /dev/null +++ b/test/evaluator/arenaEvaluator.test.ts @@ -0,0 +1,94 @@ +import { assert } from 'chai'; +import rsexport, { RandomSeed } from 'random-seed'; +const { create } = rsexport; + +import { arenaEvaluator, fromArena, toArena } from '../../lib/evaluator/arenaEvaluator.js'; +import { parseSKI } from '../../lib/parser/ski.js'; +import { prettyPrint } from '../../lib/ski/expression.js'; +import { randExpression } from '../../lib/ski/generator.js'; +import { symbolicEvaluator } from '../../lib/index.js'; + +describe('stepOnce', () => { + const first = toArena(parseSKI('III')); + const second = toArena(parseSKI('II')); + const third = toArena(parseSKI('I')); + const fourth = toArena(parseSKI('KIS')); + const fifth = toArena(parseSKI('SKKI')); + const sixth = toArena(parseSKI('SKKII')); + const seventh = toArena(parseSKI('KI(KI)')); + + it(`evaluates ${prettyPrint(fromArena(second))} + => + ${prettyPrint(fromArena(third))}`, () => { + const result = arenaEvaluator.stepOnce(second); + assert(result.altered); + assert(result.expr === third); + }); + + it(`evaluates ${prettyPrint(fromArena(first))} + => + ${prettyPrint(fromArena(third))}`, + () => { + const firstStep = arenaEvaluator.stepOnce(first); + assert(firstStep.altered); + const secondStep = arenaEvaluator.stepOnce(firstStep.expr); + assert(secondStep.altered); + assert(secondStep.expr === third); + }); + + it(`evaluates ${prettyPrint(fromArena(fourth))} + => + ${prettyPrint(fromArena(third))}`, () => { + const result = arenaEvaluator.stepOnce(fourth); + assert(result.altered); + assert(result.expr === third); + }); + + it(`evaluates + ${prettyPrint(fromArena(fifth))} + => + ${prettyPrint(fromArena(seventh))}`, () => { + const first = arenaEvaluator.stepOnce(fifth); + assert(first.altered); + assert(first.expr === seventh); + }); + + it(`${prettyPrint(fromArena(sixth))} + => + ${prettyPrint(fromArena(third))}`, + () => { + const firstStep = arenaEvaluator.stepOnce(sixth); + assert(firstStep.altered); + const secondStep = arenaEvaluator.stepOnce(firstStep.expr); + assert(secondStep.altered); + const thirdStep = arenaEvaluator.stepOnce(secondStep.expr); + assert(thirdStep.altered); + assert(thirdStep.expr === third); + }); +}); + +const seed = 'df394b'; +const NORMALISE_TESTS = 19; +const MIN_LENGTH = 5; +const MAX_LENGTH = 12; + +describe('stepOnce loop vs. reduce()', () => { + const rs: RandomSeed = create(seed); + + it(`runs ${NORMALISE_TESTS.toString()} normalization tests with random expressions`, () => { + [...Array(NORMALISE_TESTS).keys()].forEach((testNum) => { + const length = rs.intBetween(MIN_LENGTH, MAX_LENGTH); + const fresh = randExpression(rs, length); + const normal1 = fromArena(arenaEvaluator.reduce(toArena(fresh))); + const normal2 = symbolicEvaluator.reduce(fresh); + + assert.deepStrictEqual( + prettyPrint(normal2), + prettyPrint(normal1), + `Test ${(testNum + 1).toString()}/${NORMALISE_TESTS.toString()} failed: mismatch\n` + + `Input length: ${length.toString()}\n` + + `Input expression: ${prettyPrint(fresh)}` + ); + }); + }); +}); diff --git a/test/evaluator/skiEvaluator.test.ts b/test/evaluator/skiEvaluator.test.ts index 7fe6029..67cd765 100644 --- a/test/evaluator/skiEvaluator.test.ts +++ b/test/evaluator/skiEvaluator.test.ts @@ -1,8 +1,11 @@ import { assert } from 'chai'; -import { stepOnce } from '../../lib/evaluator/skiEvaluator.js'; +import { symbolicEvaluator } from '../../lib/evaluator/skiEvaluator.js'; import { parseSKI } from '../../lib/parser/ski.js'; import { SKIExpression, prettyPrint } from '../../lib/ski/expression.js'; +import rsexport, { RandomSeed } from 'random-seed'; +const { create } = rsexport; +import { randExpression } from '../../lib/ski/generator.js'; describe('stepOnce', () => { const first = parseSKI('III'); @@ -20,7 +23,7 @@ describe('stepOnce', () => { it(`evaluates ${prettyPrint(second)} => ${prettyPrint(third)}`, () => { - const result = stepOnce(second); + const result = symbolicEvaluator.stepOnce(second); assert(result.altered); compareExpressions(result.expr, third); }); @@ -29,9 +32,9 @@ describe('stepOnce', () => { => ${prettyPrint(third)}`, () => { - const firstStep = stepOnce(first); + const firstStep = symbolicEvaluator.stepOnce(first); assert(firstStep.altered); - const secondStep = stepOnce(firstStep.expr); + const secondStep = symbolicEvaluator.stepOnce(firstStep.expr); assert(secondStep.altered); compareExpressions(secondStep.expr, third); }); @@ -39,7 +42,7 @@ describe('stepOnce', () => { it(`evaluates ${prettyPrint(fourth)} => ${prettyPrint(third)}`, () => { - const result = stepOnce(fourth); + const result = symbolicEvaluator.stepOnce(fourth); assert(result.altered); compareExpressions(result.expr, third); }); @@ -48,7 +51,7 @@ describe('stepOnce', () => { ${prettyPrint(fifth)} => ${prettyPrint(seventh)}`, () => { - const first = stepOnce(fifth); + const first = symbolicEvaluator.stepOnce(fifth); assert(first.altered); compareExpressions(first.expr, seventh); }); @@ -57,12 +60,56 @@ describe('stepOnce', () => { => ${prettyPrint(third)}`, () => { - const firstStep = stepOnce(sixth); + const firstStep = symbolicEvaluator.stepOnce(sixth); assert(firstStep.altered); - const secondStep = stepOnce(firstStep.expr); + const secondStep = symbolicEvaluator.stepOnce(firstStep.expr); assert(secondStep.altered); - const thirdStep = stepOnce(secondStep.expr); + const thirdStep = symbolicEvaluator.stepOnce(secondStep.expr); assert(thirdStep.altered); compareExpressions(thirdStep.expr, third); }); }); + +const MAX_ITER = 100; + +/** + * Drive stepOnce until it returns { altered:false, expr:e } + * and count how many iterations it took. + */ +function reduceByLoop(expr: SKIExpression, maxIter = MAX_ITER) { + let cur = expr; + for (let i = 0; i < maxIter; i++) { + const r = symbolicEvaluator.stepOnce(cur); + if (!r.altered) return { expr: r.expr, steps: i }; + cur = r.expr; + } + throw new Error('stepOnce failed to normalise within maxIter'); +} + +const seed = 'df394b'; +const NORMALISE_TESTS = 19; +const MIN_LENGTH = 5; +const MAX_LENGTH = 12; + +describe('stepOnce loop vs. reduce()', () => { + const rs: RandomSeed = create(seed); + + it(`runs ${NORMALISE_TESTS.toString()} normalization tests with random expressions`, () => { + [...Array(NORMALISE_TESTS).keys()].forEach((testNum) => { + const length = rs.intBetween(MIN_LENGTH, MAX_LENGTH); + const fresh = randExpression(rs, length); + const normal1 = symbolicEvaluator.reduce(fresh); + const { expr: normal2, steps } = reduceByLoop(fresh); + + assert.deepStrictEqual( + prettyPrint(normal2), + prettyPrint(normal1), + `Test ${(testNum + 1).toString()}/${NORMALISE_TESTS.toString()} failed: mismatch after ${steps.toString()} stepOnce iterations\n` + + `Input length: ${length.toString()}\n` + + `Input expression: ${prettyPrint(fresh)}` + ); + + console.log(`${prettyPrint(fresh)} normalised in ${steps.toString()} steps`); + }); + }); +}); diff --git a/test/performance.test.ts b/test/performance.test.ts index 83492db..ddf90bb 100644 --- a/test/performance.test.ts +++ b/test/performance.test.ts @@ -2,7 +2,7 @@ import { hrtime } from 'process'; import randomSeed from 'random-seed'; import { SKIExpression } from '../lib/ski/expression.js'; -import { stepOnce } from '../lib/evaluator/skiEvaluator.js'; +import { symbolicEvaluator } from '../lib/evaluator/skiEvaluator.js'; import { randExpression } from '../lib/ski/generator.js'; describe('evaluator performance', () => { @@ -24,7 +24,7 @@ describe('evaluator performance', () => { // Now measure the time spent reducing the pre-generated trees. const start = hrtime.bigint(); for (const expr of expressions) { - stepOnce(expr); + symbolicEvaluator.stepOnce(expr); } const end = hrtime.bigint(); const elapsedNs = end - start; diff --git a/test/ski/church.test.ts b/test/ski/church.test.ts index 4c3d773..cc58fbc 100644 --- a/test/ski/church.test.ts +++ b/test/ski/church.test.ts @@ -2,7 +2,7 @@ import { describe, it } from 'mocha'; import { expect } from 'chai'; import { V, Succ, Fst, Snd, Car, Cdr, F, True, False, Plus, Zero, B } from '../../lib/consts/combinators.js'; -import { reduce } from '../../lib/evaluator/skiEvaluator.js'; +import { symbolicEvaluator } from '../../lib/evaluator/skiEvaluator.js'; import { UnChurchNumber, ChurchN, ChurchB, UnChurchBoolean } from '../../lib/ski/church.js'; import { S, K, I } from '../../lib/ski/terminal.js'; import { convertLambda } from '../../lib/conversion/converter.js'; @@ -24,7 +24,7 @@ describe('Church encodings', () => { }); it('reduces 1 + 1 to 2', () => { - expect(UnChurchNumber(reduce(apply(Succ, ChurchN(1))))) + expect(UnChurchNumber(symbolicEvaluator.reduce(apply(Succ, ChurchN(1))))) .to.deep.equal(2); }); @@ -43,7 +43,7 @@ describe('Church encodings', () => { * (AND)FT = F?T:F = F * (AND)FF = F?F:F = F */ - expect(UnChurchBoolean(reduce(apply(ChurchB(p), ChurchB(q), ChurchB(p))))) + expect(UnChurchBoolean(symbolicEvaluator.reduce(apply(ChurchB(p), ChurchB(q), ChurchB(p))))) .to.deep.equal(conj); /* @@ -54,24 +54,24 @@ describe('Church encodings', () => { * (OR)FT = F?F:T = T * (OR)FF = F?F:F = F */ - expect(UnChurchBoolean(reduce(apply(ChurchB(p), ChurchB(p), ChurchB(q))))) + expect(UnChurchBoolean(symbolicEvaluator.reduce(apply(ChurchB(p), ChurchB(p), ChurchB(q))))) .to.deep.equal(dis); }); }); }); it('reduces pairs', () => { - expect(UnChurchNumber(reduce(apply(V, ChurchN(0), ChurchN(1), Fst)))) + expect(UnChurchNumber(symbolicEvaluator.reduce(apply(V, ChurchN(0), ChurchN(1), Fst)))) .to.equal(0); - expect(UnChurchNumber(reduce(apply(V, ChurchN(0), ChurchN(1), Snd)))) + expect(UnChurchNumber(symbolicEvaluator.reduce(apply(V, ChurchN(0), ChurchN(1), Snd)))) .to.equal(1); - expect(UnChurchNumber(reduce( + expect(UnChurchNumber(symbolicEvaluator.reduce( apply(Car, apply(V, ChurchN(0), ChurchN(1))) ))).to.equal(0); - expect(UnChurchNumber(reduce( + expect(UnChurchNumber(symbolicEvaluator.reduce( apply(Cdr, apply(V, ChurchN(0), ChurchN(1))) ))).to.equal(1); }); @@ -82,23 +82,23 @@ describe('Church encodings', () => { const IsZero = apply(F, True, apply(K, False)); it('isZero tests for whether a numeral is zero', () => { - expect(UnChurchBoolean(reduce( + expect(UnChurchBoolean(symbolicEvaluator.reduce( apply(ChurchN(0), apply(K, False), True) ))).to.equal(true); - expect(UnChurchBoolean(reduce( + expect(UnChurchBoolean(symbolicEvaluator.reduce( apply(ChurchN(1), apply(K, False), True) ))).to.equal(false); - expect(UnChurchBoolean(reduce( + expect(UnChurchBoolean(symbolicEvaluator.reduce( apply(ChurchN(2), apply(K, False), True) ))).to.equal(false); - expect(UnChurchBoolean(reduce( + expect(UnChurchBoolean(symbolicEvaluator.reduce( apply(IsZero, ChurchN(0)) ))).to.equal(true); - expect(UnChurchBoolean(reduce( + expect(UnChurchBoolean(symbolicEvaluator.reduce( apply(IsZero, ChurchN(1)) ))).to.equal(false); }); @@ -109,17 +109,17 @@ describe('Church encodings', () => { for (let n = 0; n < N; n++) { // λmn.(m succ)n, or apply m +1s to n expect(UnChurchNumber( - reduce(apply(ChurchN(m), Succ, ChurchN(n))) + symbolicEvaluator.reduce(apply(ChurchN(m), Succ, ChurchN(n))) )).to.equal(m + n); // λmnfx.mf((nf)x) ≡ BS(BB) ≡ Plus expect(UnChurchNumber( - reduce(apply(Plus, ChurchN(m), ChurchN(n))) + symbolicEvaluator.reduce(apply(Plus, ChurchN(m), ChurchN(n))) )).to.equal(m + n); // λmn.m(n(succ)), or apply m +ns to 0 expect(UnChurchNumber( - reduce(apply(ChurchN(m), apply(ChurchN(n), Succ), Zero)) + symbolicEvaluator.reduce(apply(ChurchN(m), apply(ChurchN(n), Succ), Zero)) )).to.equal(m * n); /* @@ -128,7 +128,7 @@ describe('Church encodings', () => { * in the Church numerals simultaneously. */ expect(UnChurchNumber( - reduce(apply(B, ChurchN(m), ChurchN(n), Succ, Zero)) + symbolicEvaluator.reduce(apply(B, ChurchN(m), ChurchN(n), Succ, Zero)) )).to.equal(m * n); } } @@ -160,14 +160,14 @@ describe('Church encodings', () => { expect( UnChurchNumber( - reduce( + symbolicEvaluator.reduce( apply(Cdr, apply(ChurchN(m), pairShiftSucc, pairZeroZero)) ) ) ).to.equal(expected); expect( - UnChurchNumber(reduce(apply(pred, ChurchN(m)))) + UnChurchNumber(symbolicEvaluator.reduce(apply(pred, ChurchN(m)))) ).to.deep.equal(expected); } }); From 1ce77141bcdbb8cc1257e87d3c403b5bd64f2d79 Mon Sep 17 00:00:00 2001 From: Max DeLiso Date: Mon, 28 Apr 2025 22:57:07 -0400 Subject: [PATCH 2/4] use GOLD correctly --- lib/evaluator/arenaEvaluator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/evaluator/arenaEvaluator.ts b/lib/evaluator/arenaEvaluator.ts index 9924867..7fd0f12 100644 --- a/lib/evaluator/arenaEvaluator.ts +++ b/lib/evaluator/arenaEvaluator.ts @@ -35,8 +35,7 @@ const rightOf = (n: ArenaNodeId) => rightId[n]; // Donald Knuth’s multiplicative-hash suggestion in The Art of Computer Programming, Vol 3 (section 6.4, 2nd ed., §3.2). const GOLD = 0x9e3779b9; -const mix = (a: number, b: number) => - avalanche32((a + GOLD + ((b << 6) >>> 0) + (b >>> 2)) >>> 0); +const mix = (a: number, b: number) => avalanche32((a ^ (b * GOLD)) >>> 0); // make identical leaves pointer-equal const termIds: Partial> = {}; From 46337b46c7347e0cdfa42f519bac17c13cf628c8 Mon Sep 17 00:00:00 2001 From: Max DeLiso Date: Mon, 28 Apr 2025 23:00:35 -0400 Subject: [PATCH 3/4] extra space --- lib/evaluator/arenaEvaluator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/evaluator/arenaEvaluator.ts b/lib/evaluator/arenaEvaluator.ts index 7fd0f12..3b79f4a 100644 --- a/lib/evaluator/arenaEvaluator.ts +++ b/lib/evaluator/arenaEvaluator.ts @@ -73,7 +73,7 @@ function arenaCons(l: ArenaNodeId, r: ArenaNodeId): ArenaNodeId { } function arenaKernelStep(expr: ArenaNodeId): { altered: boolean; expr: ArenaNodeId } { - if(isTerminal(expr)) { + if (isTerminal(expr)) { return { altered: false, expr From a15f46d7ece74534e1d6a9c19d7cbbb44d564fd7 Mon Sep 17 00:00:00 2001 From: Max DeLiso Date: Mon, 28 Apr 2025 23:11:13 -0400 Subject: [PATCH 4/4] throw on overflow --- lib/evaluator/arenaEvaluator.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/evaluator/arenaEvaluator.ts b/lib/evaluator/arenaEvaluator.ts index 3b79f4a..1f29f01 100644 --- a/lib/evaluator/arenaEvaluator.ts +++ b/lib/evaluator/arenaEvaluator.ts @@ -17,6 +17,15 @@ const bucketShift = 16; // 65 536 buckets const buckets = new Uint32Array(1 << bucketShift).fill(EMPTY); const mask = (1 << bucketShift) - 1; +function alloc(): ArenaNodeId { + if (top >= CAP) { + throw new RangeError( + `Arena exhausted: reached CAP = ${CAP.toString()} nodes`, + ); + } + return top++; +} + // see https://github.com/aappleby/smhasher // this is a fast integer scrambler with nice distribution properties function avalanche32(x: number): number { @@ -44,7 +53,7 @@ function arenaTerminal(symVal: ArenaSym): ArenaNodeId { const cached = termIds[symVal]; if (cached !== undefined) return cached; // ← reuse - const id = top++; + const id = alloc(); kind[id] = ArenaKind.Terminal; sym[id] = symVal; hash32[id] = symVal; // injective over {1,2,3} @@ -62,7 +71,7 @@ function arenaCons(l: ArenaNodeId, r: ArenaNodeId): ArenaNodeId { } /* miss → allocate */ - const id = top++; + const id = alloc(); kind[id] = ArenaKind.NonTerm; leftId[id] = l; rightId[id] = r;