import { createHash } from "node:crypto"; import { capabilityId, type AtomId, type Conformance, type InterfaceRevision, type InterfaceRevisionId, } from "./types.js"; import { GenericTypeError, TypeSubstitution, bindTypeParameters, canonicalTypeArgument, type ClosedTypeArgument, type GenericTypeEnvironment, type InterfaceApplicationExpression, type TypeParameter, } from "./generics.js"; /** Compiler input only. Runtime plans contain ordinary closed conformances. */ export interface ConformanceRule { id: string; parameters: TypeParameter[]; source: InterfaceApplicationExpression; target: InterfaceApplicationExpression; } export interface RuleApplication { rule: ConformanceRule; atomId: AtomId; sourceId: InterfaceRevisionId; targetId: InterfaceRevisionId; bindings: Map; id: Conformance["id"]; } const parameterId = (argument: ConformanceRule["source"]["arguments"][number]) => { const value = argument.kind === "value" ? argument.type : argument.target; return value.kind === "parameter" ? value.parameterId : undefined; }; const hasParameter = (node: unknown): boolean => !!node && typeof node === "object" && (("kind" in node && node.kind === "parameter") || Object.values(node).some(hasParameter)); export function validateConformanceRules( rules: readonly ConformanceRule[], definitions: ReadonlyMap, ) { const ids = new Set(); const graph = new Map(); for (const rule of rules) { if (ids.has(rule.id)) throw new GenericTypeError("duplicate-rule-id", rule.id, `Duplicate rule ID ${rule.id}`); ids.add(rule.id); for (const application of [ rule.source, rule.target, ...rule.parameters.flatMap((p) => (p.kind === "object" ? p.implements : [])), ]) { const definition = definitions.get(application.definitionId); if (!definition) throw new GenericTypeError("unknown-interface", rule.id, `Unknown interface ${application.definitionId}`); const parameters = definition.template?.parameters ?? []; if (parameters.length !== application.arguments.length) throw new GenericTypeError("type-arity", rule.id, `Wrong arity for ${definition.displayName}`); parameters.forEach((p, i) => { if (p.kind !== application.arguments[i].kind) throw new GenericTypeError( "parameter-kind", rule.id, `Wrong argument kind for ${definition.displayName}.${p.name}`, ); }); } const captured = new Set(); for (const argument of rule.source.arguments) { const id = parameterId(argument); if (id) captured.add(id); else if (hasParameter(argument)) throw new GenericTypeError( "unsupported-rule-pattern", rule.id, "Capture whole source arguments, not nested type patterns", ); } for (const p of rule.parameters) if (!captured.has(p.id)) throw new GenericTypeError( "uncaptured-rule-parameter", rule.id, `Parameter ${p.name} must occur in the source application`, ); graph.set(rule.source.definitionId, [...(graph.get(rule.source.definitionId) ?? []), rule]); } const visited = new Set(); const active = new Set(); const visit = (id: string, trail: string[]) => { if (active.has(id)) throw new GenericTypeError( "recursive-conformance-rule", trail.at(-1) ?? id, `Recursive conformance rules: ${trail.join(" -> ")}`, ); if (visited.has(id)) return; active.add(id); for (const rule of graph.get(id) ?? []) visit(rule.target.definitionId, [...trail, rule.id]); active.delete(id); visited.add(id); }; for (const id of graph.keys()) visit(id, []); } /** Monotone closure: bounds only enable applications, never invent evidence. */ export function expandConformanceRules( rules: readonly ConformanceRule[], initial: readonly Pick[], definitions: ReadonlyMap, environment: (atom: AtomId, bindings: Map) => GenericTypeEnvironment, ): RuleApplication[] { validateConformanceRules(rules, definitions); const key = (atom: string, iface: string) => JSON.stringify([atom, iface]); const facts = new Map(initial.map((f) => [key(f.atomId, f.interfaceRevisionId), f])); const applications = new Map(); const implies = (actual: InterfaceRevisionId, required: InterfaceRevisionId, seen = new Set()): boolean => { if (actual === required) return true; if (seen.has(actual)) return false; seen.add(actual); return (definitions.get(actual)?.requiredInterfaces ?? []).some((id) => implies(id, required, seen)); }; const bySource = new Map(); for (const rule of [...rules].sort((a, b) => a.id.localeCompare(b.id))) bySource.set(rule.source.definitionId, [...(bySource.get(rule.source.definitionId) ?? []), rule]); let changed = true; while (changed) { changed = false; for (const fact of [...facts.values()].sort((a, b) => key(a.atomId, a.interfaceRevisionId).localeCompare(key(b.atomId, b.interfaceRevisionId)), )) { const definition = definitions.get(fact.interfaceRevisionId); if (!definition) continue; for (const rule of bySource.get(definition.application?.definitionId ?? definition.revisionId) ?? []) { const arguments_ = definition.application?.arguments ?? []; const bindings = new Map(); const env = environment(fact.atomId, bindings); const substitution = new TypeSubstitution(env); if ( !rule.source.arguments.every((pattern, i) => { const actual = arguments_[i]; if (!actual || actual.kind !== pattern.kind) return false; const id = parameterId(pattern); if (!id) return canonicalTypeArgument(substitution.argument(pattern)) === canonicalTypeArgument(actual); const previous = bindings.get(id); if (previous && canonicalTypeArgument(previous) !== canonicalTypeArgument(actual)) return false; bindings.set(id, actual); return true; }) ) continue; try { bindTypeParameters( rule.parameters, rule.parameters.map((p) => bindings.get(p.id)!), { ...env, implementsInterface: (target, required) => target.kind === "atom" ? facts.has(key(target.atomId, required)) : implies(target.interfaceRevisionId, required), }, rule.id, ); } catch (error) { if (error instanceof GenericTypeError && ["unsatisfied-bound", "non-storable-argument"].includes(error.code)) continue; throw error; } const applicationKey = JSON.stringify([rule.id, fact.atomId, fact.interfaceRevisionId]); if (applications.has(applicationKey)) continue; if (applications.size >= 10000) throw new GenericTypeError( "rule-expansion-limit", rule.id, "Conformance rule expansion exceeds 10000 applications", ); const targetId = substitution.application(rule.target); const id = capabilityId.conformance( `conformance-rule:sha256:${createHash("sha256") .update( JSON.stringify(["quixos-conformance-rule-v1", rule.id, fact.atomId, fact.interfaceRevisionId, targetId]), ) .digest("hex")}`, ); applications.set(applicationKey, { rule, atomId: fact.atomId, sourceId: fact.interfaceRevisionId, targetId, bindings, id, }); const targetKey = key(fact.atomId, targetId); if (!facts.has(targetKey)) { facts.set(targetKey, { atomId: fact.atomId, interfaceRevisionId: targetId }); changed = true; } } } } return [...applications.values()]; }