code wiki / _hdl_build / nx_eqsat_congruence_bench.nx
nx_eqsat_congruence_bench.nx source
↩ module page · 167 lines · 9051 B
1// nx_eqsat_congruence_bench.nx -- NON-TOY CONGRUENCE-HEAVY race vs egg 0.11.0.
2//
3// THE RACE (engine-only, SAME expression both sides):
4// egg target = /tmp/race_egg/examples/nx_cong.rs, the congruence-heavy graph:
5// base a = (* x 8), b = (<< x 3) -- mul-pow2 merges a~b
6// N pairs la_i = (- a c_i), lb_i = (- b c_i) -- become CONGRUENT after a~b
7// root = (+ (+ ... (+ 0 (+ la_0 lb_0)) ...) (+ la_{N-1} lb_{N-1}))
8// so after the single base merge, the engine's congruence closure must propagate
9// N parent merges (la_i ~ lb_i) -- the exact reachability work egg's deferred
10// rebuild does and the pre-MEET nx_eqsat could NOT do. This is NOT the toy
11// (* x 8) single-rewrite race; congruence FIRES N times per iteration.
12//
13// OUR side, per timed iteration (the SAME end-to-end optimize pipeline egg ran,
14// no proxy, no membership/provenance machinery egg lacks):
15// nx_eqsat_init (fresh e-graph)
16// enable_meet (egg hashcons + congruence rebuild ON)
17// build a, b, N parent pairs, root (egg's RecExpr build)
18// nx_eqsat_saturate (egg's Runner::run -- rules + rebuild to fixpoint)
19// nx_eqsat_recompute_best (egg's Extractor bottom-up cost)
20// nx_eqsat_extract_best_node(root) (egg's find_best)
21//
22// FAIL LOUD: before timing, one verified iteration asserts congruence actually
23// fired -- find(la_i)==find(lb_i) for ALL i (== egg's internal congruence). A
24// fast wrong answer (congruence NOT propagated) loses, exit nonzero.
25//
26// Output (one line):
27// NX_CONG_ENGINE_ONLY N=<n> ITERS=<it> CONG_MERGES=<n> TOTAL_US=<t> US_PER_OP_NS=<u>
28// (US_PER_OP_NS = total_us*1000/iters = nanoseconds/op; divide by 1000 for us/op.)
29//
30// SOVEREIGN: no .sh; runs on the pinned NishiLang compiler. license_tier: ORIGINAL
31
32import "nx_eqsat.nx"
33const K_MAGIC_2000: i64 = 2000
34const K_MAGIC_2048: i64 = 2048
35const K_MAGIC_4096: i64 = 4096
36const K_MAGIC_8192: i64 = 8192
37
38func _emit_num(v: i64) -> i64 {
39 let b: *u8 = sys_mmap(28); var n: i64 = v; if n < 0 { n = 0 - n }
40 let t2: *u8 = sys_mmap(28); var t: i64 = 0
41 if n == 0 { t2[0] = 48; t = 1 }
42 while n > 0 { t2[t] = 48 + (n % 10); n = n / 10; t = t + 1 }
43 var i: i64 = 0; while i < t { b[i] = t2[t - 1 - i]; i = i + 1 }
44 b[t] = 32; sys_write(1, b, t + 1); return 0
45}
46func _emit_str(s: *u8, len: i64) -> i64 { sys_write(1, s, len); return 0 }
47func _nl() -> i64 { let z: *u8 = sys_mmap(2); z[0] = 10; sys_write(1, z, 1); return 0 }
48
49// Build the congruence-heavy graph in `g` and return the count of parent pairs that
50// became congruent (find(la_i)==find(lb_i)). Equals N on success. The caller hands
51// reusable scratch + a list of (la,lb) ids it allocates; we record them so the
52// caller can re-verify. Returns >=0 merges or a negative sentinel on overflow.
53// la_out/lb_out are caller arrays of length >= n (one timed iter rebuilds them).
54func _one_iter(g: *NxEGraph,
55 nodes: *NxENode, cap_nodes: i64,
56 classes: *NxEClass, cap_cls: i64,
57 hc: *HashMap, par_node: *i64, par_cls: *i64, cap_par: i64,
58 worklist: *i64, cap_work: i64,
59 n: i64, la_out: *i64, lb_out: *i64) -> i64 {
60 if nx_eqsat_init(g, nodes, cap_nodes, classes, cap_cls) != NX_EQSAT_OK { return 0 - 100 }
61 nx_hmap_clear(hc)
62 if nx_eqsat_enable_meet(g, hc, par_node, par_cls, cap_par, worklist, cap_work) != NX_EQSAT_OK { return 0 - 101 }
63 let x: i64 = nx_eqsat_add_var(g, 0)
64 let c8: i64 = nx_eqsat_add_const(g, 8)
65 let c3: i64 = nx_eqsat_add_const(g, 3)
66 let a: i64 = nx_eqsat_add_binary(g, NX_EQ_OP_MUL, x, c8) // base a = (* x 8)
67 let b: i64 = nx_eqsat_add_binary(g, NX_EQ_OP_SHL, x, c3) // base b = (<< x 3)
68 if a < 0 { return 0 - 102 }
69 if b < 0 { return 0 - 103 }
70 // Parents over a/b and the fold all use XOR -- which has NO rewrite rule on
71 // EITHER engine, so no identity/self rule re-fires on the congruence-collapsed
72 // (xor y y) shape and the saturation converges cleanly on both sides. Every
73 // la_i = (xor a c_i) still becomes CONGRUENT to lb_i = (xor b c_i) after a~b.
74 var root: i64 = 0 - 1
75 var i: i64 = 0
76 while i < n {
77 let ci: i64 = nx_eqsat_add_const(g, 100 + i)
78 let la: i64 = nx_eqsat_add_binary(g, NX_EQ_OP_XOR, a, ci) // parent over a
79 let lb: i64 = nx_eqsat_add_binary(g, NX_EQ_OP_XOR, b, ci) // parent over b
80 if la < 0 { return 0 - 104 }
81 if lb < 0 { return 0 - 105 }
82 la_out[i] = la
83 lb_out[i] = lb
84 if root < 0 { root = la } else { root = nx_eqsat_add_binary(g, NX_EQ_OP_XOR, root, la) }
85 if root < 0 { return 0 - 106 }
86 root = nx_eqsat_add_binary(g, NX_EQ_OP_XOR, root, lb)
87 if root < 0 { return 0 - 106 }
88 i = i + 1
89 }
90 // SATURATE: mul_pow2 merges a~b, then the deferred congruence rebuild propagates
91 // la_i ~ lb_i for all i (the N congruence merges egg also performs).
92 let sat: i64 = nx_eqsat_saturate(g, 64)
93 if sat != NX_EQSAT_SATURATED { return 0 - 107 }
94 // EXTRACT (egg's Extractor) -- exercise the same read-the-answer work.
95 if nx_eqsat_recompute_best(g) != NX_EQSAT_OK { return 0 - 108 }
96 let best: i64 = nx_eqsat_extract_best_node(g, root)
97 if best < 0 { return 0 - 109 }
98 // count congruence merges: la_i and lb_i in the same canonical class.
99 var merges: i64 = 0
100 var j: i64 = 0
101 while j < n {
102 if nx_eqsat_find(g, la_out[j]) == nx_eqsat_find(g, lb_out[j]) { merges = merges + 1 }
103 j = j + 1
104 }
105 return merges
106}
107
108func main() -> i64 {
109 let n: i64 = 64 // parent pairs (matches egg default N=64)
110 let iters: i64 = K_MAGIC_2000 // matches egg iters for N=64
111
112 // Generous fixed caps: per iter we add ~ 5 + 4*N nodes/classes. N=64 -> ~261.
113 let cap_nodes: i64 = K_MAGIC_2048
114 let cap_cls: i64 = K_MAGIC_2048
115 let nodes: *NxENode = sys_mmap(cap_nodes * 64) as *NxENode
116 let classes: *NxEClass = sys_mmap(cap_cls * 32) as *NxEClass
117 let g: *NxEGraph = sys_mmap(128) as *NxEGraph
118 // hashcons sized power-of-2 well above nodes/0.7.
119 let hc: *HashMap = nx_hmap_alloc(K_MAGIC_4096)
120 if (hc as i64) == 0 { sys_exit(90); return 90 }
121 let cap_par: i64 = K_MAGIC_8192
122 let par_node: *i64 = sys_mmap(cap_par * 8) as *i64
123 let par_cls: *i64 = sys_mmap(cap_par * 8) as *i64
124 let cap_work: i64 = K_MAGIC_8192
125 let worklist: *i64 = sys_mmap(cap_work * 8) as *i64
126 let la_out: *i64 = sys_mmap(n * 8) as *i64
127 let lb_out: *i64 = sys_mmap(n * 8) as *i64
128
129 // ---- CORRECTNESS GATE (FAIL LOUD before timing): congruence MUST fire N times.
130 let merges: i64 = _one_iter(g, nodes, cap_nodes, classes, cap_cls,
131 hc, par_node, par_cls, cap_par, worklist, cap_work,
132 n, la_out, lb_out)
133 if merges != n { sys_exit(80); return 80 } // every parent pair congruence-merged
134
135 // ---- TIMED LOOP -------------------------------------------------------------
136 let t0: i64 = sys_clock_now_us()
137 var k: i64 = 0
138 while k < iters {
139 let m: i64 = _one_iter(g, nodes, cap_nodes, classes, cap_cls,
140 hc, par_node, par_cls, cap_par, worklist, cap_work,
141 n, la_out, lb_out)
142 if m != n { sys_exit(81); return 81 } // FAIL LOUD mid-run (no fast wrong answer)
143 k = k + 1
144 }
145 let t1: i64 = sys_clock_now_us()
146 let total_us: i64 = t1 - t0
147 let us_per_op_ns: i64 = (total_us * 1000) / iters
148
149 // ---- REPORT -----------------------------------------------------------------
150 let s_tag: *u8 = sys_mmap(32)
151 s_tag[0]=78; s_tag[1]=88; s_tag[2]=95; s_tag[3]=67; s_tag[4]=79; s_tag[5]=78; s_tag[6]=71 // "NX_CONG"
152 s_tag[7]=95; s_tag[8]=69; s_tag[9]=78; s_tag[10]=71; s_tag[11]=73; s_tag[12]=78; s_tag[13]=69; s_tag[14]=95; s_tag[15]=79; s_tag[16]=78; s_tag[17]=76; s_tag[18]=89; s_tag[19]=32 // "_ENGINE_ONLY "
153 _emit_str(s_tag, 20)
154 let s_n: *u8 = sys_mmap(4); s_n[0]=78; s_n[1]=61 // "N="
155 _emit_str(s_n, 2); _emit_num(n)
156 let s_it: *u8 = sys_mmap(8); s_it[0]=73; s_it[1]=84; s_it[2]=69; s_it[3]=82; s_it[4]=83; s_it[5]=61 // "ITERS="
157 _emit_str(s_it, 6); _emit_num(iters)
158 let s_cm: *u8 = sys_mmap(16); s_cm[0]=67; s_cm[1]=79; s_cm[2]=78; s_cm[3]=71; s_cm[4]=95; s_cm[5]=77; s_cm[6]=69; s_cm[7]=82; s_cm[8]=71; s_cm[9]=69; s_cm[10]=83; s_cm[11]=61 // "CONG_MERGES="
159 _emit_str(s_cm, 12); _emit_num(merges)
160 let s_to: *u8 = sys_mmap(16); s_to[0]=84; s_to[1]=79; s_to[2]=84; s_to[3]=65; s_to[4]=76; s_to[5]=95; s_to[6]=85; s_to[7]=83; s_to[8]=61 // "TOTAL_US="
161 _emit_str(s_to, 9); _emit_num(total_us)
162 let s_up: *u8 = sys_mmap(24); s_up[0]=85; s_up[1]=83; s_up[2]=95; s_up[3]=80; s_up[4]=69; s_up[5]=82; s_up[6]=95; s_up[7]=79; s_up[8]=80; s_up[9]=95; s_up[10]=78; s_up[11]=83; s_up[12]=61 // "US_PER_OP_NS="
163 _emit_str(s_up, 13); _emit_num(us_per_op_ns)
164 _nl()
165
166 sys_exit(0); return 0
167}