UNPKG

jambda-calc

Version:

Convert JavaScript functions to the Lambda Calculus, and visualize them with Tromp diagrams.

390 lines 18.5 kB
import * as esprima from 'esprima'; import * as ts from 'typescript'; const CHURCH_NUMERALS = {}; // we should implement numbers over 100 with operations using church numbers function generateChurchNumerals(max = 100) { for (let i = 0; i <= max; i++) { let inner = 'x'; for (let j = 0; j < i; j++) { inner = `f(${inner})`; } CHURCH_NUMERALS[i] = `(λf.λx.${inner})`; } } generateChurchNumerals(); /** * Main transpilation function that converts JS/TS to lambda calculus */ export function transpile(code) { try { const isTypeScript = code.includes(':') || code.includes('interface') || code.includes('type '); const jsCode = isTypeScript ? ts.transpileModule(code, { // compile ts if needed compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2015, removeComments: true, }, }).outputText : code; // compile into js abstract syntax tree const ast = esprima.parseScript(jsCode); // find all function declarations const functionDeclarations = new Map(); for (const node of ast.body) { if (node.type === 'FunctionDeclaration' && node.id) { functionDeclarations.set(node.id.name, node); } } if (functionDeclarations.size === 0) { throw new Error('No function declarations found in input file'); } // processing first function as the entry point const mainFuncName = [...functionDeclarations.keys()][0]; const mainFunc = functionDeclarations.get(mainFuncName); // global scope for all funcs const globalScope = new Map(); for (const [name, func] of functionDeclarations.entries()) { globalScope.set(name, { type: 'function', declaration: func, }); } // process the main function with access to the other funcs (for if they're used in the main func) return processMainFunction(mainFunc, functionDeclarations, globalScope); } catch (error) { console.error('Failed to transpile to lambda calculus:', error instanceof Error ? error.message : String(error)); throw error; } } /** * Process the main function and all its dependencies */ function processMainFunction(mainFunc, allFunctions, globalScope) { // start with local scope const localScope = new Map(); // get vars const params = mainFunc.params.map((param) => { if (param.type === 'Identifier') { return param.name; } throw new Error(`Unsupported parameter type: ${param.type}`); }); // add vars to local scope params.forEach((param) => localScope.set(param, param)); // process body const bodyExpr = processBlock(mainFunc.body, localScope, globalScope, allFunctions); // wrap in lambda abstraction for the main func const lambdaParams = params.map((param) => ${param}`).join('.'); return `${lambdaParams}.${bodyExpr}`; } /** * Process a block of statements, handling variable declarations and the return statement */ function processBlock(block, localScope, globalScope, allFunctions) { // clone to avoid modifying og const blockScope = new Map(localScope); let returnExpr = null; // process each block for (const stmt of block.body) { if (stmt.type === 'VariableDeclaration') { for (const declarator of stmt.declarations) { if (declarator.id.type === 'Identifier' && declarator.init) { const varName = declarator.id.name; const varValue = processExpression(declarator.init, blockScope, globalScope, allFunctions); blockScope.set(varName, varValue); } } } else if (stmt.type === 'ReturnStatement' && stmt.argument) { returnExpr = processExpression(stmt.argument, blockScope, globalScope, allFunctions); break; } else if (stmt.type === 'SwitchStatement') { const switchStmt = stmt; // process the value being switched on const discriminant = processExpression(switchStmt.discriminant, blockScope, globalScope, allFunctions); // find case clauses const cases = switchStmt.cases; if (cases.length > 0) { let resultExpr = null; let defaultCase = null; for (const caseClause of cases) { if (caseClause.test === null) { defaultCase = caseClause; break; } } // process in reverse to create nesting for (let i = cases.length - 1; i >= 0; i--) { const caseClause = cases[i]; if (caseClause.test === null) { continue; // skip default (handle at end) } const caseTest = caseClause.test ? processExpression(caseClause.test, blockScope, globalScope, allFunctions) : CHURCH_NUMERALS[0]; // look for a return in the body let caseResult = null; for (const stmt of caseClause.consequent) { if (stmt.type === 'ReturnStatement' && stmt.argument) { caseResult = processExpression(stmt.argument, blockScope, globalScope, allFunctions); break; } } if (caseResult === null) { continue; } // church conditional encoding const equalityTest = `((λm.λn.((m n) (λx.λy.y)) (λx.λy.x)) ${discriminant} ${caseTest})`; if (resultExpr === null) { resultExpr = defaultCase ? null : CHURCH_NUMERALS[0]; } resultExpr = resultExpr === null ? caseResult : `((λp.λa.λb.p a b) ${equalityTest} ${caseResult} ${resultExpr})`; } // handle default case if (defaultCase) { let defaultResult = null; for (const stmt of defaultCase.consequent) { if (stmt.type === 'ReturnStatement' && stmt.argument) { defaultResult = processExpression(stmt.argument, blockScope, globalScope, allFunctions); break; } } if (defaultResult !== null) { resultExpr = resultExpr === null ? defaultResult : `((λp.λa.λb.p a b) (λx.λy.y) ${resultExpr} ${defaultResult})`; } } if (resultExpr !== null) { returnExpr = resultExpr; break; } } } } if (returnExpr === null) { throw new Error('No return statement found in function body'); } return returnExpr; } /** * Process an expression to lambda calculus */ function processExpression(expr, localScope, globalScope, allFunctions) { switch (expr.type) { case 'Literal': { const literal = expr; const value = literal.value; // number literals become church numerals if (typeof value === 'number') { if (value >= 0 && value <= 100) { const approxInt = Math.round(value); // non ints are currently rounded return CHURCH_NUMERALS[approxInt]; } // TODO: to handle numbers larger than 100 we'd need to process them into a expression involving two smaller numbers // we could have some cool logic for finding the fewest possible operations } // bools: true = λx.λy.x, false = λx.λy.y if (typeof value === 'boolean') { return value ? '(λx.λy.x)' : '(λx.λy.y)'; } // string literals if (typeof value === 'string') { // TODO: handle these as variables. we're currently only processing aritmetic operations so it's not implemented } return CHURCH_NUMERALS[0]; // defaulting to 0 church numeral } case 'Identifier': { const id = expr; const name = id.name; // check if identifier is an input var or local var if (localScope.has(name)) { return localScope.get(name); } // check if it's a function in the global scope if (globalScope.has(name)) { const entry = globalScope.get(name); // process the function if (entry.type === 'function') { const funcEntry = entry; const func = funcEntry.declaration; const params = func.params.map((param) => { if (param.type === 'Identifier') { return param.name; } throw new Error(`Unsupported parameter type: ${param.type}`); }); // create new scope for func const funcScope = new Map(); params.forEach((param) => funcScope.set(param, param)); // process the body const bodyExpr = processBlock(func.body, funcScope, globalScope, allFunctions); const lambdaStr = `(${params.map((p) => ${p}`).join('.')}.${bodyExpr})`; // add to global scope as a variable globalScope.set(name, { type: 'variable', value: lambdaStr }); return lambdaStr; } else if (entry.type === 'variable') { const varEntry = entry; return varEntry.value; } } // if it's not found anywhere, treat as a free variable. although this isn't allowed in pure lambda calc return name; } case 'BinaryExpression': { const binExpr = expr; // process the left and right expressions const left = processExpression(binExpr.left, localScope, globalScope, allFunctions); const right = processExpression(binExpr.right, localScope, globalScope, allFunctions); // construct lambda expression using church operator lambda switch (binExpr.operator) { case '+': return `((λm.λn.λf.λx.m f (n f x)) ${left} ${right})`; case '-': return `((λm.λn.λf.λx.n (λg.λh.h (g f)) (λu.x) (λu.u) m) ${left} ${right})`; case '*': return `((λm.λn.λf.λx.m (n f) x) ${left} ${right})`; case '/': return `((λm.λn.n (λf.λx.m (λg.f (g x)) (λx.x)) (λf.λx.x)) ${left} ${right})`; case '==': case '===': return `((λm.λn.((m n) (λx.λy.y)) (λx.λy.x)) ${left} ${right})`; case '<': return `((λm.λn.(n (λx.λy.y)) ((m (λx.λy.x)) (λx.λy.y))) ${left} ${right})`; case '<=': return `((λm.λn.(λp.λa.λb.p b a) ((λm.λn.(n (λx.λy.y)) ((m (λx.λy.x)) (λx.λy.y))) n m)) ${left} ${right})`; case '>': return `((λm.λn.(n (λx.λy.y)) ((m (λx.λy.x)) (λx.λy.y))) ${right} ${left})`; case '>=': return `((λm.λn.(λp.λa.λb.p b a) ((λm.λn.(n (λx.λy.y)) ((m (λx.λy.x)) (λx.λy.y))) m n)) ${left} ${right})`; default: throw new Error(`Unsupported binary operator: ${binExpr.operator}`); } } case 'UnaryExpression': { const unaryExpr = expr; const argument = processExpression(unaryExpr.argument, localScope, globalScope, allFunctions); switch (unaryExpr.operator) { case '!': return `((λp.λa.λb.p b a) ${argument})`; // church not case '-': return `((λm.λn.λf.λx.n (λg.λh.h (g f)) (λu.x) (λu.u) m) ${CHURCH_NUMERALS[0]} ${argument})`; // church negative default: throw new Error(`Unsupported unary operator: ${unaryExpr.operator}`); } } case 'CallExpression': { const callExpr = expr; // func calls to global funcs if (callExpr.callee.type === 'Identifier') { const funcName = callExpr.callee.name; // inline it if it's defined in globals if (globalScope.has(funcName)) { const entry = globalScope.get(funcName); if (entry.type === 'function') { const funcEntry = entry; const funcDecl = funcEntry.declaration; const args = callExpr.arguments.map((arg) => processExpression(arg, localScope, globalScope, allFunctions)); // new scope for func const funcScope = new Map(); funcDecl.params.forEach((param, i) => { if (param.type === 'Identifier') { const paramName = param.name; funcScope.set(paramName, args[i] || CHURCH_NUMERALS[0]); // 0 for missing vars } else { throw new Error(`Unsupported parameter type: ${param.type}`); } }); // process body with the vars applied return processBlock(funcDecl.body, funcScope, globalScope, allFunctions); } } } // process cases where it's calling something not defined globally // TODO: handle this better, we should have some kind of abstraction logic if it's unknown const callee = processExpression(callExpr.callee, localScope, globalScope, allFunctions); const args = callExpr.arguments.map((arg) => processExpression(arg, localScope, globalScope, allFunctions)); // wrap application in parens let result = `(${callee}`; for (const arg of args) { result += ` ${arg}`; } result += ')'; return result; } case 'ConditionalExpression': { const condExpr = expr; const test = processExpression(condExpr.test, localScope, globalScope, allFunctions); const consequent = processExpression(condExpr.consequent, localScope, globalScope, allFunctions); const alternate = processExpression(condExpr.alternate, localScope, globalScope, allFunctions); return `((λp.λa.λb.p a b) ${test} ${consequent} ${alternate})`; // church if-then-else } case 'ArrowFunctionExpression': case 'FunctionExpression': { const funcExpr = expr; // extract param names const params = funcExpr.params.map((param) => { if (param.type === 'Identifier') { return param.name; } throw new Error(`Unsupported parameter type: ${param.type}`); }); // create a new scope for the func const funcScope = new Map(localScope); // add vars to scope params.forEach((param) => funcScope.set(param, param)); // process body let bodyExpr; if (funcExpr.type === 'ArrowFunctionExpression' && funcExpr.expression && funcExpr.body.type !== 'BlockStatement') { // arrow function with expression body: x => x + 1 bodyExpr = processExpression(funcExpr.body, funcScope, globalScope, allFunctions); } else { // func with block body const body = funcExpr.body; bodyExpr = processBlock(body, funcScope, globalScope, allFunctions); } // construct the lambda abstraction const paramStr = params.map((param) => ${param}`).join('.'); return `(${paramStr}.${bodyExpr})`; } case 'MemberExpression': { const memberExpr = expr; const object = processExpression(memberExpr.object, localScope, globalScope, allFunctions); // common array methods if (memberExpr.property.type === 'Identifier') { const propName = memberExpr.property.name; // using higher order functions for calls we don't have lambda expressions for... might need improvements switch (propName) { case 'map': return `((λarr.λf.arr f) ${object})`; case 'filter': return `((λarr.λpred.arr (λx.pred x)) ${object})`; default: return `((λobj.λprop.obj prop) ${object} (λx.x))`; } } // process the property if it's a computed property if (memberExpr.computed && memberExpr.property.type) { const prop = processExpression(memberExpr.property, localScope, globalScope, allFunctions); return `((λobj.λprop.obj prop) ${object} ${prop})`; } throw new Error('Unsupported member expression property type'); } default: throw new Error(`Unsupported expression type: ${expr.type}`); } } //# sourceMappingURL=index.js.map