mcard-js
Version:
MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers
636 lines • 24.1 kB
JavaScript
/**
* Lambda Runtime - PTR Runtime for Lambda Calculus
*
* Implements α-β-η conversions as a PTR runtime, treating MCard hashes
* as Lambda terms and performing computations that produce new MCards.
*
* This runtime can be used via CLM specifications to define and verify
* Lambda Calculus reductions.
*
* @module mcard-js/ptr/lambda/LambdaRuntime
*/
import { RuntimeFactory } from '../node/runtimes/factory.js';
// Import conversions
import { alphaRename, alphaEquivalent, alphaNormalize } from './AlphaConversion';
import { betaReduce, normalize, reduceStep, isNormalForm } from './BetaReduction';
import { etaReduce, etaExpand, etaNormalize, etaEquivalent } from './EtaConversion';
import { freeVariables, isClosed } from './FreeVariables';
import { loadTerm, storeTerm, mkVar, mkAbs, mkApp, prettyPrintDeep } from './LambdaTerm';
import { createIOHandler } from './IOEffects';
// ─────────────────────────────────────────────────────────────────────────────
// Lambda Runtime Class
// ─────────────────────────────────────────────────────────────────────────────
/**
* Lambda Calculus Runtime for PTR
*
* Executes Lambda Calculus operations on MCard-stored terms.
*/
export class LambdaRuntime {
collection;
constructor(collection) {
this.collection = collection;
}
/**
* Execute a Lambda operation
*
* @param codeOrPath - For Lambda runtime, this is the term hash to operate on
* @param context - Additional context (varies by operation)
* @param config - Lambda configuration with operation type
* @param chapterDir - Chapter directory (used for relative paths if needed)
*/
async execute(codeOrPath, context, config, chapterDir) {
let termHash = codeOrPath;
const ctx = (context && typeof context === 'object')
? context
: {};
// Prioritize context operation (from test input) over CLM config for test flexibility
const operation = ctx.operation || config.process || config.action || config.operation || 'normalize';
const lambdaConfig = { ...config, operation };
const strategy = lambdaConfig.strategy || ctx.strategy || 'normal';
const maxSteps = lambdaConfig.maxSteps || ctx.maxSteps || ctx.max_steps || 1000;
const maxTimeMs = lambdaConfig.maxTimeMs ||
ctx.maxTimeMs ||
ctx.max_time_ms ||
ctx.timeoutMs ||
ctx.timeout_ms;
// Auto-parse expression if provided directly
if (termHash && (termHash.includes('\\') || termHash.includes('λ') || termHash.includes(' ') || termHash.includes('('))) {
try {
termHash = await parseLambdaExpression(this.collection, termHash);
}
catch (e) {
return {
success: false,
error: `Failed to parse input expression: ${e instanceof Error ? e.message : String(e)}`
};
}
}
// Built-in operations that don't necessarily require a term
if (operation === 'check-readiness') {
return this.doCheckReadiness(ctx);
}
if (operation.startsWith('num-')) {
return this.doNumericOp(operation, ctx);
}
if (operation === 'http-request') {
return this.doHttpRequest(ctx);
}
if (!termHash || termHash === 'lambda-op' || termHash === 'builtin') {
// Try to get from context if missing
const expression = ctx.expression || ctx.term;
if (expression) {
try {
termHash = await parseLambdaExpression(this.collection, expression);
}
catch (e) {
return { success: false, error: `Invalid expression in context: ${e}` };
}
}
else if (operation !== 'parse' && operation !== 'build') {
return { success: false, error: `Lambda operation ${operation} requires a term or expression` };
}
}
try {
switch (operation) {
case 'alpha':
return this.doAlphaRename(termHash, lambdaConfig, ctx);
case 'beta':
return this.doBetaReduce(termHash);
case 'eta-reduce':
return this.doEtaReduce(termHash);
case 'eta-expand':
return this.doEtaExpand(termHash, lambdaConfig, ctx);
case 'normalize':
return this.doNormalize(termHash, { ...lambdaConfig, strategy, maxSteps, maxTimeMs });
case 'step':
return this.doStep(termHash, { ...lambdaConfig, strategy });
case 'alpha-equiv':
return this.doAlphaEquiv(termHash, lambdaConfig, ctx);
case 'eta-equiv':
return this.doEtaEquiv(termHash, lambdaConfig, ctx);
case 'alpha-norm':
return this.doAlphaNormalize(termHash);
case 'eta-norm':
return this.doEtaNormalize(termHash);
case 'free-vars':
return this.doFreeVars(termHash);
case 'is-closed':
return this.doIsClosed(termHash);
case 'is-normal':
return this.doIsNormal(termHash);
case 'parse':
return this.doParse(ctx);
case 'pretty':
return this.doPretty(termHash);
case 'build':
return this.doBuild(ctx);
case 'church-to-int':
return this.doChurchToInt(termHash);
default:
return {
success: false,
error: `Unknown Lambda operation: ${operation}`
};
}
}
catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : String(err)
};
}
}
// ─────────────────────────────────────────────────────────────────────────
// Operation Implementations
// ─────────────────────────────────────────────────────────────────────────
async doAlphaRename(termHash, config, ctx) {
const newName = config.newName || ctx.newName;
if (!newName) {
return { success: false, error: 'Alpha rename requires newName parameter' };
}
const result = await alphaRename(this.collection, termHash, newName).run();
if (result.isLeft) {
return { success: false, error: result.left };
}
const pretty = await prettyPrintDeep(this.collection, result.right);
return {
success: true,
result: result.right,
termHash: result.right,
prettyPrint: pretty
};
}
async doBetaReduce(termHash) {
const result = await betaReduce(this.collection, termHash).run();
if (result.isLeft) {
return { success: false, error: result.left };
}
const pretty = await prettyPrintDeep(this.collection, result.right);
return {
success: true,
result: result.right,
termHash: result.right,
prettyPrint: pretty
};
}
async doEtaReduce(termHash) {
const result = await etaReduce(this.collection, termHash).run();
if (result.isNothing) {
return { success: false, error: 'Not an η-redex' };
}
const pretty = await prettyPrintDeep(this.collection, result.value);
return {
success: true,
result: result.value,
termHash: result.value,
prettyPrint: pretty
};
}
async doEtaExpand(termHash, config, ctx) {
const freshVar = config.freshVar || ctx.freshVar || 'x';
const result = await etaExpand(this.collection, termHash, freshVar).run();
const pretty = await prettyPrintDeep(this.collection, result);
return {
success: true,
result: result,
termHash: result,
prettyPrint: pretty
};
}
async doNormalize(termHash, config) {
const strategy = config.strategy || 'normal';
const maxSteps = config.maxSteps || 1000;
const maxTimeMs = config.maxTimeMs;
// Create IO effects handler from config
const ioHandler = createIOHandler(config);
// IO callback for step events
const onStep = ioHandler.isEnabled() ? async (step, hash) => {
const pretty = await prettyPrintDeep(this.collection, hash);
await ioHandler.emitStep(step, hash, pretty);
} : undefined;
const result = await normalize(this.collection, termHash, strategy, maxSteps, maxTimeMs, onStep).run();
if (result.isLeft) {
// IO Effect: emit error
if (ioHandler.isEnabled()) {
await ioHandler.emitError(result.left, 0);
}
return { success: false, error: result.left };
}
const normResult = result.right;
const pretty = await prettyPrintDeep(this.collection, normResult.normalForm);
// IO Effect: emit completion
if (ioHandler.isEnabled()) {
await ioHandler.emitComplete(normResult.normalForm, pretty, normResult.steps, normResult.reductionPath);
}
return {
success: true,
result: {
normalForm: normResult.normalForm,
steps: normResult.steps,
reductionPath: normResult.reductionPath,
ioEvents: ioHandler.getEvents() // Include IO events in result
},
termHash: normResult.normalForm,
prettyPrint: pretty
};
}
async doStep(termHash, config) {
const strategy = config.strategy || 'normal';
const result = await reduceStep(this.collection, termHash, strategy).run();
if (result.isNothing) {
return {
success: true,
result: { alreadyNormal: true },
termHash: termHash,
prettyPrint: await prettyPrintDeep(this.collection, termHash)
};
}
const pretty = await prettyPrintDeep(this.collection, result.value);
return {
success: true,
result: result.value,
termHash: result.value,
prettyPrint: pretty
};
}
async doAlphaEquiv(termHash, config, ctx) {
const compareWith = config.compareWith || ctx.compareWith;
if (!compareWith) {
return { success: false, error: 'Alpha equivalence check requires compareWith parameter' };
}
const result = await alphaEquivalent(this.collection, termHash, compareWith).run();
if (result.isLeft) {
return { success: false, error: result.left };
}
return {
success: true,
result: { equivalent: result.right }
};
}
async doEtaEquiv(termHash, config, ctx) {
const compareWith = config.compareWith || ctx.compareWith;
if (!compareWith) {
return { success: false, error: 'Eta equivalence check requires compareWith parameter' };
}
const result = await etaEquivalent(this.collection, termHash, compareWith).run();
return {
success: true,
result: { equivalent: result }
};
}
async doAlphaNormalize(termHash) {
const result = await alphaNormalize(this.collection, termHash).run();
if (result.isLeft) {
return { success: false, error: result.left };
}
const pretty = await prettyPrintDeep(this.collection, result.right);
return {
success: true,
result: result.right,
termHash: result.right,
prettyPrint: pretty
};
}
async doEtaNormalize(termHash) {
const result = await etaNormalize(this.collection, termHash).run();
const pretty = await prettyPrintDeep(this.collection, result);
return {
success: true,
result: result,
termHash: result,
prettyPrint: pretty
};
}
async doFreeVars(termHash) {
const result = await freeVariables(this.collection, termHash).run();
if (result.isNothing) {
return { success: false, error: `Term not found: ${termHash}` };
}
return {
success: true,
result: { freeVariables: Array.from(result.value) }
};
}
async doIsClosed(termHash) {
const result = await isClosed(this.collection, termHash).run();
return {
success: true,
result: { closed: result }
};
}
async doIsNormal(termHash) {
const result = await isNormalForm(this.collection, termHash).run();
return {
success: true,
result: { normalForm: result }
};
}
async doParse(ctx) {
const expression = ctx.expression;
if (!expression) {
return { success: false, error: 'Parse requires expression parameter' };
}
try {
const hash = await parseLambdaExpression(this.collection, expression);
const pretty = await prettyPrintDeep(this.collection, hash);
return {
success: true,
result: hash,
termHash: hash,
prettyPrint: pretty
};
}
catch (err) {
return {
success: false,
error: `Parse error: ${err instanceof Error ? err.message : String(err)}`
};
}
}
async doPretty(termHash) {
const pretty = await prettyPrintDeep(this.collection, termHash);
return {
success: true,
result: pretty,
prettyPrint: pretty
};
}
async doBuild(ctx) {
const spec = ctx.term;
if (!spec) {
return { success: false, error: 'Build requires term specification' };
}
const hash = await storeTerm(this.collection, spec);
const pretty = await prettyPrintDeep(this.collection, hash);
return {
success: true,
result: hash,
termHash: hash,
prettyPrint: pretty
};
}
/**
* Decode a Church numeral to a regular integer.
* Church numeral n = λf.λx.f^n(x) where f is applied n times.
*
* Algorithm:
* 1. Normalize the term first
* 2. Expect form: Abs(f, Abs(x, body))
* 3. Count how many times 'f' appears in application position in body
*/
async doChurchToInt(termHash) {
try {
// First normalize the term
const result = await normalize(this.collection, termHash, 'normal', 1000).run();
if (result.isLeft) {
return { success: false, error: result.left };
}
const normResult = result.right;
const pretty = await prettyPrintDeep(this.collection, normResult.normalForm);
// Decode Church numeral structure: λf.λx.body
const count = await this.countChurchApplications(normResult.normalForm);
return {
success: true,
result: count,
prettyPrint: `${count} (Church: ${pretty})`
};
}
catch (err) {
return {
success: false,
error: `Church decode error: ${err instanceof Error ? err.message : String(err)}`
};
}
}
/**
* Count applications in a Church numeral body.
* Church numeral n has the form: λf.λx.f(f(f(...f(x)...)))
* where f appears n times.
*/
async countChurchApplications(termHash) {
const term = await loadTerm(this.collection, termHash);
if (!term)
return -1;
// Expect: Abs(f, Abs(x, body))
if (term.tag !== 'Abs')
return -1;
const fVar = term.param;
const innerTerm = await loadTerm(this.collection, term.body);
if (!innerTerm || innerTerm.tag !== 'Abs')
return -1;
const xVar = innerTerm.param;
// Count how many times we see App(f, ...) wrapping
let count = 0;
let currentHash = innerTerm.body;
while (true) {
const body = await loadTerm(this.collection, currentHash);
if (!body)
break;
if (body.tag === 'App') {
const func = await loadTerm(this.collection, body.func);
// Check if function is the 'f' variable
if (func && func.tag === 'Var' && func.name === fVar) {
count++;
currentHash = body.arg;
}
else {
// Not a simple Church numeral structure
break;
}
}
else if (body.tag === 'Var' && body.name === xVar) {
// Reached the base case 'x'
return count;
}
else {
break;
}
}
return count;
}
async doCheckReadiness(ctx) {
const runtimeName = ctx.runtime_name || 'lambda';
let available = false;
try {
const status = await RuntimeFactory.getAvailableRuntimes();
available = !!status[runtimeName] || runtimeName === 'lambda';
}
catch (e) {
// Fallback
available = runtimeName === 'lambda' || runtimeName === 'javascript';
}
return {
success: true,
result: `Runtime ${runtimeName} status: ${available ? 'True' : 'False'}`
};
}
async doNumericOp(op, ctx) {
const a = Number(ctx.a ?? 0);
const b = Number(ctx.b ?? 0);
let res = 0;
switch (op) {
case 'num-add':
res = a + b;
break;
case 'num-sub':
res = a - b;
break;
case 'num-mul':
res = a * b;
break;
case 'num-div':
res = b !== 0 ? a / b : 0;
break;
default: return { success: false, error: `Unknown numeric op: ${op}` };
}
return {
success: true,
result: String(res)
};
}
async doHttpRequest(ctx) {
const url = ctx.url;
if (!url)
return { success: false, error: "http-request requires 'url'" };
try {
const response = await fetch(url);
const text = await response.text();
return {
success: true,
result: text.substring(0, 1000) // Truncate for safety
};
}
catch (e) {
return { success: false, error: `HTTP Request failed: ${e}` };
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Simple Parser for Lambda Expressions
// ─────────────────────────────────────────────────────────────────────────────
/**
* Parse a simple Lambda expression string into MCards
*
* Syntax:
* x, y, z - Variables
* \x.M or λx.M - Abstraction
* (M N) - Application
* M N - Application (left-associative)
*
* Examples:
* \x.x - Identity function
* \f.\x.f x - Application combinator
* (\x.x) y - Identity applied to y
*/
export async function parseLambdaExpression(collection, expression) {
const tokens = tokenize(expression);
let pos = 0;
function peek() {
return pos < tokens.length ? tokens[pos] : null;
}
function consume() {
if (pos >= tokens.length)
throw new Error('Unexpected end of expression');
return tokens[pos++];
}
function expect(token) {
const actual = consume();
if (actual !== token) {
throw new Error(`Expected '${token}', got '${actual}'`);
}
}
async function parseExpr() {
const terms = [];
while (peek() && peek() !== ')') {
terms.push(await parseTerm());
}
if (terms.length === 0) {
throw new Error('Empty expression');
}
// Left-associate applications: a b c = ((a b) c)
let result = terms[0];
for (let i = 1; i < terms.length; i++) {
const app = mkApp(result, terms[i]);
result = await storeTerm(collection, app);
}
return result;
}
async function parseTerm() {
const token = peek();
if (token === '\\' || token === 'λ') {
return parseAbstraction();
}
if (token === '(') {
consume(); // (
const expr = await parseExpr();
expect(')');
return expr;
}
// Variable
const name = consume();
if (!name.startsWith('"') && !name.match(/^[a-zA-Z_][a-zA-Z0-9_']*$/)) {
throw new Error(`Invalid variable name: ${name}`);
}
const varTerm = mkVar(name);
return storeTerm(collection, varTerm);
}
async function parseAbstraction() {
consume(); // \ or λ
const param = consume();
if (!param.match(/^[a-zA-Z_][a-zA-Z0-9_']*$/)) {
throw new Error(`Invalid parameter name: ${param}`);
}
expect('.');
const body = await parseExpr();
const abs = mkAbs(param, body);
return storeTerm(collection, abs);
}
return parseExpr();
}
function tokenize(expression) {
const tokens = [];
let i = 0;
while (i < expression.length) {
const ch = expression[i];
// Skip whitespace
if (/\s/.test(ch)) {
i++;
continue;
}
// Single-character tokens
if ('()\\λ.'.includes(ch)) {
tokens.push(ch);
i++;
continue;
}
// String literal
if (ch === '"') {
let str = '"';
i++;
while (i < expression.length && expression[i] !== '"') {
str += expression[i++];
}
if (i < expression.length) {
str += '"';
i++;
}
else {
throw new Error('Unterminated string literal');
}
tokens.push(str);
continue;
}
// Identifiers
if (/[a-zA-Z_]/.test(ch)) {
let name = '';
while (i < expression.length && /[a-zA-Z0-9_']/.test(expression[i])) {
name += expression[i++];
}
tokens.push(name);
continue;
}
throw new Error(`Unexpected character: ${ch}`);
}
return tokens;
}
//# sourceMappingURL=LambdaRuntime.js.map