Skip to content

add arena evaluator as symbolic alternative, test #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions bin/ski.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { terminal } = tkexport;

import {
// SKI evaluator
stepOnce,
symbolicEvaluator,
// SKI expressions
prettyPrintSKI,
type SKIExpression,
Expand Down Expand Up @@ -36,7 +36,6 @@ import {
// Types
prettyPrintTy,
inferType,
reduce
} from '../lib/index.js';
import { randExpression } from '../lib/ski/generator.js';

Expand Down Expand Up @@ -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));
Expand All @@ -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));
}
Expand Down
191 changes: 191 additions & 0 deletions lib/evaluator/arenaEvaluator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
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;

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 {
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 ^ (b * GOLD)) >>> 0);

// make identical leaves pointer-equal
const termIds: Partial<Record<ArenaSym, ArenaNodeId>> = {};

function arenaTerminal(symVal: ArenaSym): ArenaNodeId {
const cached = termIds[symVal];
if (cached !== undefined) return cached; // ← reuse

const id = alloc();
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 = alloc();
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<ArenaNodeId> = {
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)));
}
7 changes: 7 additions & 0 deletions lib/evaluator/evaluator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Evaluator<E> {
/** 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;
}
16 changes: 14 additions & 2 deletions lib/evaluator/skiEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -176,7 +177,7 @@ const stepOnceMemoized = (expr: SKIExpression): SKIResult<SKIExpression> => {
* @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++) {
Expand All @@ -189,7 +190,13 @@ export const reduce = (exp: SKIExpression, maxIterations?: number): SKIExpressio
return current;
};

export const stepOnce = (expr: SKIExpression): SKIResult<SKIExpression> => {
/**
* 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<SKIExpression> => {
if (expr.kind === 'terminal') return { altered: false, expr };
let result = stepI(expr);
if (result.altered) return result;
Expand All @@ -203,3 +210,8 @@ export const stepOnce = (expr: SKIExpression): SKIResult<SKIExpression> => {
if (result.altered) return { altered: true, expr: cons(expr.lft, result.expr) };
return { altered: false, expr };
};

export const symbolicEvaluator: Evaluator<SKIExpression> = {
stepOnce,
reduce,
};
2 changes: 1 addition & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Core evaluator exports
export { stepOnce, reduce } from './evaluator/skiEvaluator.js';
export { symbolicEvaluator } from './evaluator/skiEvaluator.js';

// SKI expression exports
export {
Expand Down
11 changes: 11 additions & 0 deletions lib/ski/arena.ts
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 2 additions & 2 deletions lib/ski/church.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
};

Expand Down
8 changes: 4 additions & 4 deletions test/conversion/converter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down
Loading