// src/core/oracle/trs.ts // SOURCE: ARCHITECTURE/32-LANGUAGE-OFFICIAL-13-IMPLEMENTATION-16.md // Patch Set 6-A2: Deterministic Term Rewriting System engine import type { Val } from "../../../../src/core/eval/values"; import { matchAST, type Bindings } from "./match"; /** * A rewrite rule: pattern → template, optionally guarded by a predicate. */ export type Rule = { name: string; pattern: Val; template: Val; /** Optional predicate over bindings. Returns true if rule is applicable. */ where?: (bindings: Bindings) => boolean; }; /** * Traversal strategy for applying rewrite rules. */ export type Strategy = "topdown" | "bottomup"; /** * Result of a single rewrite step. */ export type RewriteResult = { changed: boolean; result: Val; ruleName?: string; position?: string; }; /** * Conflict report from confluence analysis. */ export type ConflictReport = { rule1: string; rule2: string; overlap: "same-head" | "embeds" | "similar-shape"; description: string; }; // ----------------------------------------------------------------------------- // Pattern substitution: apply bindings to a template // ----------------------------------------------------------------------------- function isObj(x: unknown): x is Record { return !!x && typeof x === "object" && !Array.isArray(x); } function isVarRef(node: Val): string | null { // Template variable: {tag:"Sym", name:"?x"} or {tag:"Var", name:"?x"} if (isObj(node) && typeof (node as any).name === "string") { const name = (node as any).name as string; if (name.startsWith("?")) return name.slice(1); } return null; } /** * Substitute bindings into a template, producing a new Val. */ export function substitute(template: Val, bindings: Bindings): Val { const ref = isVarRef(template); if (ref !== null) { if (ref in bindings) { return bindings[ref] as Val; } // Unbound variable stays as-is (for partial templates) return template; } if (template === null || typeof template !== "object") { return template; } if (Array.isArray(template)) { return template.map(item => substitute(item as Val, bindings)) as unknown as Val; } // Object (Val node) const obj = template as Record; const result: Record = {}; for (const k of Object.keys(obj)) { if (k === "loc" || k === "span") { // Don't recurse into source locations result[k] = obj[k]; } else { result[k] = substitute(obj[k] as Val, bindings); } } return result as Val; } // ----------------------------------------------------------------------------- // Tree traversal helpers // ----------------------------------------------------------------------------- function getChildren(node: Val): { key: string; child: Val }[] { if (node === null || typeof node !== "object") return []; if (Array.isArray(node)) { return node.map((item, i) => ({ key: String(i), child: item as Val })); } const obj = node as Record; const children: { key: string; child: Val }[] = []; // Recurse into known Val structural fields switch (obj.tag) { case "Pair": children.push({ key: "car", child: obj.car as Val }); children.push({ key: "cdr", child: obj.cdr as Val }); break; case "Vector": (obj.items as Val[]).forEach((item, i) => { children.push({ key: `items[${i}]`, child: item }); }); break; case "Map": (obj.entries as Array<[Val, Val]>).forEach(([k, v], i) => { children.push({ key: `entries[${i}][0]`, child: k }); children.push({ key: `entries[${i}][1]`, child: v }); }); break; case "Syntax": children.push({ key: "stx", child: obj.stx as Val }); break; // For quoted Exprs, recurse into their structure default: // Generic object: recurse into non-special fields for (const k of Object.keys(obj)) { if (k === "tag" || k === "loc" || k === "span") continue; const v = obj[k]; if (v && typeof v === "object") { children.push({ key: k, child: v as Val }); } } } return children; } function setChild(node: Val, key: string, newChild: Val): Val { if (Array.isArray(node)) { const idx = parseInt(key, 10); const copy = [...node]; copy[idx] = newChild; return copy as unknown as Val; } const obj = node as Record; const copy = { ...obj }; // Handle indexed keys like "items[0]" or "entries[0][1]" const arrayMatch = key.match(/^(\w+)\[(\d+)\](?:\[(\d+)\])?$/); if (arrayMatch) { const [, field, idx1, idx2] = arrayMatch; const arr = [...(copy[field] as unknown[])]; if (idx2 !== undefined) { // entries[i][j] const inner = [...(arr[parseInt(idx1, 10)] as unknown[])]; inner[parseInt(idx2, 10)] = newChild; arr[parseInt(idx1, 10)] = inner; } else { arr[parseInt(idx1, 10)] = newChild; } copy[field] = arr; } else { copy[key] = newChild; } return copy as Val; } // ----------------------------------------------------------------------------- // Core rewrite engine // ----------------------------------------------------------------------------- /** * Try to apply a single rule at the root of the node. * Returns the rewritten node if the rule matched, or null otherwise. */ function tryApplyRule(rule: Rule, node: Val): Val | null { const { ok, bindings } = matchAST(rule.pattern, node); if (!ok) return null; if (rule.where && !rule.where(bindings)) return null; return substitute(rule.template, bindings); } /** * Apply the first matching rule at the root. * Returns { changed: true, result, ruleName } if a rule matched, * or { changed: false, result: node } otherwise. */ function applyRulesAtRoot(rules: Rule[], node: Val): RewriteResult { for (const rule of rules) { const result = tryApplyRule(rule, node); if (result !== null) { return { changed: true, result, ruleName: rule.name }; } } return { changed: false, result: node }; } /** * Rewrite once using topdown strategy: try root first, then children. */ function rewriteOnceTopdown(rules: Rule[], node: Val, path: string = ""): RewriteResult { // Try root first const rootResult = applyRulesAtRoot(rules, node); if (rootResult.changed) { return { ...rootResult, position: path || "root" }; } // Try children const children = getChildren(node); for (const { key, child } of children) { const childPath = path ? `${path}.${key}` : key; const childResult = rewriteOnceTopdown(rules, child, childPath); if (childResult.changed) { const newNode = setChild(node, key, childResult.result); return { changed: true, result: newNode, ruleName: childResult.ruleName, position: childResult.position }; } } return { changed: false, result: node }; } /** * Rewrite once using bottomup strategy: try children first, then root. */ function rewriteOnceBottomup(rules: Rule[], node: Val, path: string = ""): RewriteResult { // Try children first const children = getChildren(node); let currentNode = node; let anyChildChanged = false; for (const { key, child } of children) { const childPath = path ? `${path}.${key}` : key; const childResult = rewriteOnceBottomup(rules, child, childPath); if (childResult.changed) { currentNode = setChild(currentNode, key, childResult.result); anyChildChanged = true; // Return on first change (outermost bottomup) return { changed: true, result: currentNode, ruleName: childResult.ruleName, position: childResult.position }; } } // Try root after children const rootResult = applyRulesAtRoot(rules, currentNode); if (rootResult.changed) { return { ...rootResult, position: path || "root" }; } return { changed: anyChildChanged, result: currentNode }; } /** * Apply rules once at the first matching position. */ export function rewriteOnce(rules: Rule[], expr: Val, strategy: Strategy = "topdown"): RewriteResult { if (strategy === "topdown") { return rewriteOnceTopdown(rules, expr); } else { return rewriteOnceBottomup(rules, expr); } } /** * Apply rules repeatedly until fixpoint or fuel exhausted. * @param fuel Maximum number of rewrite steps (default 100) */ export function rewriteFixpoint( rules: Rule[], expr: Val, strategy: Strategy = "topdown", fuel: number = 100 ): { result: Val; steps: number; reachedFixpoint: boolean } { let current = expr; let steps = 0; while (steps < fuel) { const { changed, result } = rewriteOnce(rules, current, strategy); if (!changed) { return { result: current, steps, reachedFixpoint: true }; } current = result; steps++; } return { result: current, steps, reachedFixpoint: false }; } /** * Apply rules and return the full trace of intermediate results. */ export function rewriteTrace( rules: Rule[], expr: Val, strategy: Strategy = "topdown", fuel: number = 100 ): { trace: Array<{ expr: Val; ruleName?: string; position?: string }>; reachedFixpoint: boolean } { const trace: Array<{ expr: Val; ruleName?: string; position?: string }> = [{ expr }]; let current = expr; let steps = 0; while (steps < fuel) { const { changed, result, ruleName, position } = rewriteOnce(rules, current, strategy); if (!changed) { return { trace, reachedFixpoint: true }; } current = result; trace.push({ expr: current, ruleName, position }); steps++; } return { trace, reachedFixpoint: false }; } // ----------------------------------------------------------------------------- // Confluence diagnostic: critical pair detection // ----------------------------------------------------------------------------- /** * Extract the "head shape" of a pattern for overlap detection. * For Val nodes, this is the tag + direct structure. */ function getPatternHead(pattern: Val): string { if (pattern === null || typeof pattern !== "object") { return String(pattern); } if (Array.isArray(pattern)) { return `Array[${pattern.length}]`; } const obj = pattern as Record; if (obj.tag) { return String(obj.tag); } return "Object"; } /** * Check if two patterns have structural overlap that could cause non-confluence. */ function patternsOverlap(p1: Val, p2: Val): "same-head" | "embeds" | "similar-shape" | null { const head1 = getPatternHead(p1); const head2 = getPatternHead(p2); // Different head tags - no overlap if (head1 !== head2) return null; // Same head tag - need deeper analysis if (isObj(p1) && isObj(p2)) { const obj1 = p1 as Record; const obj2 = p2 as Record; // For App nodes, check if the function names differ if (obj1.tag === "App" && obj2.tag === "App") { const fn1 = obj1.fn as Record | undefined; const fn2 = obj2.fn as Record | undefined; // If both have concrete (non-variable) function names that differ, no overlap if (fn1 && fn2 && fn1.tag === "Var" && fn2.tag === "Var" && typeof fn1.name === "string" && typeof fn2.name === "string" && !fn1.name.startsWith("?") && !fn2.name.startsWith("?") && fn1.name !== fn2.name) { return null; } } // For Var nodes with different concrete names, no overlap if (obj1.tag === "Var" && obj2.tag === "Var") { const name1 = obj1.name as string | undefined; const name2 = obj2.name as string | undefined; if (name1 && name2 && !name1.startsWith("?") && !name2.startsWith("?") && name1 !== name2) { return null; } } // For Num nodes with different values, no overlap if (obj1.tag === "Num" && obj2.tag === "Num") { if (obj1.n !== obj2.n) { return null; } } // For Sym nodes with different concrete names, no overlap if (obj1.tag === "Sym" && obj2.tag === "Sym") { const name1 = obj1.name as string | undefined; const name2 = obj2.name as string | undefined; if (name1 && name2 && !name1.startsWith("?") && !name2.startsWith("?") && name1 !== name2) { return null; } } // Count pattern variables to check for embedding const vars1 = countPatternVars(p1); const vars2 = countPatternVars(p2); // If one has more concrete structure (fewer vars), it might embed if (Math.abs(vars1 - vars2) > 0) { return "embeds"; } // Same structure - potential conflict return "same-head"; } // Primitive types with same head if (head1 !== "Object") { return "same-head"; } return null; } function countPatternVars(node: unknown): number { if (node === null) return 0; if (typeof node === "string") return node.startsWith("?") ? 1 : 0; if (typeof node !== "object") return 0; if (Array.isArray(node)) { return node.reduce((acc, item) => acc + countPatternVars(item), 0); } const obj = node as Record; if (typeof obj.name === "string" && obj.name.startsWith("?")) return 1; let count = 0; for (const k of Object.keys(obj)) { if (k !== "tag" && k !== "loc" && k !== "span") { count += countPatternVars(obj[k]); } } return count; } /** * Detect potential non-confluence in a rule set. * Returns conflict reports for pairs of rules that might produce different results. */ export function detectConflicts(rules: Rule[]): ConflictReport[] { const conflicts: ConflictReport[] = []; for (let i = 0; i < rules.length; i++) { for (let j = i + 1; j < rules.length; j++) { const r1 = rules[i]; const r2 = rules[j]; const overlap = patternsOverlap(r1.pattern, r2.pattern); if (overlap) { conflicts.push({ rule1: r1.name, rule2: r2.name, overlap, description: `Rules '${r1.name}' and '${r2.name}' have overlapping patterns (${overlap}). ` + `This may cause non-confluent rewriting depending on rule order.` }); } } } return conflicts; } // ----------------------------------------------------------------------------- // Rule construction helpers // ----------------------------------------------------------------------------- /** * Create a rewrite rule. */ export function rule(name: string, pattern: Val, template: Val, where?: (bindings: Bindings) => boolean): Rule { return { name, pattern, template, where }; } /** * Create a rule from pattern/template Val objects. */ export function makeRule( name: string, pattern: Val, template: Val, wherePred?: Val ): Rule { // wherePred would need to be evaluated in context; for now, leave it as optional JS function return { name, pattern, template }; }