code wiki / _hdl_build / nx_eqsat_congruence_test.nx
nx_eqsat_congruence_test.nx source
↩ module page · 205 lines · 11779 B
1// nx_eqsat_congruence_test.nx -- STRUCTURAL-WITNESS GATE for the egg-style
2// deferred CONGRUENCE CLOSURE newly integrated into nx_eqsat.nx.
3//
4// THE PROOF (the new organ actually MEETS egg's congruence rebuild):
5// Congruence axiom of equality: if a == b (same canonical e-class) then for any
6// deterministic function f, f(...a...) == f(...b...). The pre-MEET engine had a
7// real hashcons-by-linear-scan + union-find, but NO rebuild: after a union it
8// never re-merged congruent PARENTS, so f(a) and f(b) stayed in distinct classes.
9// This gate builds exactly that witness and proves the rebuild closes it.
10//
11// CASE 1 (POSITIVE -- congruence FIRES, cited + logged sound):
12// Build a, b as two DISTINCT vars; build g_a = NEG(a) and g_b = NEG(b) (so the
13// two parent e-nodes are genuinely different nodes). Pre-merge a~b via the SOUND
14// mul_pow2 path is overkill; instead we drive the canonical congruence trigger
15// through the real saturator: a=(mul x 8), b=(shl x 3) -> mul_pow2 merges them,
16// then NEG(a) and NEG(b) MUST become congruent via the rebuild (same op NEG,
17// same payload, canonically-equal child). Assert:
18// (1a) find(NEG a) == find(NEG b) -- congruence detected
19// (1b) the prov log contains id 8 (CONGRUENCE) -- the merge was CITED
20// (1c) every logged id is in the proven-sound allow-list (0..8, !=NONE-as-bad)
21// (1d) RULE_NONE (id 0) count == 0 -- no back-door (2-arg) merge
22//
23// CASE 2 (NEGATIVE control -- WITHOUT rebuild the gap is REAL):
24// Same graph, but provenance/meet OFF on a plain engine run (rebuild disabled):
25// mul_pow2 still merges (mul x 8)~(shl x 3), but the parents NEG(mul8)/NEG(shl3)
26// are NOT merged -> find(NEG a) != find(NEG b). This proves the rebuild is the
27// load-bearing organ (a passing CASE 1 is not vacuous).
28//
29// CASE 3 (NEGATIVE -- non-congruent nodes are NOT merged):
30// NEG(a) vs NOT(a): same single child class but DIFFERENT op -> distinct
31// fingerprints -> never congruence-merged. Asserts the fingerprint folds op
32// (a fingerprint that ignored op would wrongly merge them and cite CONGRUENCE).
33//
34// KNOWN ANSWER (FAIL LOUD), one line:
35// "<case1_member> <cong_logged> <none_count> <case2_member> <case3_member>"
36// = "1 1 0 0 0 " exit 0 iff every assertion holds.
37//
38// SOVEREIGN: no SMT, no .sh; runs on the pinned NishiLang compiler. The exit code
39// IS the verdict. license_tier: ORIGINAL
40
41import "nx_gate_verdict.nx"
42import "nx_eqsat.nx"
43
44func _emit_num(v: i64) -> i64 {
45 let b: *u8 = sys_mmap(28); var n: i64 = v; if n < 0 { n = 0 - n }
46 let t2: *u8 = sys_mmap(28); var t: i64 = 0
47 if n == 0 { t2[0] = 48; t = 1 }
48 while n > 0 { t2[t] = 48 + (n % 10); n = n / 10; t = t + 1 }
49 var i: i64 = 0; while i < t { b[i] = t2[t - 1 - i]; i = i + 1 }
50 b[t] = 32; sys_write(1, b, t + 1); return 0
51}
52func _nl() -> i64 { let z: *u8 = sys_mmap(2); z[0] = 10; sys_write(1, z, 1); return 0 }
53
54// Count logged ids equal to `want`; also assert every logged id is allow-listed.
55// Returns (count of `want`); writes 1 to bad_out[0] if any id is NOT in 0..RULE_N
56// or is the NONE sentinel used as a non-sound id (defense vs RULE_NONE bypass).
57func _scan_log(g: *NxEGraph, want: i64, none_out: *i64, anybad_out: *i64) -> i64 {
58 var cnt_want: i64 = 0
59 var cnt_none: i64 = 0
60 var anybad: i64 = 0
61 var i: i64 = 0
62 while i < g.n_prov {
63 let id: i64 = g.prov[i]
64 if id == want { cnt_want = cnt_want + 1 }
65 if id == NX_EQSAT_RULE_NONE { cnt_none = cnt_none + 1 }
66 // allow-listed sound ids: the 7 identity rules + CONGRUENCE (NOT NONE).
67 var ok: i64 = 0
68 if id == NX_EQSAT_RULE_ADD_ZERO { ok = 1 }
69 if id == NX_EQSAT_RULE_SUB_SELF { ok = 1 }
70 if id == NX_EQSAT_RULE_ADD_SELF { ok = 1 }
71 if id == NX_EQSAT_RULE_AND_SELF { ok = 1 }
72 if id == NX_EQSAT_RULE_OR_ZERO { ok = 1 }
73 if id == NX_EQSAT_RULE_MUL_ONE { ok = 1 }
74 if id == NX_EQSAT_RULE_MUL_POW2 { ok = 1 }
75 if id == NX_EQSAT_RULE_CONGRUENCE { ok = 1 }
76 if ok != 1 { anybad = 1 }
77 i = i + 1
78 }
79 none_out[0] = cnt_none
80 anybad_out[0] = anybad
81 return cnt_want
82}
83
84func main() -> i64 {
85 let cap_nodes: i64 = 256
86 let cap_cls: i64 = 256
87
88 // MIGRATED OFF A HAND-ROLLED VERDICT 2026-08-14, and NEVER COMPILED BEFORE THAT DAY: nx_eqsat.nx
89 // itself did not compile (a module const was read 470 lines above its declaration, which nx_cc
90 // refuses because the reader would silently see 0), so this proof and every sibling in the eqsat
91 // family had sat on disk unbuilt. The teeth below were already excellent -- two real negative
92 // controls -- they simply had no way to say so to anything outside the process.
93 gv_head("nx_eqsat congruence-closure gate -- deferred rebuild merges congruent parents, and only congruent ones" as *u8)
94 let ctr: *i64 = gv_ctr()
95
96 // ===================== CASE 1: POSITIVE (congruence fires) ================
97 let nodes: *NxENode = sys_mmap(cap_nodes * 64) as *NxENode
98 let classes: *NxEClass = sys_mmap(cap_cls * 32) as *NxEClass
99 let g: *NxEGraph = sys_mmap(NX_EQSAT_GRAPH_BYTES) as *NxEGraph
100 if nx_eqsat_init(g, nodes, cap_nodes, classes, cap_cls) != NX_EQSAT_OK { sys_exit(10); return 10 }
101
102 // MEET scratch (caller-owned). hashcons sized power-of-2 with headroom.
103 let hc: *HashMap = nx_hmap_alloc(256)
104 if (hc as i64) == 0 { sys_exit(11); return 11 }
105 let par_node: *i64 = sys_mmap(1024 * 8) as *i64
106 let par_cls: *i64 = sys_mmap(1024 * 8) as *i64
107 let worklist: *i64 = sys_mmap(1024 * 8) as *i64
108 if nx_eqsat_enable_meet(g, hc, par_node, par_cls, 1024, worklist, 1024) != NX_EQSAT_OK { sys_exit(12); return 12 }
109
110 // provenance ON so we can certify the congruence merge was CITED + sound.
111 let plog: *i64 = sys_mmap(256 * 8) as *i64
112 if nx_eqsat_enable_prov(g, plog, 256) != NX_EQSAT_OK { sys_exit(13); return 13 }
113
114 let x: i64 = nx_eqsat_add_var(g, 0)
115 let c8: i64 = nx_eqsat_add_const(g, 8)
116 let c3: i64 = nx_eqsat_add_const(g, 3)
117 let mul8: i64 = nx_eqsat_add_binary(g, NX_EQ_OP_MUL, x, c8) // a
118 let shl3: i64 = nx_eqsat_add_binary(g, NX_EQ_OP_SHL, x, c3) // b (becomes ~ a)
119 // PARENTS: f(a)=NEG(mul8), f(b)=NEG(shl3) -- distinct e-nodes, same op NEG.
120 let neg_a: i64 = nx_eqsat_add_unary(g, NX_EQ_OP_NEG, mul8)
121 let neg_b: i64 = nx_eqsat_add_unary(g, NX_EQ_OP_NEG, shl3)
122
123 // Pre-state: parents must be in DIFFERENT classes (no congruence yet). THIS IS THE ANTI-VACUITY
124 // TOOTH -- if the parents were already merged before saturation, CASE 1 would prove nothing at all.
125 var pre_distinct: i64 = 1
126 if nx_eqsat_find(g, neg_a) == nx_eqsat_find(g, neg_b) { pre_distinct = 0 }
127
128 // Run the REAL saturator: mul_pow2 merges mul8~shl3; the deferred rebuild then
129 // re-fingerprints the NEG parents -> both canonicalize to NEG(same class) ->
130 // congruence-merges them, citing NX_EQSAT_RULE_CONGRUENCE.
131 let sat: i64 = nx_eqsat_saturate(g, 32)
132 var reached_fixpoint: i64 = 0
133 if sat == NX_EQSAT_SATURATED { reached_fixpoint = 1 }
134
135 var case1_member: i64 = 0
136 if nx_eqsat_find(g, neg_a) == nx_eqsat_find(g, neg_b) { case1_member = 1 }
137
138 let none_out: *i64 = sys_mmap(8) as *i64
139 let bad_out: *i64 = sys_mmap(8) as *i64
140 let cong_count: i64 = _scan_log(g, NX_EQSAT_RULE_CONGRUENCE, none_out, bad_out)
141 var cong_logged: i64 = 0
142 if cong_count >= 1 { cong_logged = 1 }
143 let none_count: i64 = none_out[0]
144 let any_bad: i64 = bad_out[0]
145
146 // ===================== CASE 2: NEGATIVE control (no rebuild) ==============
147 // Same graph, plain engine (meet OFF) -> mul_pow2 still fires but parents NOT
148 // re-merged: find(NEG a) != find(NEG b). Proves the rebuild is load-bearing.
149 let nodes2: *NxENode = sys_mmap(cap_nodes * 64) as *NxENode
150 let classes2: *NxEClass = sys_mmap(cap_cls * 32) as *NxEClass
151 let g2: *NxEGraph = sys_mmap(NX_EQSAT_GRAPH_BYTES) as *NxEGraph
152 if nx_eqsat_init(g2, nodes2, cap_nodes, classes2, cap_cls) != NX_EQSAT_OK { sys_exit(20); return 20 }
153 let x2: i64 = nx_eqsat_add_var(g2, 0)
154 let c8b: i64 = nx_eqsat_add_const(g2, 8)
155 let c3b: i64 = nx_eqsat_add_const(g2, 3)
156 let mul8b: i64 = nx_eqsat_add_binary(g2, NX_EQ_OP_MUL, x2, c8b)
157 let shl3b: i64 = nx_eqsat_add_binary(g2, NX_EQ_OP_SHL, x2, c3b)
158 let neg_a2: i64 = nx_eqsat_add_unary(g2, NX_EQ_OP_NEG, mul8b)
159 let neg_b2: i64 = nx_eqsat_add_unary(g2, NX_EQ_OP_NEG, shl3b)
160 nx_eqsat_saturate(g2, 32)
161 // sanity: the children DID merge (mul_pow2), so the gap is purely congruence.
162 var children_merged_without_meet: i64 = 0
163 if nx_eqsat_find(g2, mul8b) == nx_eqsat_find(g2, shl3b) { children_merged_without_meet = 1 }
164 var case2_member: i64 = 0
165 if nx_eqsat_find(g2, neg_a2) == nx_eqsat_find(g2, neg_b2) { case2_member = 1 }
166
167 // ===================== CASE 3: NEGATIVE (non-congruent: diff op) ==========
168 // NEG(a) vs NOT(a): same child class, DIFFERENT op -> distinct fingerprints ->
169 // never merged. Proves the fingerprint folds op (no false CONGRUENCE merge).
170 let nodes3: *NxENode = sys_mmap(cap_nodes * 64) as *NxENode
171 let classes3: *NxEClass = sys_mmap(cap_cls * 32) as *NxEClass
172 let g3: *NxEGraph = sys_mmap(NX_EQSAT_GRAPH_BYTES) as *NxEGraph
173 if nx_eqsat_init(g3, nodes3, cap_nodes, classes3, cap_cls) != NX_EQSAT_OK { sys_exit(30); return 30 }
174 let hc3: *HashMap = nx_hmap_alloc(256)
175 let pn3: *i64 = sys_mmap(1024 * 8) as *i64
176 let pc3: *i64 = sys_mmap(1024 * 8) as *i64
177 let wl3: *i64 = sys_mmap(1024 * 8) as *i64
178 if nx_eqsat_enable_meet(g3, hc3, pn3, pc3, 1024, wl3, 1024) != NX_EQSAT_OK { sys_exit(31); return 31 }
179 let x3: i64 = nx_eqsat_add_var(g3, 0)
180 let neg3: i64 = nx_eqsat_add_unary(g3, NX_EQ_OP_NEG, x3)
181 let not3: i64 = nx_eqsat_add_unary(g3, NX_EQ_OP_NOT, x3)
182 nx_eqsat_saturate(g3, 32)
183 var case3_member: i64 = 0
184 if nx_eqsat_find(g3, neg3) == nx_eqsat_find(g3, not3) { case3_member = 1 }
185
186 // ===================== known answer + FAIL-LOUD ==========================
187 _emit_num(case1_member); _emit_num(cong_logged); _emit_num(none_count);
188 _emit_num(case2_member); _emit_num(case3_member); _nl()
189
190 gv_check("pre-state: the two NEG parents start in DIFFERENT classes" as *u8, pre_distinct == 1, ctr)
191 gv_check("saturation reached a fixpoint rather than hitting the iteration bound" as *u8, reached_fixpoint == 1, ctr)
192 gv_check("congruence DETECTED: after the deferred rebuild f(a) and f(b) share a class" as *u8, case1_member == 1, ctr)
193 gv_check("the congruence merge was CITED in the provenance log" as *u8, cong_logged == 1, ctr)
194 gv_check("every logged merge cites an allow-listed rule id" as *u8, any_bad == 0, ctr)
195 gv_check("no back-door RULE_NONE merge slipped in uncited" as *u8, none_count == 0, ctr)
196 gv_check("the provenance log is complete, not overflowed (fail-closed)" as *u8, g.prov_overflow == 0, ctr)
197 // THE TWO CONTROLS THAT MAKE THE POSITIVE MEAN SOMETHING.
198 // Without them, an engine that merged EVERYTHING would score full marks above.
199 gv_check("control: the children DO merge without meet, so the gap is purely congruence" as *u8, children_merged_without_meet == 1, ctr)
200 gv_check("neg-control-without-the-rebuild-the-parents-are-NOT-merged" as *u8, case2_member == 0, ctr)
201 gv_check("neg-control-same-child-DIFFERENT-op-NEG-vs-NOT-is-never-merged" as *u8, case3_member == 0, ctr)
202
203 return gv_verdict("nx_eqsat_congruence_test" as *u8, ctr,
204 "egg-style deferred congruence closure: cited, fail-closed provenance, with controls proving the rebuild is load-bearing and the fingerprint folds the operator" as *u8)
205}