code wiki / _hdl_build / nx_eqsat_dsl_parity_test.nx
nx_eqsat_dsl_parity_test.nx source
↩ module page · 301 lines · 15359 B
1// nx_eqsat_dsl_parity_test.nx -- the 24th GATE: proves the egg-style DATA-driven
2// rule DSL + general e-matcher (nx_eqsat_apply_dsl_table) is BEHAVIORALLY-IDENTICAL
3// to the 7 hand-coded inline rules, AND proves ONE genuinely-new sound rule
4// ((xor x x)==0) is admitted-only-after-proof, applied via the DSL, and CERTIFIES
5// through membership-as-proof -- with a load-bearing NEGATIVE control showing an
6// UNPROVEN/un-admitted rule is REFUSED.
7//
8// WHY behavioral-parity (not byte-identity) for the DSL path: the inline saturate
9// runs verbatim whenever g.dsl==null (byte-identity by construction, proven by the
10// four frozen organs staying 1:1). This gate proves the OPT-IN DSL path produces
11// the SAME observable result -- same canonical partition, same extracted op+cost,
12// same provenance multiset, same saturate return code -- over multiple graphs, so
13// the data table genuinely DRIVES saturation (it is not a no-op shim).
14//
15// STRUCTURE (each step self-asserts; mismatch => sys_exit(nonzero)):
16// STEP 1 PARITY over eqsat_test Case 1 (add (mul x x) 0): inline vs DSL agree on
17// find-partition + recompute_best op/cost + provenance multiset + sat rc.
18// STEP 2 PARITY over eqsat_test Case 2 (mul x 8 -> shl x 3): same checks; this
19// exercises the mul_pow2 side-condition + real-merge counting AS DATA.
20// STEP 3 PARITY over a RULE-DENSE graph touching all 7 rules at once.
21// STEP 4 NEW RULE soundness: run the W=8 masked exhaustive two-witness battery
22// for xor_self; assert passed==total==256 (this JUSTIFIES the allow-list
23// arm). Without this passing, mp_is_sound_rule(9) must not be trusted.
24// STEP 5 NEW RULE applied via DSL + CERTIFIED: build (xor v v), enable prov+DSL
25// with the 8-row table (7 builtins + xor_self), saturate; the e-matcher
26// merges the xor-class with const-0 citing id 9; mp_certify_live==1.
27// STEP 6 NEGATIVE CONTROL (load-bearing): a graph whose only merge cites an
28// UN-admitted rule id must REFUSE. We drive the certifier's log scan with
29// a hand-forged log containing an un-admitted id (200) and assert
30// mp_log_all_sound==0 -> certificate refused; AND assert the engine's
31// xor_self merge, if its id were NOT in the allow-list, would refute
32// (mp_is_sound_rule on a bogus id == 0). Proves the gate is load-bearing.
33//
34// KNOWN ANSWER (FAIL LOUD), one line:
35// "<p1> <p2> <p3> <xor_passed> <xor_total> <xor_cert> <neg_refused>"
36// = "1 1 1 256 256 1 1 " exit 0 iff every assertion holds.
37//
38// SOVEREIGN: no SMT, no .sh; runs on the pinned NishiLang compiler. license_tier: ORIGINAL
39
40import "nx_eqsat_membership_proof.nx"
41
42func _emit_num(v: i64) -> i64 {
43 let b: *u8 = sys_mmap(28); var n: i64 = v; if n < 0 { n = 0 - n }
44 let t2: *u8 = sys_mmap(28); var t: i64 = 0
45 if n == 0 { t2[0] = 48; t = 1 }
46 while n > 0 { t2[t] = 48 + (n % 10); n = n / 10; t = t + 1 }
47 var i: i64 = 0; while i < t { b[i] = t2[t - 1 - i]; i = i + 1 }
48 b[t] = 32; sys_write(1, b, t + 1); return 0
49}
50func _nl() -> i64 { let z: *u8 = sys_mmap(2); z[0] = 10; sys_write(1, z, 1); return 0 }
51
52// Per-rule-id histogram of a provenance log (ids 0..MP_RULE_N-1). Used to compare
53// the inline vs DSL logged-rule-id MULTISET exactly (order-independent equality).
54func _log_hist(g: *NxEGraph, hist: *i64) -> i64 {
55 var i: i64 = 0
56 while i < NX_EQSAT_RULE_N { hist[i] = 0; i = i + 1 }
57 var j: i64 = 0
58 while j < g.n_prov {
59 let id: i64 = g.prov[j]
60 if id >= 0 { if id < NX_EQSAT_RULE_N { hist[id] = hist[id] + 1 } }
61 j = j + 1
62 }
63 return g.n_prov
64}
65
66func _hist_eq(a: *i64, b: *i64) -> i64 {
67 var i: i64 = 0
68 while i < NX_EQSAT_RULE_N {
69 if a[i] != b[i] { return 0 }
70 i = i + 1
71 }
72 return 1
73}
74
75// Fresh e-graph factory (caller owns scratch via mmap). Returns the graph ptr.
76func _mk_graph(cap_nodes: i64, cap_cls: i64) -> *NxEGraph {
77 let nodes: *NxENode = sys_mmap(cap_nodes * 64) as *NxENode
78 let classes: *NxEClass = sys_mmap(cap_cls * 32) as *NxEClass
79 let g: *NxEGraph = sys_mmap(256) as *NxEGraph
80 nx_eqsat_init(g, nodes, cap_nodes, classes, cap_cls)
81 return g
82}
83
84// Build the 7-row builtin table + (optionally) append xor_self as an 8th row.
85func _mk_table(with_xor: i64) -> *NxDslRule {
86 let table: *NxDslRule = sys_mmap(16 * 96) as *NxDslRule // 16 rows of >=88B
87 let n: i64 = nx_eqsat_builtin_dsl_table(table)
88 if with_xor == 1 {
89 // ONE new data row: (xor x x)==0. zero new matcher control flow.
90 // CNT_REAL_MERGE so saturation converges (the union is idempotent).
91 nx_eqsat_dsl_set_row(table, n, NX_EQSAT_RULE_XOR_SELF, NX_EQ_OP_XOR,
92 DSL_VAR, 0, DSL_SAME_AS_A, 0,
93 RHS_CONST, 0, SC_NONE, CNT_REAL_MERGE, 0)
94 }
95 return table
96}
97
98// Run ONE parity comparison: build the SAME graph twice (inline path then DSL
99// path), saturate both with provenance ON, and assert identical observable result.
100// graph_kind selects which input to build. Returns 1 iff every facet agrees.
101func _parity(graph_kind: i64) -> i64 {
102 let cap_nodes: i64 = 256
103 let cap_cls: i64 = 256
104
105 // ---------- INLINE path ----------
106 let gi: *NxEGraph = _mk_graph(cap_nodes, cap_cls)
107 let plogi: *i64 = sys_mmap(256 * 8) as *i64
108 nx_eqsat_enable_prov(gi, plogi, 256)
109 // roots of interest (filled per graph kind)
110 var r0i: i64 = 0 - 1
111 var r1i: i64 = 0 - 1
112 if graph_kind == 1 {
113 // Case 1: (add (mul x x) 0)
114 let x: i64 = nx_eqsat_add_var(gi, 0)
115 let mxx: i64 = nx_eqsat_add_binary(gi, NX_EQ_OP_MUL, x, x)
116 let z: i64 = nx_eqsat_add_const(gi, 0)
117 r0i = nx_eqsat_add_binary(gi, NX_EQ_OP_ADD, mxx, z)
118 r1i = mxx
119 }
120 if graph_kind == 2 {
121 // Case 2: (mul x 8)
122 let x: i64 = nx_eqsat_add_var(gi, 0)
123 let c8: i64 = nx_eqsat_add_const(gi, 8)
124 r0i = nx_eqsat_add_binary(gi, NX_EQ_OP_MUL, x, c8)
125 r1i = r0i
126 }
127 if graph_kind == 3 {
128 // rule-dense: (add (sub y y) 0), (add w w), (and z z), (or u 0), (mul v 1),
129 // (mul v 16) -- touches add_zero, sub_self, add_self, and_self, or_zero,
130 // mul_one, mul_pow2 in ONE graph.
131 let y: i64 = nx_eqsat_add_var(gi, 1)
132 let suby: i64 = nx_eqsat_add_binary(gi, NX_EQ_OP_SUB, y, y)
133 let z0: i64 = nx_eqsat_add_const(gi, 0)
134 r0i = nx_eqsat_add_binary(gi, NX_EQ_OP_ADD, suby, z0) // add_zero + sub_self
135 let w: i64 = nx_eqsat_add_var(gi, 2)
136 r1i = nx_eqsat_add_binary(gi, NX_EQ_OP_ADD, w, w) // add_self
137 let zz: i64 = nx_eqsat_add_var(gi, 3)
138 nx_eqsat_add_binary(gi, NX_EQ_OP_AND, zz, zz) // and_self
139 let u: i64 = nx_eqsat_add_var(gi, 4)
140 let zc: i64 = nx_eqsat_add_const(gi, 0)
141 nx_eqsat_add_binary(gi, NX_EQ_OP_OR, u, zc) // or_zero
142 let v: i64 = nx_eqsat_add_var(gi, 5)
143 let one: i64 = nx_eqsat_add_const(gi, 1)
144 nx_eqsat_add_binary(gi, NX_EQ_OP_MUL, v, one) // mul_one
145 let c16: i64 = nx_eqsat_add_const(gi, 16)
146 nx_eqsat_add_binary(gi, NX_EQ_OP_MUL, v, c16) // mul_pow2 (k=4)
147 }
148 let sati: i64 = nx_eqsat_saturate(gi, 16)
149 nx_eqsat_recompute_best(gi)
150 let bni0: i64 = nx_eqsat_extract_best_node(gi, r0i)
151 let opi0: i64 = gi.nodes[bni0].op
152 let costi0: i64 = nx_eqsat_best_cost(gi, r0i)
153 let memi: i64 = if nx_eqsat_find(gi, r0i) == nx_eqsat_find(gi, r1i) then 1 else 0
154 let histi: *i64 = sys_mmap(NX_EQSAT_RULE_N * 8) as *i64
155 _log_hist(gi, histi)
156 let nprovi: i64 = gi.n_prov
157
158 // ---------- DSL path (SAME graph, opt-in table) ----------
159 let gd: *NxEGraph = _mk_graph(cap_nodes, cap_cls)
160 let plogd: *i64 = sys_mmap(256 * 8) as *i64
161 nx_eqsat_enable_prov(gd, plogd, 256)
162 let table: *NxDslRule = _mk_table(0) // 7 builtins only (parity set)
163 nx_eqsat_enable_dsl(gd, table, 7, 16)
164 var r0d: i64 = 0 - 1
165 var r1d: i64 = 0 - 1
166 if graph_kind == 1 {
167 let x: i64 = nx_eqsat_add_var(gd, 0)
168 let mxx: i64 = nx_eqsat_add_binary(gd, NX_EQ_OP_MUL, x, x)
169 let z: i64 = nx_eqsat_add_const(gd, 0)
170 r0d = nx_eqsat_add_binary(gd, NX_EQ_OP_ADD, mxx, z)
171 r1d = mxx
172 }
173 if graph_kind == 2 {
174 let x: i64 = nx_eqsat_add_var(gd, 0)
175 let c8: i64 = nx_eqsat_add_const(gd, 8)
176 r0d = nx_eqsat_add_binary(gd, NX_EQ_OP_MUL, x, c8)
177 r1d = r0d
178 }
179 if graph_kind == 3 {
180 let y: i64 = nx_eqsat_add_var(gd, 1)
181 let suby: i64 = nx_eqsat_add_binary(gd, NX_EQ_OP_SUB, y, y)
182 let z0: i64 = nx_eqsat_add_const(gd, 0)
183 r0d = nx_eqsat_add_binary(gd, NX_EQ_OP_ADD, suby, z0)
184 let w: i64 = nx_eqsat_add_var(gd, 2)
185 r1d = nx_eqsat_add_binary(gd, NX_EQ_OP_ADD, w, w)
186 let zz: i64 = nx_eqsat_add_var(gd, 3)
187 nx_eqsat_add_binary(gd, NX_EQ_OP_AND, zz, zz)
188 let u: i64 = nx_eqsat_add_var(gd, 4)
189 let zc: i64 = nx_eqsat_add_const(gd, 0)
190 nx_eqsat_add_binary(gd, NX_EQ_OP_OR, u, zc)
191 let v: i64 = nx_eqsat_add_var(gd, 5)
192 let one: i64 = nx_eqsat_add_const(gd, 1)
193 nx_eqsat_add_binary(gd, NX_EQ_OP_MUL, v, one)
194 let c16: i64 = nx_eqsat_add_const(gd, 16)
195 nx_eqsat_add_binary(gd, NX_EQ_OP_MUL, v, c16)
196 }
197 let satd: i64 = nx_eqsat_saturate(gd, 16)
198 nx_eqsat_recompute_best(gd)
199 let bnd0: i64 = nx_eqsat_extract_best_node(gd, r0d)
200 let opd0: i64 = gd.nodes[bnd0].op
201 let costd0: i64 = nx_eqsat_best_cost(gd, r0d)
202 let memd: i64 = if nx_eqsat_find(gd, r0d) == nx_eqsat_find(gd, r1d) then 1 else 0
203 let histd: *i64 = sys_mmap(NX_EQSAT_RULE_N * 8) as *i64
204 _log_hist(gd, histd)
205 let nprovd: i64 = gd.n_prov
206
207 // ---------- compare every observable facet ----------
208 if sati != satd { return 0 } // same saturate return code
209 if opi0 != opd0 { return 0 } // same extracted root op
210 if costi0 != costd0 { return 0 } // same extracted cost
211 if memi != memd { return 0 } // same canonical partition (root pair)
212 if nprovi != nprovd { return 0 } // same number of logged merges
213 if _hist_eq(histi, histd) != 1 { return 0 } // same logged-rule-id MULTISET
214 return 1
215}
216
217func main() -> i64 {
218 // shared gsim scratch for the soundness battery
219 let vals: *i64 = sys_mmap(64 * 8) as *i64
220 let cells: *NxGsimCell = sys_mmap(64 * 48) as *NxGsimCell
221 let gs: *NxGsim = sys_mmap(64) as *NxGsim
222
223 // STEP 0: lockstep (engine ids == membership ids, incl the new xor_self id 9).
224 if mp_rule_ids_lockstep() != 1 { sys_exit(70); return 70 }
225
226 // ===================== STEP 1-3: behavioral PARITY ========================
227 let p1: i64 = _parity(1)
228 let p2: i64 = _parity(2)
229 let p3: i64 = _parity(3)
230
231 // ===================== STEP 4: NEW RULE soundness battery =================
232 // The W=8 masked exhaustive two-witness battery for xor_self MUST be unanimous;
233 // this is what JUSTIFIES admitting MP_RULE_XOR_SELF to the proven-sound set.
234 let t: *NxTriTally = sys_mmap(64) as *NxTriTally
235 nx_tri_tally_init(t)
236 let xor_total: i64 = mp_battery_xor_self(gs, vals, cells, t)
237 let xor_passed: i64 = t.passed
238
239 // ===================== STEP 5: NEW RULE applied + CERTIFIED ===============
240 // Build (xor v v), enable prov + DSL with the 8-row table, run the REAL
241 // saturator. The e-matcher merges the xor-class with const-0 citing id 9.
242 let cap_nodes: i64 = 64
243 let cap_cls: i64 = 64
244 let g5: *NxEGraph = _mk_graph(cap_nodes, cap_cls)
245 let plog5: *i64 = sys_mmap(64 * 8) as *i64
246 if nx_eqsat_enable_prov(g5, plog5, 64) != NX_EQSAT_OK { sys_exit(50); return 50 }
247 let table8: *NxDslRule = _mk_table(1) // 7 builtins + xor_self
248 if nx_eqsat_enable_dsl(g5, table8, 8, 16) != NX_EQSAT_OK { sys_exit(51); return 51 }
249 let v: i64 = nx_eqsat_add_var(g5, 0)
250 let xorvv: i64 = nx_eqsat_add_binary(g5, NX_EQ_OP_XOR, v, v) // (xor v v)
251 let zero5: i64 = nx_eqsat_add_const(g5, 0) // 0
252 // pre-merge: distinct classes
253 if nx_eqsat_find(g5, xorvv) == nx_eqsat_find(g5, zero5) { sys_exit(52); return 52 }
254 let sat5: i64 = nx_eqsat_saturate(g5, 16)
255 if sat5 != NX_EQSAT_SATURATED { sys_exit(53); return 53 }
256 // certify (xor v v) == 0 strictly from the engine's log.
257 let cert5: *NxMpCertificate = sys_mmap(64) as *NxMpCertificate
258 let xor_cert: i64 = mp_certify_live(g5, xorvv, zero5, cert5)
259 if cert5.member != 1 { sys_exit(54); return 54 } // xor-class merged with 0
260 if cert5.sound != 1 { sys_exit(55); return 55 } // every logged id sound (incl 9)
261 if xor_cert != 1 { sys_exit(56); return 56 } // CERTIFIED via membership-as-proof
262 if g5.n_prov < 1 { sys_exit(57); return 57 } // a real merge WAS logged
263 if g5.prov_overflow != 0 { sys_exit(58); return 58 }
264 // the logged merge cited id 9 (xor_self), proving the DSL rule fired.
265 var saw_xor: i64 = 0
266 var qi: i64 = 0
267 while qi < g5.n_prov { if g5.prov[qi] == NX_EQSAT_RULE_XOR_SELF { saw_xor = 1 } qi = qi + 1 }
268 if saw_xor != 1 { sys_exit(59); return 59 }
269
270 // ===================== STEP 6: NEGATIVE CONTROL (load-bearing) ============
271 // (6a) An UN-admitted rule id in the log must REFUSE the certificate. Forge a
272 // log on a fresh graph whose only "merge" cites a bogus id (200) and assert the
273 // certifier's log scan refuses it. This is the same poison path RULE_NONE uses.
274 let g6: *NxEGraph = _mk_graph(cap_nodes, cap_cls)
275 let plog6: *i64 = sys_mmap(64 * 8) as *i64
276 nx_eqsat_enable_prov(g6, plog6, 64)
277 // hand-forge ONE un-admitted citation directly into the live log buffer.
278 plog6[0] = 200 // an id NOT in the proven-sound allow-list
279 g6.n_prov = 1
280 let neg_scan: i64 = mp_log_all_sound(g6) // must be 0 (refused)
281 // (6b) prove the allow-list arm is load-bearing: mp_is_sound_rule on a bogus id
282 // is 0, so had xor_self's id NOT been admitted (battery never run), certify would
283 // refute -- the membership check is a real consequence of admission, not free.
284 let bogus_sound: i64 = mp_is_sound_rule(200) // must be 0
285 let xor_admitted: i64 = mp_is_sound_rule(NX_EQSAT_RULE_XOR_SELF) // must be 1 (after battery)
286 var neg_refused: i64 = 0
287 if neg_scan == 0 { if bogus_sound == 0 { if xor_admitted == 1 { neg_refused = 1 } } }
288
289 // ===================== known answer + FAIL-LOUD ==========================
290 _emit_num(p1); _emit_num(p2); _emit_num(p3);
291 _emit_num(xor_passed); _emit_num(xor_total); _emit_num(xor_cert); _emit_num(neg_refused); _nl()
292
293 if p1 != 1 { sys_exit(1); return 1 } // parity Case 1
294 if p2 != 1 { sys_exit(2); return 2 } // parity Case 2
295 if p3 != 1 { sys_exit(3); return 3 } // parity rule-dense
296 if xor_total != 256 { sys_exit(4); return 4 } // 2^W vectors swept
297 if xor_passed != xor_total { sys_exit(5); return 5 } // every vector both-witness pass
298 if xor_cert != 1 { sys_exit(6); return 6 } // new rule CERTIFIED live
299 if neg_refused != 1 { sys_exit(7); return 7 } // negative control: unproven => refused
300 sys_exit(0); return 0
301}