UNPKG

mcard-js

Version:

MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers

244 lines 10.6 kB
/** * Alpha Conversion (α-conversion) * * Renames bound variables in Lambda terms while preserving meaning. * * Rule: λx.M ≡α λy.M[x:=y] (where y is fresh) * * Alpha conversion is the basis of variable renaming and is essential * for avoiding variable capture during beta reduction. * * @module mcard-js/ptr/lambda/AlphaConversion */ import { loadTerm, storeTerm, mkVar, mkAbs, mkApp, isAbs } from './LambdaTerm'; import { freeVariables } from './FreeVariables'; import { IO } from '../../monads/IO'; import { Either } from '../../monads/Either'; // ───────────────────────────────────────────────────────────────────────────── // Alpha Conversion // ───────────────────────────────────────────────────────────────────────────── /** * Alpha-rename: Rename the bound variable of an abstraction * * λx.M → λy.M[x:=y] * * @param collection - Card collection for term storage * @param absHash - Hash of the abstraction term * @param newParam - New name for the bound variable * @returns IO<Either<error, newHash>> */ export function alphaRename(collection, absHash, newParam) { return IO.of(async () => { const term = await loadTerm(collection, absHash); if (!term) { return Either.left(`Term not found: ${absHash}`); } if (!isAbs(term)) { return Either.left(`Alpha conversion only applies to abstractions, got ${term.tag}`); } // If same name, no change needed if (term.param === newParam) { return Either.right(absHash); } // Check that new name doesn't capture free variables in body const bodyFV = await freeVariables(collection, term.body).run(); if (bodyFV.isJust && bodyFV.value.has(newParam)) { return Either.left(`Cannot α-rename to '${newParam}': would capture free variable in body`); } // Substitute old param with new param in body const newBodyHash = await substituteVar(collection, term.body, term.param, newParam); // Create new abstraction const newAbs = mkAbs(newParam, newBodyHash); const resultHash = await storeTerm(collection, newAbs); return Either.right(resultHash); }); } /** * Substitute a variable with another variable name throughout a term * * M[x:=y] - replaces all free occurrences of x with y */ async function substituteVar(collection, termHash, oldVar, newVar) { const term = await loadTerm(collection, termHash); if (!term) { throw new Error(`Term not found: ${termHash}`); } switch (term.tag) { case 'Var': if (term.name === oldVar) { // Replace with new variable const newTerm = mkVar(newVar); return storeTerm(collection, newTerm); } // Different variable, unchanged return termHash; case 'Abs': if (term.param === oldVar) { // Bound variable shadows, stop substitution return termHash; } // Recurse into body const newBody = await substituteVar(collection, term.body, oldVar, newVar); if (newBody === term.body) { // No change return termHash; } const newAbs = mkAbs(term.param, newBody); return storeTerm(collection, newAbs); case 'App': const newFunc = await substituteVar(collection, term.func, oldVar, newVar); const newArg = await substituteVar(collection, term.arg, oldVar, newVar); if (newFunc === term.func && newArg === term.arg) { // No change return termHash; } const newApp = mkApp(newFunc, newArg); return storeTerm(collection, newApp); } } // ───────────────────────────────────────────────────────────────────────────── // Alpha Equivalence // ───────────────────────────────────────────────────────────────────────────── /** * Check if two terms are alpha-equivalent * * Two terms are α-equivalent if they differ only in the names of bound variables. * * Note: If hashes are equal, terms are definitionally identical (stronger than α-equiv). */ export function alphaEquivalent(collection, hash1, hash2) { return IO.of(async () => { // Same hash = identical terms if (hash1 === hash2) { return Either.right(true); } const term1 = await loadTerm(collection, hash1); const term2 = await loadTerm(collection, hash2); if (!term1) return Either.left(`Term not found: ${hash1}`); if (!term2) return Either.left(`Term not found: ${hash2}`); const equiv = await checkAlphaEquiv(collection, term1, term2, new Map(), new Map()); return Either.right(equiv); }); } /** * Internal alpha-equivalence check with variable renaming maps */ async function checkAlphaEquiv(collection, term1, term2, rename1, // Maps var name in term1 to de Bruijn index rename2 // Maps var name in term2 to de Bruijn index ) { if (term1.tag !== term2.tag) { return false; } switch (term1.tag) { case 'Var': { const t2 = term2; const idx1 = rename1.get(term1.name); const idx2 = rename2.get(t2.name); if (idx1 !== undefined && idx2 !== undefined) { // Both are bound variables - compare by binding depth return idx1 === idx2; } if (idx1 === undefined && idx2 === undefined) { // Both are free variables - compare by name return term1.name === t2.name; } // One bound, one free - not equivalent return false; } case 'Abs': { const t2 = term2; // Extend renaming maps with new binding const depth = rename1.size; const newRename1 = new Map(rename1); const newRename2 = new Map(rename2); newRename1.set(term1.param, depth); newRename2.set(t2.param, depth); // Load and compare bodies const body1 = await loadTerm(collection, term1.body); const body2 = await loadTerm(collection, t2.body); if (!body1 || !body2) return false; return checkAlphaEquiv(collection, body1, body2, newRename1, newRename2); } case 'App': { const t2 = term2; const func1 = await loadTerm(collection, term1.func); const func2 = await loadTerm(collection, t2.func); const arg1 = await loadTerm(collection, term1.arg); const arg2 = await loadTerm(collection, t2.arg); if (!func1 || !func2 || !arg1 || !arg2) return false; const funcEquiv = await checkAlphaEquiv(collection, func1, func2, rename1, rename2); if (!funcEquiv) return false; return checkAlphaEquiv(collection, arg1, arg2, rename1, rename2); } } } // ───────────────────────────────────────────────────────────────────────────── // Alpha Normalization (De Bruijn-like canonical form) // ───────────────────────────────────────────────────────────────────────────── /** * Alpha-normalize a term using canonical variable names * * All bound variables are renamed to a₀, a₁, a₂... based on binding depth. * This produces a canonical representative of the α-equivalence class. */ export function alphaNormalize(collection, termHash) { return IO.of(async () => { const result = await normalizeWithDepth(collection, termHash, new Map(), 0); if (result === null) { return Either.left(`Term not found: ${termHash}`); } return Either.right(result); }); } /** * Internal normalization with depth tracking */ async function normalizeWithDepth(collection, termHash, bindings, // original name -> canonical name depth) { const term = await loadTerm(collection, termHash); if (!term) return null; switch (term.tag) { case 'Var': { const canonicalName = bindings.get(term.name); if (canonicalName) { // Bound variable - use canonical name const newVar = mkVar(canonicalName); return storeTerm(collection, newVar); } // Free variable - keep original name return termHash; } case 'Abs': { // Generate canonical name for this binding depth const canonicalName = `a${depth}`; // Extend bindings const newBindings = new Map(bindings); newBindings.set(term.param, canonicalName); // Normalize body const newBody = await normalizeWithDepth(collection, term.body, newBindings, depth + 1); if (newBody === null) return null; const newAbs = mkAbs(canonicalName, newBody); return storeTerm(collection, newAbs); } case 'App': { const newFunc = await normalizeWithDepth(collection, term.func, bindings, depth); const newArg = await normalizeWithDepth(collection, term.arg, bindings, depth); if (newFunc === null || newArg === null) return null; // If unchanged, return original if (newFunc === term.func && newArg === term.arg) { return termHash; } const newApp = mkApp(newFunc, newArg); return storeTerm(collection, newApp); } } } //# sourceMappingURL=AlphaConversion.js.map