Skip to content

Commit

Permalink
add eval cache, simplify
Browse files Browse the repository at this point in the history
  • Loading branch information
maxdeliso committed Feb 16, 2025
1 parent 98fd9ef commit 9a504ec
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 158 deletions.
10 changes: 6 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
stepOnceImmediate,
stepOnce,
// SKI expressions
prettyPrintSKI,
type SKIExpression,
Expand Down Expand Up @@ -47,8 +47,9 @@ enum Mode {
SystemF = 'SystemF'
}

const N = 8;
let currentMode: Mode = Mode.SKI;
let currentSKI: SKIExpression = randExpression(create(hrtime.bigint().toString()), 32);
let currentSKI: SKIExpression = randExpression(create(hrtime.bigint().toString()), N);
let currentLambda: UntypedLambda | null = null;
let currentTypedLambda: TypedLambda | null = null;
let currentSystemF: SystemFTerm | null = null;
Expand Down Expand Up @@ -143,7 +144,7 @@ function printCurrentTerm(): void {
}

function skiStepOnce(): void {
const result = stepOnceImmediate(currentSKI);
const result = stepOnce(currentSKI);
if (result.altered) {
currentSKI = result.expr;
printGreen('stepped: ' + prettyPrintSKI(currentSKI));
Expand All @@ -155,12 +156,13 @@ function skiStepOnce(): void {
function skiStepMany(): void {
const MAX_ITER = 100;
const result = reduce(currentSKI, MAX_ITER);
currentSKI = result;
printGreen(`stepped many (with max of ${MAX_ITER}): ` + prettyPrintSKI(result));
}

function skiRegenerate(): void {
const rs = create(hrtime.bigint().toString());
currentSKI = randExpression(rs, 32);
currentSKI = randExpression(rs, N);
printGreen('generated new SKI expression: ' + prettyPrintSKI(currentSKI));
}

Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default tseslint.config(
},
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
Expand Down
274 changes: 133 additions & 141 deletions lib/evaluator/skiEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,201 +13,193 @@ interface SKIResult<E> {
expr: E;
}

type ExtractStep<E> = (expr: E) => E | false;
type SKIStep<E> = (input: E) => SKIResult<E>;

/**
* Helper that applies a step function; if the extraction function returns a new expression,
* we mark the step as having altered the expression.
* A step function type.
*/
function extractStep(
expr: SKIExpression,
extractFn: ExtractStep<SKIExpression>
): SKIResult<SKIExpression> {
const extractionResult = extractFn(expr);
return extractionResult
? { altered: true, expr: extractionResult }
: { altered: false, expr };
}
type SKIStep<E> = (input: E) => SKIResult<E>;

const stepI: SKIStep<SKIExpression> = (expr: SKIExpression) =>
extractStep(expr, (e: SKIExpression) =>
e.kind === 'non-terminal' &&
e.lft.kind === 'terminal' &&
e.lft.sym === SKITerminalSymbol.I &&
e.rgt
);

const stepK: SKIStep<SKIExpression> = (expr: SKIExpression) =>
extractStep(expr, (e: SKIExpression) =>
e.kind === 'non-terminal' &&
e.lft.kind === 'non-terminal' &&
e.lft.lft.kind === 'terminal' &&
e.lft.lft.sym === SKITerminalSymbol.K &&
e.lft.rgt
);

const stepS: SKIStep<SKIExpression> = (expr: SKIExpression) =>
extractStep(expr, (e: SKIExpression) => {
if (
e.kind === 'non-terminal' &&
e.lft.kind === 'non-terminal' &&
e.lft.lft.kind === 'non-terminal' &&
e.lft.lft.lft.kind === 'terminal' &&
e.lft.lft.lft.sym === SKITerminalSymbol.S
) {
const x = e.lft.lft.rgt;
const y = e.lft.rgt;
const z = e.rgt;
return cons(cons(x, z), cons(y, z));
} else {
return false;
}
});
const stepI: SKIStep<SKIExpression> = (expr: SKIExpression) => {
if (
expr.kind === 'non-terminal' &&
expr.lft.kind === 'terminal' &&
expr.lft.sym === SKITerminalSymbol.I
) {
return { altered: true, expr: expr.rgt };
}
return { altered: false, expr };
};

const stepK: SKIStep<SKIExpression> = (expr: SKIExpression) => {
if (
expr.kind === 'non-terminal' &&
expr.lft.kind === 'non-terminal' &&
expr.lft.lft.kind === 'terminal' &&
expr.lft.lft.sym === SKITerminalSymbol.K
) {
return { altered: true, expr: expr.lft.rgt };
}
return { altered: false, expr };
};

const stepS: SKIStep<SKIExpression> = (expr: SKIExpression) => {
if (
expr.kind === 'non-terminal' &&
expr.lft.kind === 'non-terminal' &&
expr.lft.lft.kind === 'non-terminal' &&
expr.lft.lft.lft.kind === 'terminal' &&
expr.lft.lft.lft.sym === SKITerminalSymbol.S
) {
const x = expr.lft.lft.rgt;
const y = expr.lft.rgt;
const z = expr.rgt;
return { altered: true, expr: cons(cons(x, z), cons(y, z)) };
}
return { altered: false, expr };
};

/**
* A frame used for DFS during the memoized (cached) tree‐step.
* A frame for the iterative DFS.
*
* - phase "left" means we are about to reduce the left child.
* - phase "right" means the left child is done (its result is in `leftResult`)
* and we now need to reduce the right child.
*/
interface Frame {
node: ConsCell<SKIExpression>;
phase: 'left' | 'right';
// When phase is 'right', `leftResult` holds the reduced left subtree.
leftResult?: SKIResult<SKIExpression>;
leftResult?: SKIExpression;
}

/**
* Global memoization cache.
* expressionCache stores associations between an expression's canonical key and its
* intermediate reduction.
*/
let globalMemo: SKIMap = createMap();
let expressionCache: SKIMap = createMap();

/**
* DFS‑based tree-step that uses the global memoization cache.
* If a node's canonical key is already cached, its result is reused.
* evaluationCache stores associations between an expression's canonical key and its
* fully reduced (normalized) form.
*/
function treeStep(
expr: SKIExpression,
step: SKIStep<SKIExpression>
): SKIResult<SKIExpression> {
const stack: Frame[] = [];
let evaluationCache: SKIMap = createMap();

/**
* Iteratively performs one DFS-based tree step (one “step‐once”),
* trying the S, K, and I rules at each node.
*
* Uses the evaluation cache only at the very beginning (to avoid work on a fully
* normalized input) and at the very end (to cache a fully normalized result),
* while using only the intermediate expressionCache during the DFS.
*/
const stepOnceMemoized = (expr: SKIExpression): SKIResult<SKIExpression> => {
const orig = expr;
const origKey = toSKIKey(orig);
const evalCached = searchMap(evaluationCache, origKey);

if (evalCached !== undefined) {
return { altered: !expressionEquivalent(orig, evalCached), expr: evalCached };
}

let current: SKIExpression = expr;
let result: SKIResult<SKIExpression>;
let next: SKIExpression;
const stack: Frame[] = [];

for (;;) {
for(;;) {
const key = toSKIKey(current);
const memoized = searchMap(globalMemo, key);
if (memoized !== undefined) {
const memoDiff = !expressionEquivalent(current, memoized);
result = { altered: memoDiff, expr: memoized };
const cached = searchMap(expressionCache, key);
if (cached !== undefined) {
next = cached;
} else {
if (current.kind === 'terminal') {
result = { altered: false, expr: current };
next = current;
} else {
const stepResult = step(current);
let stepResult = stepI(current);
if (!stepResult.altered) {
stepResult = stepK(current);
}
if (!stepResult.altered) {
stepResult = stepS(current);
}
if (stepResult.altered) {
result = stepResult;
next = stepResult.expr;
expressionCache = insertMap(expressionCache, key, next);
} else {
// No reduction at this node; descend into the left subtree.
// No rule applied here; continue DFS by descending into the left child.
stack.push({ node: current, phase: 'left' });
current = current.lft;
continue;
}
}
// Cache the result.
globalMemo = insertMap(globalMemo, key, result.expr);
}

// If there are no frames left, we are at the top level.
if (stack.length === 0) {
return result;
// Determine if the top-level expression changed.
const changed = !expressionEquivalent(orig, next);
// If no change occurred, then newExpr is fully normalized; cache it.
if (!changed) {
evaluationCache = insertMap(evaluationCache, origKey, next);
}
return { altered: changed, expr: next };
}

// Pop a frame and combine results.
const frame = stack.pop();
if (!frame) {
throw new Error('Expected stack frame but got undefined');
}
// Pop a frame and combine the result with its parent.
const frame = stack.pop()!;
if (frame.phase === 'left') {
if (result.altered) {
result = { altered: true, expr: cons(result.expr, frame.node.rgt) };
globalMemo = insertMap(globalMemo, toSKIKey(frame.node), result.expr);
if (!expressionEquivalent(frame.node.lft, next)) {
// The left subtree was reduced. Rebuild the parent's node.
next = cons(next, frame.node.rgt);
expressionCache = insertMap(expressionCache, toSKIKey(frame.node), next);
} else {
stack.push({
node: frame.node,
phase: 'right',
leftResult: { altered: false, expr: frame.node.lft }
});
// The left branch is fully normalized.
// Now prepare to reduce the right branch by pushing a frame with phase 'right'
frame.phase = 'right';
frame.leftResult = next;
stack.push(frame);
current = frame.node.rgt;
continue;
}
} else { // frame.phase === 'right'
if (!frame.leftResult) throw new Error('missing left result');
result = { altered: result.altered, expr: cons(frame.leftResult.expr, result.expr) };
globalMemo = insertMap(globalMemo, toSKIKey(frame.node), result.expr);
}
if (stack.length === 0) return result;
current = result.expr;
}
}

/**
* Helper that tries a list of step functions (in order) on the given expression.
*/
function scanStep(
expr: SKIExpression,
steppers: SKIStep<SKIExpression>[]
): SKIResult<SKIExpression> {
for (const step of steppers) {
const result = step(expr);
if (result.altered) {
return result;
// Now combine the left result (already normalized) with the just-reduced right branch.
next = cons(frame.leftResult!, next);
expressionCache = insertMap(expressionCache, toSKIKey(frame.node), next);
}
// Propagate the new (combined) expression upward.
current = next;
}
return { altered: false, expr };
}

/**
* Step‑once (cached version) using the DFS tree step and global memoization.
*/
const stepOnceMemoized = (expr: SKIExpression): SKIResult<SKIExpression> =>
scanStep(expr, [e => treeStep(e, stepS), e => treeStep(e, stepK), e => treeStep(e, stepI)]);

/**
* Step‑once (immediate version) that does not use caching.
*/
export const stepOnceImmediate = (expr: SKIExpression): SKIResult<SKIExpression> => {
if (expr.kind === 'terminal') return { altered: false, expr };
const iStep = stepI(expr);
if (iStep.altered) return iStep;
const kStep = stepK(expr);
if (kStep.altered) return kStep;
const sStep = stepS(expr);
if (sStep.altered) return sStep;
const leftResult = stepOnceImmediate(expr.lft);
if (leftResult.altered) return { altered: true, expr: cons(leftResult.expr, expr.rgt) };
const rightResult = stepOnceImmediate(expr.rgt);
if (rightResult.altered) return { altered: true, expr: cons(expr.lft, rightResult.expr) };
return { altered: false, expr };
};

/**
* Repeatedly applies cached reduction steps until no more changes occur
* or until the maximum number of iterations is reached.
* Repeatedly applies reduction steps until no further reduction is possible
* (or until the maximum number of iterations is reached), then returns the result.
*
* @param exp the initial SKI expression.
* @param maxIterations (optional) the maximum number of reduction iterations.
* If omitted, the reduction will continue until a fixed point is reached.
* @returns the reduced SKI expression.
*/
export const reduce = (exp: SKIExpression, maxIterations?: number): SKIExpression => {
let current = exp;
let result = stepOnceMemoized(current);
let iterations = 0;
const maxIter = maxIterations ?? Infinity;

while (result.altered && iterations < maxIter) {
for (let i = 0; i < maxIter; i++) {
const result = stepOnceMemoized(current);
if (!result.altered) {
return result.expr;
}
current = result.expr;
result = stepOnceMemoized(current);
iterations++;
}

return current;
};

export const stepOnce = (expr: SKIExpression): SKIResult<SKIExpression> => {
if (expr.kind === 'terminal') return { altered: false, expr };
let result = stepI(expr);
if (result.altered) return result;
result = stepK(expr);
if (result.altered) return result;
result = stepS(expr);
if (result.altered) return result;
result = stepOnce(expr.lft);
if (result.altered) return { altered: true, expr: cons(result.expr, expr.rgt) };
result = stepOnce(expr.rgt);
if (result.altered) return { altered: true, expr: cons(expr.lft, result.expr) };
return { altered: false, expr };
};
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 { stepOnceImmediate, reduce } from './evaluator/skiEvaluator.js';
export { stepOnce, reduce } from './evaluator/skiEvaluator.js';

// SKI expression exports
export {
Expand Down
Loading

0 comments on commit 9a504ec

Please sign in to comment.