|
| 1 | +import { describe, it } from "vitest"; |
| 2 | +import fc from "fast-check"; |
| 3 | +import { type Atom, sym, variable, expr, gint, atomEq, atomVars } from "./atom"; |
| 4 | +import { parse, format } from "./parser"; |
| 5 | +import { standardTokenizer } from "./runner"; |
| 6 | +import { alphaEq } from "./alpha"; |
| 7 | +import { matchAtoms } from "./match"; |
| 8 | +import { instantiate } from "./instantiate"; |
| 9 | + |
| 10 | +// A safe symbol name: starts with a letter, not "True"/"False", not all-digits (those tokenize). |
| 11 | +const name = fc |
| 12 | + .stringMatching(/^[a-z][a-z0-9-]{0,6}$/) |
| 13 | + .filter((s) => s !== "True" && s !== "False"); |
| 14 | + |
| 15 | +const atomArb: fc.Arbitrary<Atom> = fc.letrec<{ atom: Atom }>((tie) => ({ |
| 16 | + atom: fc.oneof( |
| 17 | + { depthSize: "small", withCrossShrink: true }, |
| 18 | + name.map(sym), |
| 19 | + name.map(variable), |
| 20 | + fc.integer({ min: -999, max: 999 }).map(gint), |
| 21 | + fc.array(tie("atom"), { maxLength: 3 }).map((xs) => expr(xs)), |
| 22 | + ), |
| 23 | +})).atom; |
| 24 | + |
| 25 | +const tk = standardTokenizer(); |
| 26 | + |
| 27 | +describe("properties (fast-check)", () => { |
| 28 | + it("parser round-trip: parse(format(a)) ≡ a", () => { |
| 29 | + fc.assert( |
| 30 | + fc.property(atomArb, (a) => { |
| 31 | + const r = parse(format(a), tk); |
| 32 | + return r !== undefined && atomEq(r, a); |
| 33 | + }), |
| 34 | + ); |
| 35 | + }); |
| 36 | + |
| 37 | + it("alphaEq is reflexive and symmetric", () => { |
| 38 | + fc.assert(fc.property(atomArb, (a) => alphaEq(a, a))); |
| 39 | + fc.assert(fc.property(atomArb, atomArb, (a, b) => alphaEq(a, b) === alphaEq(b, a))); |
| 40 | + }); |
| 41 | + |
| 42 | + it("matcher soundness: a binding set instantiates the pattern to the ground target", () => { |
| 43 | + fc.assert( |
| 44 | + fc.property(atomArb, atomArb, (pattern, ground0) => { |
| 45 | + // Make the target ground by substituting any vars with a constant. |
| 46 | + const subst = (x: Atom): Atom => |
| 47 | + x.kind === "var" ? sym("k") : x.kind === "expr" ? expr(x.items.map(subst)) : x; |
| 48 | + const ground = subst(ground0); |
| 49 | + for (const b of matchAtoms(pattern, ground)) { |
| 50 | + if (!atomEq(instantiate(b, pattern), ground)) return false; |
| 51 | + } |
| 52 | + return true; |
| 53 | + }), |
| 54 | + ); |
| 55 | + }); |
| 56 | + |
| 57 | + it("a matched pattern with no extra vars resolves all its variables", () => { |
| 58 | + fc.assert( |
| 59 | + fc.property(atomArb, (ground0) => { |
| 60 | + const subst = (x: Atom): Atom => |
| 61 | + x.kind === "var" ? sym("k") : x.kind === "expr" ? expr(x.items.map(subst)) : x; |
| 62 | + const ground = subst(ground0); |
| 63 | + // pattern = ground with one leaf turned into a fresh var still matches and resolves. |
| 64 | + const pat = expr([variable("p"), ground]); |
| 65 | + const tgt = expr([sym("anchor"), ground]); |
| 66 | + const res = matchAtoms(pat, tgt); |
| 67 | + if (res.length === 0) return true; |
| 68 | + return atomVars(instantiate(res[0]!, pat)).length === 0; |
| 69 | + }), |
| 70 | + ); |
| 71 | + }); |
| 72 | +}); |
0 commit comments