code wiki / _hdl_build / nx_superopt.nx
nx_superopt.nx source
↩ module page · 238 lines · 12495 B
1// nx_superopt.nx -- the AUTHOR leg of the Sovereign Invention Engine: a
2// MECHANIZED SUPEROPTIMIZER. With NO LLM in the loop it runs one full invention
3// tick over an arithmetic expression:
4//
5// PROPOSE -- nx_eqsat saturation generates the equivalence-class closure of
6// the seed under the wired sound rewrite rules (the proposal space
7// is materialized deterministically; no search policy, no LLM).
8// SCORE -- TWO tiers, both honest. Tier-1 = the e-graph's bottom-up cost
9// extractor (nx_eqsat_recompute_best / nx_eqsat_best_cost, the
10// RV64IM op-cost model). Tier-2 = an INDEPENDENT width-weighted
11// gate-level critical-path (nx_lat_honest over a lowered NxGsim
12// netlist) so "cheaper" cannot be the unit-metric self-deception.
13// VERIFY -- nx_triangulate equivalence-by-agreement: the candidate must
14// compute the SAME value as the seed over a dense+random+edge
15// battery (exhaustive at the small example width). Crucially the
16// legs are GENUINELY INDEPENDENT realizations (see below), so a
17// collusion among same-instruction legs cannot rubber-stamp an
18// unsound rewrite -- and we prove the check CATCHES a planted
19// non-equivalent candidate.
20// EMIT -- only after VERIFY passes AND the cost strictly decreased on the
21// honest metric do we accept the extracted cheapest DAG.
22//
23// This is mechanized superoptimization = the first rung of mechanized INVENTION
24// (imitate -> match -> superopt/exceed). Honesty: equivalence is high-confidence
25// AGREEMENT over a battery (exhaustive at small width), NOT a symbolic proof --
26// stated, per the racing doctrine.
27//
28// LEG INDEPENDENCE (the keystone defense). A naive triangulation of (mul x 2^k)
29// -> (shl x k) would have every leg evaluate `x << k` on the same i64 ALU and
30// collude: native MUL and SHL wrap identically on i64, so a shared-substrate
31// battery passes by echoing one computation three times -- worthless. So our
32// three legs do NOT share the candidate's arithmetic path:
33// LEG 0 = interpret the CANDIDATE emit-DAG (native ops; the lowered form).
34// LEG 1 = run the lowered CANDIDATE NxGsim netlist (independent gate-sim path).
35// LEG 2 = the OPERAND-RECONSTRUCTING witness: recompute the SEED's value WITHOUT
36// any shift fast-path -- a behavioral shift-add multiply that adds
37// shifted partial products bit-by-bit. An off-by-one shift count, or a
38// rule firing outside its proven width domain, makes LEG 2 disagree.
39// The ORACLE is the SEED emit-DAG interpreted directly. (LEG 0 vs ORACLE already
40// pits candidate-form against seed-form; LEG 2 is the cross-substrate check.)
41//
42// license_tier: ORIGINAL
43
44import "nx_eqsat.nx"
45import "nx_triangulate.nx"
46import "nx_latency_metric.nx"
47const NX_MAGIC_999999: i64 = 999999
48
49const NX_SUPEROPT_OK: i64 = 0
50const NX_SUPEROPT_BAD_ARGS: i64 = 1
51const NX_SUPEROPT_OVERFLOW: i64 = 2
52const NX_SUPEROPT_BAD_KIND: i64 = 3
53
54// Result of one superopt tick. The Engineer asserts on these fields (FAIL LOUD).
55struct NxSuperoptResult {
56 found: i64 // 1 iff a strictly-cheaper verified-equivalent was emitted
57 seed_cost: i64 // Tier-1 e-graph op-cost of the seed root expression
58 cand_cost: i64 // Tier-1 e-graph op-cost of the extracted cheapest form
59 seed_lat: i64 // Tier-2 honest gate-level critical-path latency of the seed
60 cand_lat: i64 // Tier-2 honest gate-level critical-path latency of the candidate
61 cand_root_op: i64 // root op of the emitted candidate DAG
62 cand_count: i64 // number of nodes in the emitted candidate DAG
63 battery_pass: i64 // vectors that triangulated-equivalent
64 battery_total:i64 // vectors judged
65}
66
67// ===== NxEmitNode DAG evaluator (the interpreter behind LEG 0 + the ORACLE) ====
68//
69// Interpret a flat post-order NxEmitNode DAG (as produced by nx_eqsat_emit) over
70// an i64 environment: env[var_id] holds the value of VAR var_id. Children are
71// referenced by out-array index < the parent's index (post-order), so a single
72// in-place evaluation array (val[i] = value of node i) is an exact fold -- when
73// we reach node i, every child's val is final. Native i64 two's-complement ops
74// (this is the in-band realization; LEG 2 below is the independent cross-check).
75// Returns the root value; the caller passes root = the index nx_eqsat_emit gave.
76func nx_superopt_eval_emit(out: *NxEmitNode, n: i64, root: i64, env: *i64, val: *i64) -> i64 {
77 var i: i64 = 0
78 while i < n {
79 let op: i64 = out[i].op
80 var r: i64 = 0
81 if op == NX_EQ_OP_CONST { r = out[i].payload }
82 if op == NX_EQ_OP_VAR { r = env[out[i].payload] }
83 if op == NX_EQ_OP_ADD { r = val[out[i].c0] + val[out[i].c1] }
84 if op == NX_EQ_OP_SUB { r = val[out[i].c0] - val[out[i].c1] }
85 if op == NX_EQ_OP_MUL { r = val[out[i].c0] * val[out[i].c1] }
86 if op == NX_EQ_OP_AND { r = val[out[i].c0] & val[out[i].c1] }
87 if op == NX_EQ_OP_OR { r = val[out[i].c0] | val[out[i].c1] }
88 if op == NX_EQ_OP_XOR { r = val[out[i].c0] ^ val[out[i].c1] }
89 if op == NX_EQ_OP_SHL { r = val[out[i].c0] << val[out[i].c1] }
90 if op == NX_EQ_OP_SHR { r = val[out[i].c0] >> val[out[i].c1] }
91 val[i] = r
92 i = i + 1
93 }
94 return val[root]
95}
96
97// ===== LEG 2: operand-reconstructing, shift-free multiply witness =============
98//
99// Compute (a * b) WITHOUT a multiply instruction and WITHOUT a single-shift
100// fast-path: accumulate shifted copies of a, one partial product per set bit of
101// b, building each shifted copy by REPEATED DOUBLING (add), not by `<<`. This is
102// the textbook shift-add multiplier the candidate's SHL would have to match. If a
103// strength-reduction rule emits the WRONG shift count (or fires for a non-power-
104// of-two, or outside the width domain), this leg computes the true product and
105// DISAGREES -- it cannot collude with the candidate because it never takes the
106// candidate's shift path. Handles negative b by sign-folding (a*b == -(a*(-b))).
107func nx_superopt_mul_shiftadd(a: i64, b: i64) -> i64 {
108 var bb: i64 = b
109 var neg: i64 = 0
110 if bb < 0 { neg = 1; bb = 0 - bb }
111 var acc: i64 = 0
112 var partial: i64 = a // a * 2^bit, grown by doubling (add), never `<<`
113 while bb > 0 {
114 if (bb % 2) != 0 { acc = acc + partial }
115 partial = partial + partial // double via add -- the independent path
116 bb = bb / 2
117 }
118 if neg == 1 { acc = 0 - acc }
119 return acc
120}
121
122// ===== NxEmitNode DAG -> NxGsim netlist lowering (LEG 1 + Tier-2 score) ========
123//
124// Lower a flat NxEmitNode DAG to an NxGsim word-level netlist 1:1: one cell per
125// node, net id = node index, the op-kind map below. VAR cells lower to a CONST
126// placeholder whose value the caller seeds per-vector (a primary input); the real
127// CONST nodes lower to CONST cells carrying their payload. The op map is the
128// confirmed-present NX_EQ_OP_* -> NX_GATE_KIND_* correspondence. Cells come out in
129// the same post-order, so they are topologically sorted for nx_gsim_run /
130// nx_lat_honest. Returns NX_SUPEROPT_OK or 0-NX_SUPEROPT_BAD_KIND (LOUD) on an op
131// with no gate kind. var_net[k], written here, records which net carries VAR k so
132// the caller can drive primary inputs before nx_gsim_run.
133func nx_superopt_eqop_to_gate(op: i64) -> i64 {
134 if op == NX_EQ_OP_CONST { return NX_GATE_KIND_CONST }
135 if op == NX_EQ_OP_VAR { return NX_GATE_KIND_CONST } // primary input, seeded per-vector
136 if op == NX_EQ_OP_ADD { return NX_GATE_KIND_ADD }
137 if op == NX_EQ_OP_SUB { return NX_GATE_KIND_SUB }
138 if op == NX_EQ_OP_MUL { return NX_GATE_KIND_MUL }
139 if op == NX_EQ_OP_AND { return NX_GATE_KIND_AND }
140 if op == NX_EQ_OP_OR { return NX_GATE_KIND_OR }
141 if op == NX_EQ_OP_XOR { return NX_GATE_KIND_XOR }
142 if op == NX_EQ_OP_SHL { return NX_GATE_KIND_SHL }
143 if op == NX_EQ_OP_SHR { return NX_GATE_KIND_SHR }
144 return 0 - NX_SUPEROPT_BAD_KIND
145}
146
147func nx_superopt_lower_to_gsim(out: *NxEmitNode, n: i64,
148 cells: *NxGsimCell, vals: *i64, gs: *NxGsim,
149 var_net: *i64, n_var_slots: i64) -> i64 {
150 var i: i64 = 0
151 while i < n {
152 let op: i64 = out[i].op
153 let kind: i64 = nx_superopt_eqop_to_gate(op)
154 if kind < 0 { return 0 - NX_SUPEROPT_BAD_KIND }
155 cells[i].kind = kind
156 cells[i].fanout = i
157 cells[i].f0 = out[i].c0
158 cells[i].f1 = out[i].c1
159 cells[i].f2 = out[i].c2
160 cells[i].val = 0
161 if op == NX_EQ_OP_CONST { cells[i].val = out[i].payload }
162 if op == NX_EQ_OP_VAR {
163 let vid: i64 = out[i].payload
164 if vid < 0 { return 0 - NX_SUPEROPT_BAD_KIND }
165 if vid >= n_var_slots { return 0 - NX_SUPEROPT_OVERFLOW }
166 var_net[vid] = i // net i carries VAR vid (a primary input)
167 }
168 i = i + 1
169 }
170 gs.vals = vals
171 gs.n_nets = n
172 gs.cells = cells
173 gs.n_cells = n
174 return NX_SUPEROPT_OK
175}
176
177// ===== Triangulation context + one-vector judge ==============================
178//
179// Bundles everything one verify-vector needs into a single record (the call-arg
180// count otherwise exceeds the compiler's 16-arg IR cap, and it keeps the verify
181// loop a clean 3-arg call). The caller fills the pointers/roots ONCE, then calls
182// nx_superopt_judge per input value x. The candidate is judged against the seed
183// over THREE independent legs (LEG0 emit-interp of the candidate, LEG1 gate-sim of
184// the candidate's lowered netlist, LEG2 the shift-FREE reconstruction of the
185// SEED's value); the oracle is the seed emit-DAG interpreted directly.
186struct NxSoCtx {
187 cand_out: *NxEmitNode // candidate emit-DAG
188 cand_n: i64
189 cand_root: i64
190 seed_out: *NxEmitNode // seed (oracle) emit-DAG
191 seed_n: i64
192 seed_root: i64
193 gs: *NxGsim // candidate's lowered netlist (LEG1)
194 var_net: *i64
195 n_var: i64
196 cval: i64 // multiplier constant the seed multiplies by (LEG2 reconstructs x*cval)
197 env: *i64 // var environment scratch (env[0] = x)
198 cbuf: *i64 // emit-interp scratch for the candidate
199 sbuf: *i64 // emit-interp scratch for the seed/oracle
200 legs: *i64 // 3-leg buffer
201 v: *NxTriVerdict
202 t: *NxTriTally
203}
204
205// (nx_superopt_judge is defined at the end of the file, after all the leg
206// realizations it calls -- backward references only.)
207
208// Evaluate a lowered netlist for one input vector (LEG 1). Each VAR lowered to a
209// CONST cell driving net var_net[vid]; nx_gsim_run writes that net from the
210// cell's .val, so we inject this vector's input by setting that CONST cell's .val
211// to env[vid] BEFORE running (writing gs.vals directly would be clobbered by the
212// CONST cell's own re-evaluation in nx_gsim_run). Independent code path from the
213// emit interpreter: the gate sim walks cells, not the post-order val[] fold.
214func nx_superopt_gsim_eval(gs: *NxGsim, var_net: *i64, n_var_slots: i64,
215 env: *i64, root: i64) -> i64 {
216 var k: i64 = 0
217 while k < n_var_slots {
218 if var_net[k] >= 0 { gs.cells[var_net[k]].val = env[k] }
219 k = k + 1
220 }
221 let rc: i64 = nx_gsim_run(gs)
222 if rc != NX_GSIM_OK { return 0 - NX_MAGIC_999999 } // LOUD sentinel; battery will catch
223 return gs.vals[root]
224}
225
226// Judge ONE input value x: fill the 3 independent legs, judge against the seed
227// oracle with strict unanimity, fold into the tally. Returns the verdict pass
228// flag (the negative control reads ctx.v.first_bad after this call).
229func nx_superopt_judge(ctx: *NxSoCtx, x: i64, vec: i64) -> i64 {
230 ctx.env[0] = x
231 let oracle: i64 = nx_superopt_eval_emit(ctx.seed_out, ctx.seed_n, ctx.seed_root, ctx.env, ctx.sbuf)
232 ctx.legs[0] = nx_superopt_eval_emit(ctx.cand_out, ctx.cand_n, ctx.cand_root, ctx.env, ctx.cbuf) // LEG0
233 ctx.legs[1] = nx_superopt_gsim_eval(ctx.gs, ctx.var_net, ctx.n_var, ctx.env, ctx.cand_root) // LEG1
234 ctx.legs[2] = nx_superopt_mul_shiftadd(x, ctx.cval) // LEG2
235 nx_tri_pass_strict(ctx.legs, 3, oracle, 3, ctx.v)
236 nx_tri_tally_add(ctx.t, ctx.v, vec)
237 return ctx.v.pass
238}