Full SDK workspace: core, contracts, sdk, cli, browser-host, browser-kit, federation, omega-core, oracle, self-healing, strategies, tools, emacs. Clean extraction from the development monorepo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
494 lines
14 KiB
TypeScript
494 lines
14 KiB
TypeScript
// 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<string, unknown> {
|
|
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<string, unknown>;
|
|
const result: Record<string, unknown> = {};
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
const obj2 = p2 as Record<string, unknown>;
|
|
|
|
// For App nodes, check if the function names differ
|
|
if (obj1.tag === "App" && obj2.tag === "App") {
|
|
const fn1 = obj1.fn as Record<string, unknown> | undefined;
|
|
const fn2 = obj2.fn as Record<string, unknown> | 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<string, unknown>;
|
|
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 };
|
|
}
|