UNPKG

mcard-js

Version:

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

227 lines 9.4 kB
/** * Free Variables Analysis * * Computes the set of free variables in a Lambda term. * Free variables are those not bound by any enclosing λ. * * FV(x) = {x} * FV(λx.M) = FV(M) \ {x} * FV(M N) = FV(M) ∪ FV(N) * * @module mcard-js/ptr/lambda/FreeVariables */ import { loadTerm } from './LambdaTerm'; import { IO } from '../../monads/IO'; import { Maybe } from '../../monads/Maybe'; // ───────────────────────────────────────────────────────────────────────────── // Free Variable Cache (avoids redundant tree traversals) // ───────────────────────────────────────────────────────────────────────────── // Global cache for free variables: hash -> Set<string> // Since terms are immutable and content-addressed, FV is deterministic const freeVarCache = new Map(); /** * Clear the free variable cache. */ export function clearFreeVarCache() { freeVarCache.clear(); } // ───────────────────────────────────────────────────────────────────────────── // Free Variable Computation // ───────────────────────────────────────────────────────────────────────────── /** * Compute free variables of a term (by hash) * Returns IO<Maybe<Set<string>>> - Nothing if term not found */ export function freeVariables(collection, termHash) { return IO.of(async () => { const result = await computeFreeVars(collection, termHash, new Set()); return result; }); } /** * Internal recursive implementation with caching */ async function computeFreeVars(collection, termHash, visited) { // Check cache first const cached = freeVarCache.get(termHash); if (cached) { return Maybe.just(new Set(cached)); // Return copy to avoid mutation } // Cycle detection if (visited.has(termHash)) { return Maybe.just(new Set()); } visited.add(termHash); const term = await loadTerm(collection, termHash); if (!term) { return Maybe.nothing(); } const result = await freeVarsOfTerm(collection, term, visited); // Cache the result freeVarCache.set(termHash, new Set(result)); return Maybe.just(result); } /** * Compute free variables of a term structure */ async function freeVarsOfTerm(collection, term, visited) { switch (term.tag) { case 'Var': // FV(x) = {x} return new Set([term.name]); case 'Abs': { // FV(λx.M) = FV(M) \ {x} const bodyFV = await computeFreeVars(collection, term.body, visited); if (bodyFV.isNothing) { throw new Error(`Body term not found: ${term.body}`); } const result = new Set(bodyFV.value); result.delete(term.param); return result; } case 'App': { // FV(M N) = FV(M) ∪ FV(N) const funcFV = await computeFreeVars(collection, term.func, visited); const argFV = await computeFreeVars(collection, term.arg, visited); if (funcFV.isNothing) { throw new Error(`Function term not found: ${term.func}`); } if (argFV.isNothing) { throw new Error(`Argument term not found: ${term.arg}`); } return union(funcFV.value, argFV.value); } } } // ───────────────────────────────────────────────────────────────────────────── // Bound Variables // ───────────────────────────────────────────────────────────────────────────── /** * Compute bound variables of a term (by hash) * Bound variables are those occurring in binding positions (λx) */ export function boundVariables(collection, termHash) { return IO.of(async () => { return computeBoundVars(collection, termHash, new Set()); }); } async function computeBoundVars(collection, termHash, visited) { if (visited.has(termHash)) { return Maybe.just(new Set()); } visited.add(termHash); const term = await loadTerm(collection, termHash); if (!term) { return Maybe.nothing(); } switch (term.tag) { case 'Var': // No bound variables in a variable return Maybe.just(new Set()); case 'Abs': { // BV(λx.M) = {x} ∪ BV(M) const bodyBV = await computeBoundVars(collection, term.body, visited); if (bodyBV.isNothing) return bodyBV; const result = new Set(bodyBV.value); result.add(term.param); return Maybe.just(result); } case 'App': { // BV(M N) = BV(M) ∪ BV(N) const funcBV = await computeBoundVars(collection, term.func, visited); const argBV = await computeBoundVars(collection, term.arg, visited); if (funcBV.isNothing || argBV.isNothing) { return Maybe.nothing(); } return Maybe.just(union(funcBV.value, argBV.value)); } } } // ───────────────────────────────────────────────────────────────────────────── // Utility: Check if variable is free in term // ───────────────────────────────────────────────────────────────────────────── /** * Check if a variable is free in a term */ export function isFreeIn(collection, variable, termHash) { return freeVariables(collection, termHash).map(maybeFV => { if (maybeFV.isNothing) return false; return maybeFV.value.has(variable); }); } /** * Check if a term is closed (has no free variables) */ export function isClosed(collection, termHash) { return freeVariables(collection, termHash).map(maybeFV => { if (maybeFV.isNothing) return false; return maybeFV.value.size === 0; }); } // ───────────────────────────────────────────────────────────────────────────── // Fresh Variable Generation // ───────────────────────────────────────────────────────────────────────────── /** * Generate a fresh variable name that is not in the given set. * Uses primes (x, x', x'', ...) to match Python implementation. */ export function generateFresh(base, avoid) { if (!avoid.has(base)) { return base; } // Use primes like Python: x, x', x'', x'''... let candidate = base + "'"; while (avoid.has(candidate)) { candidate += "'"; } return candidate; } /** * Generate a fresh variable avoiding all variables in a term */ export async function generateFreshFor(collection, base, termHash) { const fvResult = await freeVariables(collection, termHash).run(); const bvResult = await boundVariables(collection, termHash).run(); const avoid = new Set(); if (fvResult.isJust) { for (const v of fvResult.value) avoid.add(v); } if (bvResult.isJust) { for (const v of bvResult.value) avoid.add(v); } return generateFresh(base, avoid); } // ───────────────────────────────────────────────────────────────────────────── // Set Utilities // ───────────────────────────────────────────────────────────────────────────── function union(a, b) { const result = new Set(a); for (const item of b) { result.add(item); } return result; } export function difference(a, b) { const result = new Set(); for (const item of a) { if (!b.has(item)) { result.add(item); } } return result; } export function intersection(a, b) { const result = new Set(); for (const item of a) { if (b.has(item)) { result.add(item); } } return result; } //# sourceMappingURL=FreeVariables.js.map