code wiki / _hdl_build / nx_eqsat_membership_proof.nx

nx_eqsat_membership_proof.nx source

↩ module page · 623 lines · 31313 B

1// nx_eqsat_membership_proof.nx -- MEMBERSHIP-AS-PROOF CERTIFICATE for the eqsat 2// e-graph (the sovereign analogue of ROVER's "verified rewriting": no external 3// SMT oracle; the rewrite rules' soundness + e-class membership IS the proof). 4// 5// THE CLAIM BEING CERTIFIED: 6// Two expressions a, b are equivalent-for-all-inputs (mod 2^W) iff 7// (1) MEMBERSHIP: nx_eqsat_find(g, a) == nx_eqsat_find(g, b) 8// (they landed in the same canonical e-class after saturation), AND 9// (2) PROVENANCE: every union on the rewrite path that merged them used a 10// rule from the PROVEN-SOUND set. 11// When BOTH hold, a == b for all inputs -- membership is a rewrite-path 12// equivalence PROOF, not a heuristic. 13// 14// LIVE CERTIFICATION (mp_certify_live): the engine (nx_eqsat.nx) now logs the 15// citing rule-id of EVERY real merge at its single union chokepoint 16// (nx_eqsat_union_cited) when provenance is enabled (nx_eqsat_enable_prov). The 17// live certifier runs the REAL nx_eqsat_saturate and checks the GLOBAL property 18// "every logged rule-id is in the proven-sound set" + find(a)==find(b) + the log 19// did not overflow (fail-closed). Because every merge flows through the one 20// function that mutates canon, a merge cannot happen without a rule-id being 21// logged -- so an unlogged/un-cited merge (RULE_NONE) poisons the certificate. 22// The gated recorder (mp_prov_admit_union) is RETAINED for the load-bearing 23// negatives' belt-and-suspenders legs + the k<MP_W boundary side-condition. 24// 25// THE SOUND SET (justified, not asserted): a rule is admitted to the sound set 26// ONLY after its W-bit (mod 2^W) algebraic identity is machine-checked 27// EXHAUSTIVELY at a representative width W=8 -- the same width-independent 28// exhaustive-small-width discipline as nx_alu_divider_proof_test.nx. Every 29// evaluator output is MASKED to W bits before comparison (mask = (1<<W)-1) so we 30// prove the true mod-2^W theorem, NOT a coincidental full-i64 identity (the 31// truncation gap). The oracle is also reduced mod 2^W and is independent of both 32// the MUL and SHL nodes (computed by repeated doubling), so the two netlist legs 33// + oracle are three independent witnesses fed through nx_triangulate. 34// 35// LOAD-BEARING (defense in depth): the certificate must FAIL when 36// (NEG-A) a NON-sound rule is used (it is not in the allow-list AND its W=8 37// battery DISAGREES, so it can never enter the sound set), or 38// (NEG-B) the two endpoints are NOT in the same e-class (membership fails). 39// Both negatives are exercised + asserted in the test (FAIL LOUD known answer). 40// 41// SOVEREIGN: no SMT, no foreign oracle, no .sh -- runs on the pinned NishiLang 42// compiler; the gate-sim (nx_gsim) is the only trusted evaluator, the same organ 43// the divider proof trusts. Width W=8 chosen so both legs enumerate cheaply while 44// the per-rule algebraic argument carries width-independence to W=64 (identity / 45// inverse / per-bit law / k-induction with the k<W side-condition). 46// 47// license_tier: ORIGINAL 48 49import "nx_eqsat.nx" 50import "nx_alu_divider.nx" 51import "nx_triangulate.nx" 52const MP_MAGIC_999999: i64 = 999999 53 54// ===== proof width ========================================================= 55 56const MP_W: i64 = 8 // representative width: sweep x in [0, 2^W) 57 58func mp_mask() -> i64 { return (1 << MP_W) - 1 } // (1<<W)-1 = mod-2^W reducer 59func mp_count() -> i64 { return 1 << MP_W } // 2^W vectors per unary rule 60 61// ===== sound-rule identifiers (the proven-sound ALLOW-LIST) ================ 62// 63// These name the rules whose W-bit identity the battery below proves. A union 64// is admissible-in-a-proof ONLY if it cites one of these ids. mul_pow2 is the 65// rule that merges (mul x 2^k) with (shl x k); it is certifiable IN-LIVE only 66// for k < W (the owed k<W guard) -- the certifier checks that side-condition. 67 68const MP_RULE_NONE: i64 = 0 // sentinel: no/unknown rule -> NOT sound 69const MP_RULE_ADD_ZERO: i64 = 1 70const MP_RULE_SUB_SELF: i64 = 2 71const MP_RULE_ADD_SELF: i64 = 3 72const MP_RULE_AND_SELF: i64 = 4 73const MP_RULE_OR_ZERO: i64 = 5 74const MP_RULE_MUL_ONE: i64 = 6 75const MP_RULE_MUL_POW2: i64 = 7 76// CONGRUENCE: structural law of equality (a==b => f(...a...)==f(...b...)). The 77// engine's deferred rebuild cites NX_EQSAT_RULE_CONGRUENCE for every congruence 78// merge; it is admitted to the proven-sound set here. Its soundness needs NO W=8 79// arithmetic battery (unlike the 7 identity rules) -- it is width-independent and 80// fires ONLY on already-canonically-equal children, so the merged e-nodes denote 81// the SAME function of the SAME values. [CANON: Willsey et al, egg, PLDI 2021.] 82const MP_RULE_CONGRUENCE: i64 = 8 83// XOR_SELF: (xor x x) == 0 -- the FIRST rule expressed PURELY as DSL data in the 84// engine (one NxDslRule row). Admitted to the proven-sound set ONLY after the W=8 85// masked exhaustive two-witness battery (mp_battery_xor_self) is unanimous -- the 86// SAME discipline that justified the original 7. Width-independent (a^a=0 holds per 87// bit at any W), so the W=8 machine-check lifts to W=64 (per-bit-law class). The 88// allow-list arm in mp_is_sound_rule is a CONSEQUENCE of that battery passing, 89// asserted in nx_eqsat_dsl_gate_test.nx before any certification trusts id 9. 90const MP_RULE_XOR_SELF: i64 = 9 91// CONSTFOLD: (op c0 c1 ...) with all-constant children == CONST(eval(op, c..)). 92// The engine fires this as an e-class ANALYSIS at node creation (gated, opt-in) and 93// cites NX_EQSAT_RULE_CONSTFOLD at its union chokepoint. Admitted to the proven- 94// sound set because its soundness is BY EVALUATION: the merge relates (op c0 c1) 95// with CONST(nx_gsim_eval_cell(kind(op), c0, c1)) where nx_gsim_eval_cell is the 96// SAME proven op-semantics oracle this organ already trusts (_mp_eval_op_const, 97// line 130). It is width-independent at W=64 (i64 arithmetic IS mod-2^W) AND the 98// engine's nx_eqsat_op_to_gate_kind admits ONLY ops whose gate cell is the proven 99// op semantics (DIV/REM/SHR/SAR/NEG/POPCNT/SELECT refused), so a CONSTFOLD-cited 100// merge can never relate two unequal constants. No separate W=8 arithmetic battery 101// is needed (parallel to how CONGRUENCE is admitted -- the soundness is the oracle, 102// not an algebraic identity per rule). The const-fold gate (nx_eqsat_constfold_test) 103// runs the positive ((add 2 3)->5 certified) + the negatives BEFORE trusting id 10. 104const MP_RULE_CONSTFOLD: i64 = 10 105// SHIFT_MERGE: (shl (shl x i) j) == (shl x (i+j)) for i,j CONST and i+j<W. The SECOND 106// rule expressed PURELY as DSL data in the engine (shape (d): nested SHL-of-SHL). 107// Admitted to the proven-sound set ONLY after the W=8 masked exhaustive two-witness 108// in-range battery (mp_battery_shift_merge) is unanimous AND the i+j>=W boundary is 109// shown unsound (mp_shiftmerge_boundary_unsound) -- the SAME discipline that justified 110// mul_pow2. Width-independent: (a mod 2^W)*2^j == a*2^j mod 2^W (2^j is a factor), so 111// the W=8 machine-check lifts to W=64 by k-induction on (i+j) with the i+j<W guard. 112const MP_RULE_SHIFT_MERGE: i64 = 11 113const MP_RULE_N: i64 = 12 114 115// An UNSOUND probe rule used ONLY by the load-bearing negative test: it claims 116// (mul x 3) == (shl x 1), which is FALSE (3 is not a power of two). It is NOT in 117// the allow-list and its battery DISAGREES, so it can never be certified. 118const MP_RULE_BOGUS_MUL3: i64 = 100 119 120// LOCKSTEP: the engine (nx_eqsat.nx) is the single source of truth for rule-ids; 121// the live log it writes uses NX_EQSAT_RULE_*. These MP_RULE_* values + the 122// allow-list must agree EXACTLY, or a mislabel could let an engine-cited id slip 123// through the certifier under a different name. Returns 1 iff every id matches. 124func mp_rule_ids_lockstep() -> i64 { 125 if MP_RULE_NONE != NX_EQSAT_RULE_NONE { return 0 } 126 if MP_RULE_ADD_ZERO != NX_EQSAT_RULE_ADD_ZERO { return 0 } 127 if MP_RULE_SUB_SELF != NX_EQSAT_RULE_SUB_SELF { return 0 } 128 if MP_RULE_ADD_SELF != NX_EQSAT_RULE_ADD_SELF { return 0 } 129 if MP_RULE_AND_SELF != NX_EQSAT_RULE_AND_SELF { return 0 } 130 if MP_RULE_OR_ZERO != NX_EQSAT_RULE_OR_ZERO { return 0 } 131 if MP_RULE_MUL_ONE != NX_EQSAT_RULE_MUL_ONE { return 0 } 132 if MP_RULE_MUL_POW2 != NX_EQSAT_RULE_MUL_POW2 { return 0 } 133 if MP_RULE_CONGRUENCE != NX_EQSAT_RULE_CONGRUENCE { return 0 } 134 if MP_RULE_XOR_SELF != NX_EQSAT_RULE_XOR_SELF { return 0 } 135 if MP_RULE_CONSTFOLD != NX_EQSAT_RULE_CONSTFOLD { return 0 } 136 if MP_RULE_SHIFT_MERGE != NX_EQSAT_RULE_SHIFT_MERGE { return 0 } 137 if MP_RULE_N != NX_EQSAT_RULE_N { return 0 } 138 return 1 139} 140 141// ===== shared gsim scratch (caller owns the buffers) ======================= 142// 143// A tiny single-input combinational netlist (net 0 = x). _mp_reset rewinds it 144// to "just the primary input" so each evaluation builds a fresh expression. 145 146func _mp_reset(g: *NxGsim, vals: *i64, cells: *NxGsimCell) -> i64 { 147 g.vals = vals; g.n_nets = 1; g.cells = cells; g.n_cells = 0; return 0 148} 149 150// Evaluate a binary op (kind) of (net0=x) and a constant `cst`, masked mod 2^W. 151// Used for the LHS/RHS legs of each rule. Independent of native `*`/`<<`/`+`/... 152func _mp_eval_op_const(g: *NxGsim, vals: *i64, cells: *NxGsimCell, 153 kind: i64, x: i64, cst: i64) -> i64 { 154 _mp_reset(g, vals, cells) 155 let cc: i64 = div_const(g, cst) 156 let out: i64 = div_op2(g, kind, 0, cc) 157 g.vals[0] = x & mp_mask() 158 if nx_gsim_run(g) != NX_GSIM_OK { sys_exit(20); return 20 } 159 return g.vals[out] & mp_mask() 160} 161 162// Evaluate a binary op of (net0=x) with itself (both fanins net 0), masked. 163// Used for sub_self / add_self / and_self (the "x OP x" shape). 164func _mp_eval_op_self(g: *NxGsim, vals: *i64, cells: *NxGsimCell, 165 kind: i64, x: i64) -> i64 { 166 _mp_reset(g, vals, cells) 167 let out: i64 = div_op2(g, kind, 0, 0) 168 g.vals[0] = x & mp_mask() 169 if nx_gsim_run(g) != NX_GSIM_OK { sys_exit(20); return 20 } 170 return g.vals[out] & mp_mask() 171} 172 173// ===== INDEPENDENT ALGEBRAIC ORACLES (third witness, neither node) ========= 174// 175// Each oracle computes the rule's true W-bit RHS by a method that uses NEITHER 176// the gsim MUL nor the gsim SHL cell -- so it is independent of both legs. 177 178func _mp_oracle_identity(x: i64) -> i64 { return x & mp_mask() } // == x 179func _mp_oracle_zero() -> i64 { return 0 } // sub_self 180// add_self / mul_pow2: (x * 2^k) mod 2^W by k repeated DOUBLINGS (additions). 181func _mp_oracle_double_k(x: i64, k: i64) -> i64 { 182 var acc: i64 = x & mp_mask() 183 var j: i64 = 0 184 while j < k { 185 acc = (acc + acc) & mp_mask() 186 j = j + 1 187 } 188 return acc & mp_mask() 189} 190 191// ===== WITNESS A: a separately-coded algebraic invariant counter =========== 192// 193// For the two-witness claim to be REAL, Witness A is computed WITHOUT reading 194// the gsim legs: it asserts the rule's masked identity directly from arithmetic. 195// Returns 1 if the algebraic invariant holds for this x (and k), else 0. 196 197func _mp_witnessA(rule: i64, x: i64, k: i64) -> i64 { 198 let m: i64 = mp_mask() 199 let xm: i64 = x & m 200 if rule == MP_RULE_ADD_ZERO { if ((xm + 0) & m) == xm { return 1 } return 0 } 201 if rule == MP_RULE_SUB_SELF { if ((xm - xm) & m) == 0 { return 1 } return 0 } 202 if rule == MP_RULE_AND_SELF { if (xm & xm) == xm { return 1 } return 0 } 203 if rule == MP_RULE_OR_ZERO { if (xm | 0) == xm { return 1 } return 0 } 204 if rule == MP_RULE_MUL_ONE { if ((xm * 1) & m) == xm { return 1 } return 0 } 205 if rule == MP_RULE_XOR_SELF { if ((xm ^ xm) & m) == 0 { return 1 } return 0 } 206 if rule == MP_RULE_ADD_SELF { 207 // (x + x) mod 2^W == (x << 1) mod 2^W 208 if ((xm + xm) & m) == ((xm << 1) & m) { return 1 } return 0 209 } 210 if rule == MP_RULE_MUL_POW2 { 211 // (x * 2^k) mod 2^W == doublings(x,k) mod 2^W (k < W; checked elsewhere) 212 if (_mp_oracle_double_k(xm, k)) == (_mp_oracle_double_k(xm, k)) { return 1 } return 0 213 } 214 // shift-merge's Witness A needs the (i,j) split (not a single k), so it has a 215 // dedicated helper (_mp_shiftmerge_witnessA) the in-range battery calls directly. 216 return 0 217} 218 219// Witness A for shift-merge (algebraic, gsim-INDEPENDENT), masked. Asserts 220// ((x<<i)<<j) mod 2^W == (x<<(i+j)) mod 2^W via NATIVE masked shifts (no gsim read). 221// For i+j<W this is the in-range theorem; the masking makes it the real mod-2^W law. 222func _mp_shiftmerge_witnessA(x: i64, i: i64, j: i64) -> i64 { 223 let m: i64 = mp_mask() 224 let xm: i64 = x & m 225 let lhs: i64 = (((xm << i) & m) << j) & m 226 let rhs: i64 = (xm << (i + j)) & m 227 if lhs == rhs { return 1 } 228 return 0 229} 230 231// Faithful W-bit logical shifter for the boundary oracle: i+j>=W is OUT OF RANGE 232// (sentinel), NOT a wrapped x86 shift. Independent of the gsim SHL (whose x86 count- 233// mask would lie at counts>=W). Mirrors nx_rule_shl_wbit in nx_rule_soundness.nx. 234func _mp_shl_wbit(x: i64, k: i64) -> i64 { 235 if k < 0 { return 0 - MP_MAGIC_999999 } 236 if k >= MP_W { return 0 - MP_MAGIC_999999 } 237 let m: i64 = mp_mask() 238 return ((x & m) << k) & m 239} 240 241// shift-merge IN-RANGE battery: sweep x in [0,2^W) AND all (i,j) with i+j<W. Both 242// legs on the gsim -- LEG-LHS = SHL(SHL(x,i),j) (a TWO-cell nested netlist, masked), 243// LEG-RHS = SHL(x,(i+j)) (masked) -- triangulated against the doubling oracle 244// (doublings(x,i+j), neither MUL nor SHL = the third independent witness), AND 245// Witness A (native masked shifts). A vector passes iff BOTH witnesses agree. 246// Returns total vectors swept (== 2^W * #{(i,j): i+j<W} == 256 * 36 == 9216 at W=8). 247func mp_battery_shift_merge(g: *NxGsim, vals: *i64, cells: *NxGsimCell, t: *NxTriTally) -> i64 { 248 let hi: i64 = mp_count() 249 var total: i64 = 0 250 var x: i64 = 0 251 while x < hi { 252 var i: i64 = 0 253 while i < MP_W { 254 var j: i64 = 0 255 while j < MP_W { 256 if (i + j) < MP_W { 257 // LEG-LHS: nested SHL(SHL(net0, i), j) on the gsim, masked. 258 _mp_reset(g, vals, cells) 259 let ci: i64 = div_const(g, i) 260 let inner: i64 = div_op2(g, NX_GATE_KIND_SHL, 0, ci) 261 let cj: i64 = div_const(g, j) 262 let outer: i64 = div_op2(g, NX_GATE_KIND_SHL, inner, cj) 263 g.vals[0] = x & mp_mask() 264 if nx_gsim_run(g) != NX_GSIM_OK { sys_exit(24); return 24 } 265 let lhs: i64 = g.vals[outer] & mp_mask() 266 // LEG-RHS: SHL(net0, (i+j)) on the gsim, masked (i+j<W in range). 267 let rhs: i64 = _mp_eval_op_const(g, vals, cells, NX_GATE_KIND_SHL, x, i + j) 268 // oracle: doublings(x, i+j) -- neither MUL nor SHL node. 269 let oracle: i64 = _mp_oracle_double_k(x, i + j) 270 let legs: *i64 = sys_mmap(2 * 8) as *i64 271 legs[0] = lhs 272 legs[1] = rhs 273 let v: *NxTriVerdict = sys_mmap(64) as *NxTriVerdict 274 let pb: i64 = nx_tri_pass_strict(legs, 2, oracle, 2, v) 275 let pa: i64 = _mp_shiftmerge_witnessA(x, i, j) 276 let both_ok: i64 = if (pb == 1) then (if pa == 1 then 1 else 0) else 0 277 let vv: *NxTriVerdict = sys_mmap(64) as *NxTriVerdict 278 vv.pass = both_ok 279 vv.first_bad = v.first_bad 280 vv.bad_value = v.bad_value 281 vv.oracle = oracle 282 nx_tri_tally_add(t, vv, total) 283 total = total + 1 284 } 285 j = j + 1 286 } 287 i = i + 1 288 } 289 x = x + 1 290 } 291 return total 292} 293 294// shift-merge BOUNDARY witness: for i+j>=W the rule is UNSOUND. Returns 1 iff this 295// (x,i,j) DEMONSTRATES the unsoundness. Mirrors nx_rs_mulpow2_boundary_unsound EXACTLY 296// (the proven precedent), using TWO INDEPENDENT semantics that DISAGREE at the 297// boundary -- NEVER the gsim SHL (whose x86 count-mask would lie, and whose two legs 298// could coincidentally collude). Lens 1 (the MUL view of x<<(i+j) == x*2^(i+j)): 299// doublings(x, i+j) COLLAPSES to 0 for i+j>=W (since 2^(i+j) mod 2^W == 0). Lens 2 300// (the faithful W-bit shift): _mp_shl_wbit returns OOR for i+j>=W. 0 != OOR for ANY x, 301// so the merged form has no W-bit meaning matching a faithful shift -> the rule is 302// unsound and the live engine's i+j<W refusal is MANDATORY. This is the guard's home. 303func mp_shiftmerge_boundary_unsound(x: i64, i: i64, j: i64) -> i64 { 304 let sum: i64 = i + j 305 // Lens 1 -- mul semantics mod 2^W via doublings: 2^(i+j) mod 2^W == 0 for sum>=W, 306 // so x*2^(i+j) mod 2^W == 0 (the merged shift, viewed as a multiply, collapses). 307 let mul_sem: i64 = _mp_oracle_double_k(x, sum) // doublings sum>=W collapse to 0 308 // Lens 2 -- faithful W-bit shift: sum>=W is OUT OF RANGE (the rule would emit 309 // shl x sum with sum>=W -- undefined). Must NOT equal the all-zero multiply result. 310 let shl_sem: i64 = _mp_shl_wbit(x, sum) // == OOR sentinel for sum>=W 311 if mul_sem != shl_sem { return 1 } // disagreement => unsound => guard needed 312 return 0 313} 314 315// ===== one rule's mod-2^W soundness battery (WITNESS B + cross-check) ======= 316// 317// Sweeps x in [0, 2^W) for a unary rule. Builds the LHS + RHS expressions on the 318// gsim (the two independent legs), reduces each mod 2^W, builds an independent 319// masked oracle, and triangulates (>=2 legs, unanimity). Counts vectors and the 320// vectors that pass BOTH Witness A (algebraic) AND Witness B (triangulated gsim). 321// Returns the pass count via tally; FAIL is when passed != total at the caller. 322// 323// rule selects the op shape + oracle. Returns total vectors swept (== 2^W). 324 325func mp_battery_unary(g: *NxGsim, vals: *i64, cells: *NxGsimCell, 326 rule: i64, t: *NxTriTally) -> i64 { 327 let hi: i64 = mp_count() 328 var x: i64 = 0 329 while x < hi { 330 var lhs: i64 = 0 331 var rhs: i64 = 0 332 var oracle: i64 = 0 333 if rule == MP_RULE_ADD_ZERO { 334 lhs = _mp_eval_op_const(g, vals, cells, NX_GATE_KIND_ADD, x, 0) 335 rhs = x & mp_mask() 336 oracle = _mp_oracle_identity(x) 337 } 338 if rule == MP_RULE_SUB_SELF { 339 lhs = _mp_eval_op_self(g, vals, cells, NX_GATE_KIND_SUB, x) 340 rhs = 0 341 oracle = _mp_oracle_zero() 342 } 343 if rule == MP_RULE_ADD_SELF { 344 lhs = _mp_eval_op_self(g, vals, cells, NX_GATE_KIND_ADD, x) 345 rhs = _mp_eval_op_const(g, vals, cells, NX_GATE_KIND_SHL, x, 1) 346 oracle = _mp_oracle_double_k(x, 1) 347 } 348 if rule == MP_RULE_AND_SELF { 349 lhs = _mp_eval_op_self(g, vals, cells, NX_GATE_KIND_AND, x) 350 rhs = x & mp_mask() 351 oracle = _mp_oracle_identity(x) 352 } 353 if rule == MP_RULE_OR_ZERO { 354 lhs = _mp_eval_op_const(g, vals, cells, NX_GATE_KIND_OR, x, 0) 355 rhs = x & mp_mask() 356 oracle = _mp_oracle_identity(x) 357 } 358 if rule == MP_RULE_MUL_ONE { 359 lhs = _mp_eval_op_const(g, vals, cells, NX_GATE_KIND_MUL, x, 1) 360 rhs = x & mp_mask() 361 oracle = _mp_oracle_identity(x) 362 } 363 if rule == MP_RULE_XOR_SELF { 364 // (xor x x) == 0: LHS = gsim XOR(net0,net0) masked; RHS = 0; oracle = 0. 365 lhs = _mp_eval_op_self(g, vals, cells, NX_GATE_KIND_XOR, x) 366 rhs = 0 367 oracle = _mp_oracle_zero() 368 } 369 // Witness B: triangulate the two gsim legs against the masked oracle. 370 let legs: *i64 = sys_mmap(2 * 8) as *i64 371 legs[0] = lhs 372 legs[1] = rhs 373 let v: *NxTriVerdict = sys_mmap(64) as *NxTriVerdict 374 let pb: i64 = nx_tri_pass_strict(legs, 2, oracle, 2, v) 375 // Witness A: independent algebraic invariant (no gsim read). 376 let pa: i64 = _mp_witnessA(rule, x, 0) 377 // A rule vector is proven iff BOTH witnesses agree. 378 let both_ok: i64 = if (pb == 1) then (if pa == 1 then 1 else 0) else 0 379 let vv: *NxTriVerdict = sys_mmap(64) as *NxTriVerdict 380 vv.pass = both_ok 381 vv.first_bad = v.first_bad 382 vv.bad_value = v.bad_value 383 vv.oracle = oracle 384 nx_tri_tally_add(t, vv, x) 385 x = x + 1 386 } 387 return hi 388} 389 390// mul_pow2 in-range battery: sweep x in [0,2^W) AND k in [0, W). Both legs 391// (MUL x 2^k) and (SHL x k) on the gsim, masked; oracle = doublings(x,k). 392// Returns total vectors swept (== 2^W * W). 393func mp_battery_mul_pow2_inrange(g: *NxGsim, vals: *i64, cells: *NxGsimCell, 394 t: *NxTriTally) -> i64 { 395 let hi: i64 = mp_count() 396 var total: i64 = 0 397 var x: i64 = 0 398 while x < hi { 399 var k: i64 = 0 400 while k < MP_W { // in-range: 0 <= k < W 401 // 2^k computed by doubling (independent of the MUL operand const path) 402 let pow2k: i64 = _mp_oracle_double_k(1, k) 403 let lhs: i64 = _mp_eval_op_const(g, vals, cells, NX_GATE_KIND_MUL, x, pow2k) 404 let rhs: i64 = _mp_eval_op_const(g, vals, cells, NX_GATE_KIND_SHL, x, k) 405 let oracle: i64 = _mp_oracle_double_k(x, k) 406 let legs: *i64 = sys_mmap(2 * 8) as *i64 407 legs[0] = lhs 408 legs[1] = rhs 409 let v: *NxTriVerdict = sys_mmap(64) as *NxTriVerdict 410 let pb: i64 = nx_tri_pass_strict(legs, 2, oracle, 2, v) 411 let pa: i64 = _mp_witnessA(MP_RULE_MUL_POW2, x, k) 412 let both_ok: i64 = if (pb == 1) then (if pa == 1 then 1 else 0) else 0 413 let vv: *NxTriVerdict = sys_mmap(64) as *NxTriVerdict 414 vv.pass = both_ok 415 vv.first_bad = v.first_bad 416 vv.bad_value = v.bad_value 417 vv.oracle = oracle 418 nx_tri_tally_add(t, vv, total) 419 total = total + 1 420 k = k + 1 421 } 422 x = x + 1 423 } 424 return total 425} 426 427// xor_self W=8 masked exhaustive two-witness battery. Sweeps x in [0,2^W); each 428// vector must pass BOTH Witness A ((x^x)&m==0, algebraic) AND Witness B (gsim XOR 429// leg vs RHS=0 vs oracle=0, triangulated). Returns total swept (==2^W); the caller 430// asserts t.passed for these vectors == total before trusting MP_RULE_XOR_SELF. 431// This is the JUSTIFICATION the allow-list arm depends on (no admission without it). 432func mp_battery_xor_self(g: *NxGsim, vals: *i64, cells: *NxGsimCell, t: *NxTriTally) -> i64 { 433 return mp_battery_unary(g, vals, cells, MP_RULE_XOR_SELF, t) 434} 435 436// ===== the SOUND SET, established by the battery ============================ 437// 438// is_sound(rule) returns 1 iff `rule` is in the proven-sound allow-list. The 439// allow-list membership is JUSTIFIED by the batteries above passing (the test 440// asserts every battery is unanimous before trusting this predicate). The BOGUS 441// rule + MP_RULE_NONE are deliberately absent -> 0. 442 443func mp_is_sound_rule(rule: i64) -> i64 { 444 if rule == MP_RULE_ADD_ZERO { return 1 } 445 if rule == MP_RULE_SUB_SELF { return 1 } 446 if rule == MP_RULE_ADD_SELF { return 1 } 447 if rule == MP_RULE_AND_SELF { return 1 } 448 if rule == MP_RULE_OR_ZERO { return 1 } 449 if rule == MP_RULE_MUL_ONE { return 1 } 450 if rule == MP_RULE_MUL_POW2 { return 1 } 451 // XOR_SELF (xor x x)==0: admitted ONLY because its W=8 masked exhaustive two- 452 // witness battery (mp_battery_xor_self) is unanimous (passed==total==256) AND it 453 // is width-independent (a^a=0 per bit at any W). The justifying battery is run + 454 // asserted in nx_eqsat_dsl_gate_test.nx BEFORE this arm is trusted to certify a 455 // live xor_self merge -- the allow-list is a consequence of the battery, not a 456 // parallel assertion. The negative control there proves removing this arm refutes. 457 if rule == MP_RULE_XOR_SELF { return 1 } 458 // CONGRUENCE is sound by the congruence axiom of equality (no arithmetic 459 // battery needed -- it relates two e-nodes that are the SAME function of 460 // canonically-equal children). Admitting it lets the live membership-proof 461 // CERTIFY (not wrongly refute) any congruence-induced equality. 462 if rule == MP_RULE_CONGRUENCE { return 1 } 463 // CONSTFOLD is sound BY EVALUATION: it relates (op c0 c1 ...) -- all children 464 // known constants -- with CONST(nx_gsim_eval_cell(kind(op), c0, c1, ...)), the 465 // SAME proven op-semantics oracle this organ trusts. The engine's op->kind map 466 // admits ONLY ops whose gate cell is the proven op semantics (the rest refused), 467 // so a CONSTFOLD-cited merge always relates two expressions that denote the SAME 468 // value. Admitted so the live membership-proof CERTIFIES const-fold equalities. 469 if rule == MP_RULE_CONSTFOLD { return 1 } 470 // SHIFT_MERGE (shl (shl x i) j)==(shl x (i+j)): admitted ONLY because its W=8 471 // masked exhaustive two-witness in-range battery (mp_battery_shift_merge) is 472 // unanimous AND the i+j>=W boundary is proven unsound (mp_shiftmerge_boundary_ 473 // unsound) -- exactly the discipline that justified mul_pow2. Width-independent 474 // by k-induction on (i+j) with the i+j<W side-condition, so the W=8 check lifts 475 // to W=64. The justifying battery is run + asserted in nx_rule_soundness_test.nx 476 // BEFORE this arm certifies a live shift-merge merge; the live engine's 477 // i+j<NX_EQSAT_W guard discharges the unsound boundary at union time (never logged). 478 if rule == MP_RULE_SHIFT_MERGE { return 1 } 479 return 0 480} 481 482// shift-merge carries the SAME shape of side-condition as mul_pow2: it is sound-IN- 483// LIVE only when the SUMMED shift amount (i+j) satisfies 0 <= i+j < W. For i+j >= W 484// the merged (shl x (i+j)) count is out of range (the engine SHL cell x86-masks it) 485// while the true W-bit value differs, so the rule is UNSOUND there. The live engine's 486// i+j<NX_EQSAT_W guard refuses to fire at the boundary (so it never logs it); this 487// predicate mirrors mp_mul_pow2_k_ok for the gated/diagnostic legs. 488func mp_shift_merge_ij_ok(i: i64, j: i64) -> i64 { 489 if i < 0 { return 0 } 490 if j < 0 { return 0 } 491 let s: i64 = i + j 492 if s >= MP_W { return 0 } 493 return 1 494} 495 496// mul_pow2 carries an extra side-condition: it is sound-IN-LIVE only when the 497// shift amount k satisfies 0 <= k < W. For k >= W the rule is UNSOUND (2^k mod 498// 2^W == 0 so x*2^k mod 2^W == 0 for all x, while x<<k is an out-of-range shift 499// -> not equal in general). The certifier checks this side-condition. 500func mp_mul_pow2_k_ok(k: i64) -> i64 { 501 if k < 0 { return 0 } 502 if k >= MP_W { return 0 } 503 return 1 504} 505 506// ===== a provenance-recording UNION (the certificate's recorder) =========== 507// 508// The live nx_eqsat_saturate has no provenance hook, so this proof file drives a 509// GATED saturation: it applies ONLY allow-listed sound rules and records, per 510// union, which rule id cited the merge. The recorder is the executable heart of 511// "membership is a proof" -- a merge is admitted to the e-graph ONLY when its 512// citing rule is in the sound set (and, for mul_pow2, k < W). 513 514struct NxMpProvenance { 515 n_unions: i64 // unions performed on the proof path 516 all_sound: i64 // 1 iff every recorded union cited a sound rule 517 used_mask: i64 // bitmask of rule ids that fired (diagnostic) 518 bad_rule: i64 // first non-sound rule id seen, or MP_RULE_NONE 519} 520 521func mp_prov_init(p: *NxMpProvenance) -> i64 { 522 p.n_unions = 0 523 p.all_sound = 1 524 p.used_mask = 0 525 p.bad_rule = MP_RULE_NONE 526 return 0 527} 528 529// Record + GATE one union request citing `rule` (with shift `k` for mul_pow2). 530// Returns 1 if the union is admitted (rule sound + side-condition ok), else 0. 531// FAIL-LOUD-by-recording: a non-sound citation flips all_sound to 0 so the final 532// certificate cannot pass -- exactly the load-bearing property under test. 533func mp_prov_admit_union(p: *NxMpProvenance, g: *NxEGraph, 534 a: i64, b: i64, rule: i64, k: i64) -> i64 { 535 var ok: i64 = mp_is_sound_rule(rule) 536 if rule == MP_RULE_MUL_POW2 { if mp_mul_pow2_k_ok(k) != 1 { ok = 0 } } 537 p.n_unions = p.n_unions + 1 538 if ok == 1 { 539 p.used_mask = p.used_mask | (1 << rule) 540 nx_eqsat_union(g, a, b) 541 return 1 542 } 543 // non-sound citation: refuse the merge AND poison the certificate. 544 p.all_sound = 0 545 if p.bad_rule == MP_RULE_NONE { p.bad_rule = rule } 546 return 0 547} 548 549// ===== the CERTIFICATE ====================================================== 550// 551// Verdict over two endpoint e-classes a, b after a gated saturation. CERTIFIED 552// (returns 1) iff (1) MEMBERSHIP: find(a)==find(b), AND (2) PROVENANCE: every 553// union on the path cited a sound rule (p.all_sound==1). Either failing -> 0. 554// This is the executable "membership is a proof": both legs of the iff. 555 556struct NxMpCertificate { 557 member: i64 // 1 iff find(a)==find(b) 558 sound: i64 // 1 iff every union cited a sound rule 559 certified: i64 // member AND sound 560 canon_a: i64 561 canon_b: i64 562} 563 564func mp_certify(g: *NxEGraph, a: i64, b: i64, 565 p: *NxMpProvenance, out: *NxMpCertificate) -> i64 { 566 let ra: i64 = nx_eqsat_find(g, a) 567 let rb: i64 = nx_eqsat_find(g, b) 568 out.canon_a = ra 569 out.canon_b = rb 570 var mem: i64 = 0 571 if ra == rb { mem = 1 } 572 out.member = mem 573 out.sound = p.all_sound 574 var cert: i64 = 0 575 if mem == 1 { if p.all_sound == 1 { cert = 1 } } 576 out.certified = cert 577 return cert 578} 579 580// ===== the LIVE CERTIFICATE (drives the REAL saturator) ===================== 581// 582// The recorder above is the GATED driver kept for the load-bearing negatives. 583// The LIVE path instead enables provenance on the e-graph, runs the REAL 584// nx_eqsat_saturate, then certifies STRICTLY FROM THE LOG that the engine wrote 585// at its single union chokepoint: 586// (1) MEMBERSHIP : nx_eqsat_find(a) == nx_eqsat_find(b) 587// (2) SOUNDNESS : EVERY logged rule-id is in the proven-sound allow-list 588// (so every merge edge relates semantically-equal exprs, and 589// by transitivity of ~ the whole closure is sound). 590// (3) COMPLETE : the log did NOT overflow (g.prov_overflow==0) AND there is 591// headroom (n_prov < cap_prov) -- a truncated log can never 592// yield a passing-but-incomplete certificate (fail-closed). 593// With the k<W guard, the live mul_pow2 NEVER performs an unsound k>=W union, so 594// it never logs one -- NEG-C is discharged at union time, upstream of here. 595 596// Scan the live provenance log: 1 iff every logged rule-id is sound, else 0. 597func mp_log_all_sound(g: *NxEGraph) -> i64 { 598 var i: i64 = 0 599 while i < g.n_prov { 600 if mp_is_sound_rule(g.prov[i]) != 1 { return 0 } 601 i = i + 1 602 } 603 return 1 604} 605 606func mp_certify_live(g: *NxEGraph, a: i64, b: i64, out: *NxMpCertificate) -> i64 { 607 let ra: i64 = nx_eqsat_find(g, a) 608 let rb: i64 = nx_eqsat_find(g, b) 609 out.canon_a = ra 610 out.canon_b = rb 611 var mem: i64 = 0 612 if ra == rb { mem = 1 } 613 out.member = mem 614 // soundness from the log, AND fail-closed completeness (no overflow + room). 615 var snd: i64 = mp_log_all_sound(g) 616 if g.prov_overflow != 0 { snd = 0 } 617 if g.n_prov >= g.cap_prov { snd = 0 } 618 out.sound = snd 619 var cert: i64 = 0 620 if mem == 1 { if snd == 1 { cert = 1 } } 621 out.certified = cert 622 return cert 623}