UNPKG

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
/** * 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