|
| 1 | +// SPDX-FileCopyrightText: 2026 MesTTo |
| 2 | +// |
| 3 | +// SPDX-License-Identifier: MIT |
| 4 | + |
| 5 | +// `instantiate` applies a binding set as a substitution resolved to a fixpoint. A binding set from |
| 6 | +// unification is triangular (a value can mention another still-bound variable), so a one-pass |
| 7 | +// application would leave inner variables unresolved and later lose their constraints. These tests pin |
| 8 | +// the transitive resolution and its termination on binding cycles. |
| 9 | +import { describe, it, expect } from "vitest"; |
| 10 | +import { instantiate } from "./instantiate"; |
| 11 | +import { fromRelations, makeValRel, makeEqRel } from "./bindings"; |
| 12 | +import { sym, variable, expr, type Atom } from "./atom"; |
| 13 | +import { format } from "./parser"; |
| 14 | + |
| 15 | +const f = (a: Atom): string => format(a); |
| 16 | + |
| 17 | +describe("instantiate transitive (fixpoint) resolution", () => { |
| 18 | + it("resolves a variable whose value mentions another bound variable", () => { |
| 19 | + const b = fromRelations([ |
| 20 | + makeValRel("x", expr([sym("f"), variable("y")])), |
| 21 | + makeValRel("y", sym("a")), |
| 22 | + ]); |
| 23 | + // One-pass would leave `$y`: `(out (f $y))`. The fixpoint bakes in `$y := a`. |
| 24 | + expect(f(instantiate(b, expr([sym("out"), variable("x")])))).toBe("(out (f a))"); |
| 25 | + }); |
| 26 | + |
| 27 | + it("resolves regardless of relation order", () => { |
| 28 | + const b = fromRelations([ |
| 29 | + makeValRel("y", sym("a")), |
| 30 | + makeValRel("x", expr([sym("f"), variable("y")])), |
| 31 | + ]); |
| 32 | + expect(f(instantiate(b, expr([sym("out"), variable("x")])))).toBe("(out (f a))"); |
| 33 | + }); |
| 34 | + |
| 35 | + it("resolves through several levels of nested structure", () => { |
| 36 | + const b = fromRelations([ |
| 37 | + makeValRel( |
| 38 | + "a", |
| 39 | + expr([sym("Arr"), variable("p"), expr([sym("Arr"), variable("q"), variable("p")])]), |
| 40 | + ), |
| 41 | + makeValRel("p", sym("P")), |
| 42 | + makeValRel("q", sym("Q")), |
| 43 | + ]); |
| 44 | + expect(f(instantiate(b, expr([sym("t"), variable("a")])))).toBe("(t (Arr P (Arr Q P)))"); |
| 45 | + }); |
| 46 | + |
| 47 | + it("bakes the deep resolution into a rule RHS via the scoping suffix", () => { |
| 48 | + // The reduce path instantiates a rule RHS with a per-application suffix. A value pulled from the |
| 49 | + // binding set already carries final (suffixed) names and must still be resolved transitively. |
| 50 | + const b = fromRelations([ |
| 51 | + makeValRel("b#1", expr([sym("Pair"), variable("a#0")])), |
| 52 | + makeValRel("a#0", expr([sym("S"), variable("p#1")])), |
| 53 | + makeValRel("p#1", sym("Z")), |
| 54 | + ]); |
| 55 | + expect(f(instantiate(b, expr([sym("res"), variable("b")]), "#1"))).toBe("(res (Pair (S Z)))"); |
| 56 | + }); |
| 57 | + |
| 58 | + it("terminates on a direct variable-alias cycle, truncating at the variable", () => { |
| 59 | + const b = fromRelations([makeValRel("x", variable("y")), makeValRel("y", variable("x"))]); |
| 60 | + expect(f(instantiate(b, expr([sym("out"), variable("x")])))).toBe("(out $x)"); |
| 61 | + }); |
| 62 | + |
| 63 | + it("terminates on a structural binding cycle", () => { |
| 64 | + const b = fromRelations([ |
| 65 | + makeValRel("x", expr([sym("f"), variable("y")])), |
| 66 | + makeValRel("y", expr([sym("g"), variable("x")])), |
| 67 | + ]); |
| 68 | + expect(f(instantiate(b, expr([sym("out"), variable("x")])))).toBe("(out (f (g $x)))"); |
| 69 | + }); |
| 70 | + |
| 71 | + it("leaves a genuinely unbound variable free", () => { |
| 72 | + const b = fromRelations([makeValRel("x", expr([sym("S"), variable("y")]))]); |
| 73 | + expect(f(instantiate(b, expr([sym("out"), variable("x")])))).toBe("(out (S $y))"); |
| 74 | + }); |
| 75 | + |
| 76 | + it("ignores eq aliases, resolving only value bindings", () => { |
| 77 | + const b = fromRelations([makeEqRel("x", "y"), makeValRel("y", sym("a"))]); |
| 78 | + // `instantiate` applies value bindings only (eq aliases are dropped), so `$x` stays free. |
| 79 | + expect(f(instantiate(b, expr([sym("out"), variable("x")])))).toBe("(out $x)"); |
| 80 | + }); |
| 81 | +}); |
0 commit comments