Skip to main content

logicaffeine_compile/codegen_sva/
wprog_engine_rtl.rs

1//! wprog_engine_rtl — the WHOLE-PROGRAM spatial engine: run an entire control-flow program on the
2//! reconfigurable fabric, streamed over the wire, not just one straight-line block. Where the block engine
3//! (`block_engine_rtl`) runs a single straight-line residual, this runs a whole `ProgramCfgFabric` — multiple
4//! basic blocks with branch conditions (`Select` cells), a register file that survives block reconfiguration,
5//! and a block-walk terminator (Jump / Branch / Return) — the RTL of `run_program_cfg_on_grid` driven by a
6//! flat config, so ANY acyclic branchy program runs on ONE physical array by loading its config.
7//!
8//! This module is the reference + config layer (pure Rust, provable without a simulator): `serialize_program`
9//! flattens a `ProgramCfgFabric` into a `WProgConfig`, and `eval_wprog_config` interprets that flat config
10//! exactly as the RTL will — proven bit-identical to `run_program_cfg_on_grid` (≡ the VM) by
11//! `wprog_engine_lock`. The RTL emitter + the over-the-wire SoC lower this same flat config to silicon.
12//!
13//! Correct-first scope: Arith + Select cells (arithmetic + branch decisions) — the whole acyclic branchy
14//! control-flow family. The state plane (`Mem` cells → heap BRAM), loops (the loop fabric), and function
15//! calls (a call stack) are the follow-ons that extend it toward the whole language.
16
17use super::chip_protocol::{
18    fill_verilog, IMAGE_GO, IMAGE_HEAP, TAG_BOOL, TAG_FLOAT, TAG_INT, TAG_TEXT, TAG_WORD32,
19};
20use super::fabric_rtl::{
21    AluOp, CfgBlock, CmpKind, DecisionFabric, FuncUnit, LoopCell, ProgramCfgFabric, Src, Term,
22};
23use super::wpe_floorplan::ArrayRole;
24
25/// An operand source in the flat config: a live-in register (`In`), a constant, or an earlier cell's value.
26#[derive(Clone, Copy, Debug)]
27pub struct WSrc {
28    pub mode: u8, // 0 = In(ref), 1 = Const(konst), 2 = Val(ref)
29    pub reference: u16,
30    pub konst: i64,
31}
32
33/// One cell. `kind`: 0 = Arith `op(ops[0], ops[1])`, 1 = Select `cmp(ops[0],ops[1]) ? ops[2] : ops[3]`,
34/// 3 = HeapRead `heap[ops[0]]`, 4 = HeapWrite `heap[ops[0]] = ops[1]` (drives `ops[1]`), 5 = HeapAlloc (grow the
35/// heap by `ops[0]` words, drive the new base pointer) — the state plane, mirroring `eval_grid_with_heap`.
36#[derive(Clone, Copy, Debug)]
37pub struct WCell {
38    pub kind: u8,
39    pub code: u8, // AluOp code (Arith) or CmpKind code (Select); unused otherwise
40    pub ops: [WSrc; 4],
41}
42
43/// A block's terminator in the flat config.
44#[derive(Clone, Debug)]
45pub enum WTerm {
46    Jump(usize),
47    Branch {
48        cond: WSrc,
49        then_blk: usize,
50        else_blk: usize,
51    },
52    Return(WSrc),
53    /// A function CALL: run the callee (its entry block `callee_block`) with `args` bound to its parameter
54    /// registers `0..args.len()`, then resume at `ret_block` with the return value written to `dst_reg`. Runs
55    /// on a FRAME STACK (each frame saves the caller's register file), so recursion of any depth works — the
56    /// spatial call mechanism the whole-program fabric needs for the full language.
57    Call {
58        callee_block: usize,
59        args: Vec<WSrc>,
60        dst_reg: u16,
61        ret_block: usize,
62    },
63}
64
65/// One basic block: live-in registers, cells, register writes, terminator.
66#[derive(Clone, Debug)]
67pub struct WBlock {
68    pub livein: Vec<u16>,
69    pub cells: Vec<WCell>,
70    pub writes: Vec<(u16, WSrc)>,
71    pub term: WTerm,
72}
73
74/// A whole program flattened for the fabric.
75#[derive(Clone, Debug)]
76pub struct WProgConfig {
77    pub n_params: usize,
78    pub blocks: Vec<WBlock>,
79}
80
81fn alu_code(op: AluOp) -> u8 {
82    match op {
83        AluOp::Add => 0,
84        AluOp::Sub => 1,
85        AluOp::Mul => 2,
86        AluOp::And => 3,
87        AluOp::Or => 4,
88        AluOp::Xor => 5,
89        AluOp::Shl => 6,
90        AluOp::Shr => 7,
91        AluOp::UShr => 8,
92    }
93}
94
95fn cmp_code(c: CmpKind) -> u8 {
96    match c {
97        CmpKind::Lt => 0,
98        CmpKind::Gt => 1,
99        CmpKind::Le => 2,
100        CmpKind::Ge => 3,
101        CmpKind::Eq => 4,
102        CmpKind::Ne => 5,
103    }
104}
105
106/// Encode a FUNCTIONAL UNIT to the `WCell.code` byte — integer division/mod/floor-div/power/magic-high-mul
107/// AND the FLOAT LANE (`f64`-bit-pattern add/sub/mul/div/sqrt/floor + approximate-equality). `eval_wprog_config`
108/// evaluates every one of these via the same `FuncUnit::eval` the CFG model uses, so the float benchmarks run.
109fn func_code(u: FuncUnit) -> u8 {
110    match u {
111        FuncUnit::DivS => 0,
112        FuncUnit::ModS => 1,
113        FuncUnit::FloorDivS => 2,
114        FuncUnit::Pow => 3,
115        FuncUnit::MulHiU => 4,
116        FuncUnit::FAddF => 5,
117        FuncUnit::FSubF => 6,
118        FuncUnit::FMulF => 7,
119        FuncUnit::FDivF => 8,
120        FuncUnit::FSqrtF => 9,
121        FuncUnit::FFloorF => 10,
122        FuncUnit::ApproxEqF => 11,
123        FuncUnit::MulOvfS => 12,
124        FuncUnit::IntToF => 13,
125        FuncUnit::FLtF => 14,
126        FuncUnit::FGtF => 15,
127        FuncUnit::FLeF => 16,
128        FuncUnit::FGeF => 17,
129        FuncUnit::FEqF => 18,
130        FuncUnit::FNeF => 19,
131        FuncUnit::PopCount => 20,
132    }
133}
134
135/// Does a `Func` cell with this code run on the FPU lane (`WITH_FPU`)? The float units (add/sub/mul/div/sqrt/
136/// floor, the IEEE compares, int→float, approx-eq) exist only when the engine is built with the FPU, and so does
137/// popcount (it rides the FPU's combinational lane; the FPU-less `MUL_FUNC_F` answers 0 for it). An image built
138/// `WITH_FPU(0)` has no datapath for any of these and must not be handed such a program. The integer units —
139/// div/mod/floor-div/pow and the DSP multiplies — run on every image.
140pub fn func_code_needs_fpu(code: u8) -> bool {
141    matches!(code, 5..=11 | 13..=20)
142}
143
144fn func_from_code(c: u8) -> FuncUnit {
145    match c {
146        0 => FuncUnit::DivS,
147        1 => FuncUnit::ModS,
148        2 => FuncUnit::FloorDivS,
149        3 => FuncUnit::Pow,
150        4 => FuncUnit::MulHiU,
151        5 => FuncUnit::FAddF,
152        6 => FuncUnit::FSubF,
153        7 => FuncUnit::FMulF,
154        8 => FuncUnit::FDivF,
155        9 => FuncUnit::FSqrtF,
156        10 => FuncUnit::FFloorF,
157        11 => FuncUnit::ApproxEqF,
158        12 => FuncUnit::MulOvfS,
159        13 => FuncUnit::IntToF,
160        14 => FuncUnit::FLtF,
161        15 => FuncUnit::FGtF,
162        16 => FuncUnit::FLeF,
163        17 => FuncUnit::FGeF,
164        18 => FuncUnit::FEqF,
165        19 => FuncUnit::FNeF,
166        _ => FuncUnit::PopCount,
167    }
168}
169
170/// The Verilog `func_f` — the whole-program engine's FUNCTIONAL-UNIT evaluator, shared by the specialized
171/// (`emit_wprog_verilog`) and the generic config-driven (`emit_wprog_engine_verilog`) emitters so the two can
172/// never drift. Integer division/mod/floor-div/power are pure `i64`; the FLOAT LANE (codes 5–19) is a
173/// BEHAVIORAL FPU using iverilog's real support (`$bitstoreal`/`$realtobits`/`$sqrt`/`$floor`) — bit-identical
174/// to [`FuncUnit::eval`] in simulation, so a float program's image runs on the engine under iverilog with a real
175/// cycle count. (A SYNTHESIZABLE multi-cycle FPU — the `fpu.v` datapath the decision-fabric float lane already
176/// binds — is the hardware follow-on; this behavioral model proves the sequencing + the result, not the gates.)
177/// The `op` selector is 8 bits (float-compare codes reach 19), so callers must pass the full code, not `[3:0]`.
178fn func_f_verilog() -> &'static str {
179    r#"  function signed [63:0] func_f(input [7:0] op, input signed [63:0] a, input signed [63:0] x);
180    real ra, rx; integer pk;
181    begin
182      ra = $bitstoreal(a); rx = $bitstoreal(x);
183      case (op)
184        8'd0: func_f = (x==0) ? 64'sd0 : (a / x);
185        8'd1: func_f = (x==0) ? 64'sd0 : (a % x);
186        8'd2: func_f = (x==0) ? 64'sd0 : (((a % x) != 0) && ((a < 0) != (x < 0)) ? (a / x) - 64'sd1 : (a / x));
187        8'd3: begin func_f = 64'sd1; for (pk=0; pk<x; pk=pk+1) func_f = func_f * a; end
188        8'd5:  func_f = $realtobits(ra + rx);
189        8'd6:  func_f = $realtobits(ra - rx);
190        8'd7:  func_f = $realtobits(ra * rx);
191        8'd8:  func_f = $realtobits(ra / rx);
192        8'd9:  func_f = $realtobits($sqrt(ra));
193        8'd10: func_f = $floor(ra);
194        8'd13: begin ra = a; func_f = $realtobits(ra); end
195        8'd14: func_f = (ra <  rx) ? 64'sd1 : 64'sd0;
196        8'd15: func_f = (ra >  rx) ? 64'sd1 : 64'sd0;
197        8'd16: func_f = (ra <= rx) ? 64'sd1 : 64'sd0;
198        8'd17: func_f = (ra >= rx) ? 64'sd1 : 64'sd0;
199        8'd18: func_f = (ra == rx) ? 64'sd1 : 64'sd0;
200        8'd19: func_f = (ra != rx) ? 64'sd1 : 64'sd0;
201        8'd20: func_f = $countones(a);
202        default: func_f = 64'sd0;
203      endcase
204    end
205  endfunction
206"#
207}
208
209/// The INT→FLOAT promotion of a float op's operand for the CALL-STACK fabric (WCell form): a float op with an
210/// integer operand promotes it through the `IntToF` functional unit (code 13) first, mirroring `program_cfg_fabric`'s
211/// `promote_float_operand`. Returns the operand unchanged when it already carries an `f64` bit pattern.
212fn wfpromote(cells: &mut Vec<WCell>, s: WSrc, is_float: bool) -> WSrc {
213    if is_float {
214        return s;
215    }
216    let zero = WSrc {
217        mode: 1,
218        reference: 0,
219        konst: 0,
220    };
221    let k = cells.len();
222    cells.push(WCell {
223        kind: 2,
224        code: func_code(FuncUnit::IntToF),
225        ops: [s, zero, zero, zero],
226    });
227    WSrc {
228        mode: 2,
229        reference: k as u16,
230        konst: 0,
231    }
232}
233
234/// Mask a value to 32 bits (`& 0xFFFFFFFF`) — the WORD32 wrapping into ℤ/2³² after arithmetic. An `And` ALU cell.
235fn wmask32(cells: &mut Vec<WCell>, s: WSrc) -> WSrc {
236    let zero = WSrc {
237        mode: 1,
238        reference: 0,
239        konst: 0,
240    };
241    let mask = WSrc {
242        mode: 1,
243        reference: 0,
244        konst: 0xFFFF_FFFF,
245    };
246    let k = cells.len();
247    cells.push(WCell {
248        kind: 0,
249        code: 3,
250        ops: [s, mask, zero, zero],
251    }); // code 3 = AluOp::And
252    WSrc {
253        mode: 2,
254        reference: k as u16,
255        konst: 0,
256    }
257}
258
259fn enc_src(s: Src) -> WSrc {
260    match s {
261        Src::In(k) => WSrc {
262            mode: 0,
263            reference: k as u16,
264            konst: 0,
265        },
266        Src::Const(c) => WSrc {
267            mode: 1,
268            reference: 0,
269            konst: c,
270        },
271        Src::Val(j) => WSrc {
272            mode: 2,
273            reference: j as u16,
274            konst: 0,
275        },
276    }
277}
278
279const ZERO_SRC: WSrc = WSrc {
280    mode: 1,
281    reference: 0,
282    konst: 0,
283};
284
285fn enc_cell(c: &LoopCell) -> WCell {
286    match *c {
287        LoopCell::Arith { op, a, b } => {
288            WCell { kind: 0, code: alu_code(op), ops: [enc_src(a), enc_src(b), ZERO_SRC, ZERO_SRC] }
289        }
290        LoopCell::Select { cmp, ca, cb, t, e } => WCell {
291            kind: 1,
292            code: cmp_code(cmp),
293            ops: [enc_src(ca), enc_src(cb), enc_src(t), enc_src(e)],
294        },
295        LoopCell::HeapRead { addr } => {
296            WCell { kind: 3, code: 0, ops: [enc_src(addr), ZERO_SRC, ZERO_SRC, ZERO_SRC] }
297        }
298        LoopCell::HeapWrite { addr, val } => {
299            WCell { kind: 4, code: 0, ops: [enc_src(addr), enc_src(val), ZERO_SRC, ZERO_SRC] }
300        }
301        LoopCell::HeapAlloc { size } => {
302            WCell { kind: 5, code: 0, ops: [enc_src(size), ZERO_SRC, ZERO_SRC, ZERO_SRC] }
303        }
304        LoopCell::Func { unit, a, b } => {
305            WCell { kind: 2, code: func_code(unit), ops: [enc_src(a), enc_src(b), ZERO_SRC, ZERO_SRC] }
306        }
307        ref other => panic!(
308            "wprog: cell {other:?} is outside the current scope (Arith + Select + Func + heap state plane) — \
309             float-lane and loop-sequence cells are the follow-on"
310        ),
311    }
312}
313
314fn enc_block(b: &CfgBlock) -> WBlock {
315    WBlock {
316        livein: b.inputs.clone(),
317        cells: b.body.iter().map(enc_cell).collect(),
318        writes: b.writes.iter().map(|(r, s)| (*r, enc_src(*s))).collect(),
319        term: match &b.term {
320            Term::Jump(t) => WTerm::Jump(*t),
321            Term::Branch {
322                cond,
323                then_blk,
324                else_blk,
325            } => WTerm::Branch {
326                cond: enc_src(*cond),
327                then_blk: *then_blk,
328                else_blk: *else_blk,
329            },
330            Term::Return(s) => WTerm::Return(enc_src(*s)),
331        },
332    }
333}
334
335/// Flatten a whole-function CFG fabric into the fabric's flat config.
336pub fn serialize_program(pf: &ProgramCfgFabric) -> WProgConfig {
337    WProgConfig {
338        n_params: pf.n_params,
339        blocks: pf.blocks.iter().map(enc_block).collect(),
340    }
341}
342
343/// Wrap a single-block if-converted DECISION FABRIC as a one-block flat config, so its cells run on the
344/// CROSSBAR whole-program engine (`eval_wprog_config`). The crossbar datapath has full interconnect — a cell
345/// may read any earlier cell's value directly — so a COMPUTED comparison predicate needs no spatial routing.
346/// This runs constructs the nearest-neighbour placer (`route_loop_body`) cannot place, e.g. a `Set`'s dedup
347/// scan, on the fabric ≡ the VM (the decision fabric's lowering is proven; only the *placement* was the limit).
348pub fn wprog_from_decision_fabric(df: &DecisionFabric) -> WProgConfig {
349    let block = WBlock {
350        livein: (0..df.n_in as u16).collect(),
351        cells: df.cells.iter().map(enc_cell).collect(),
352        writes: Vec::new(),
353        term: WTerm::Return(enc_src(df.result)),
354    };
355    WProgConfig {
356        n_params: df.n_in,
357        blocks: vec![block],
358    }
359}
360
361/// Lift a WHOLE multi-function program (with function CALLS and recursion) to a `WProgConfig` carrying `Call`
362/// terminators — the source→config half of recursion (the frame-stack `eval_wprog_config` is the eval half).
363/// `fn_name` is lifted FIRST so its entry is block 0 (the config's entry); every function shares one global
364/// block index space, and a `Call` targets the callee function's entry block. Correct-first scope: integer
365/// bodies + calls (LoadConst-int / Move / Add·Sub·Mul / AddAssign / Div·Mod·FloorDiv / comparisons / Call /
366/// Return / Jump family) — enough for fib/sumdown/gcd-style recursion; collections-in-recursion is the
367/// follow-on (the heap arms would be added the same way `program_cfg_fabric` has them).
368pub fn program_calls_fabric(src: &str, fn_name: &str) -> WProgConfig {
369    lift_program(src, Some(fn_name))
370}
371
372/// Lift a WHOLE LOGOS PROGRAM from its `## Main`: the code region the compiler emits before the first function
373/// body (terminated by `Halt`), plus every function it transitively calls. Main takes no parameters and its
374/// `Halt` is a top-level `Return 0`; its `Show`s become SHOW cells (kind 10) so the program's OUTPUT — not just a
375/// return value — runs on the engine, ≡ `vm_outcome(src).output` line for line (`program_output`).
376pub fn program_main_fabric(src: &str) -> WProgConfig {
377    lift_program(src, None)
378}
379
380fn lift_program(src: &str, fn_name: Option<&str>) -> WProgConfig {
381    use crate::vm::{Compiler, Constant, Op};
382    use std::collections::{BTreeSet, HashMap};
383
384    if let Some(refusal) = crate::compile::exact_refusal(src, "fabric", "the fabric") {
385        panic!("{refusal}");
386    }
387
388    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
389        let (stmts, _t, _p) = parsed.expect("parse failed");
390        let program = Compiler::compile(stmts, interner).expect("compile failed");
391        let code = &program.code;
392
393        // The entry REGION: a named function, or `## Main` — the code the compiler emits before the first
394        // function body (pass 2a), terminated by `Halt`. Main is not a `CompiledFunction`, so it is described by
395        // its pc range directly; `None` in the function-index slots below means "the Main region".
396        let main_end = program
397            .functions
398            .iter()
399            .map(|f| f.entry_pc)
400            .min()
401            .unwrap_or(code.len());
402        let entry_fi: Option<usize> = fn_name.map(|name| {
403            program
404                .functions
405                .iter()
406                .position(|f| interner.resolve(f.name) == name)
407                .unwrap_or_else(|| panic!("function `{name}` not found"))
408        });
409        // REACHABILITY: lift only functions transitively CALLED from the entry. Dead prelude/stdlib functions
410        // (e.g. an unused math tower — `sqrt`/`complex`/`parseFloat`) must NOT be lowered, or their unsupported
411        // ops would falsely refuse a program that never calls them (the fannkuch phantom-`Sqrt`). `Op::Call`'s
412        // `func` is the callee index directly, so the closure is a plain worklist.
413        let fn_end = |fi: usize| -> usize {
414            let s = program.functions[fi].entry_pc;
415            program
416                .functions
417                .iter()
418                .map(|f| f.entry_pc)
419                .filter(|&e| e > s)
420                .min()
421                .unwrap_or(code.len())
422        };
423        let region_of = |fi: Option<usize>| -> (usize, usize) {
424            match fi {
425                Some(fi) => (program.functions[fi].entry_pc, fn_end(fi)),
426                None => (0, main_end),
427            }
428        };
429        let mut reachable: BTreeSet<usize> = BTreeSet::new();
430        let mut work: Vec<Option<usize>> = vec![entry_fi];
431        let mut main_scanned = false;
432        while let Some(fi) = work.pop() {
433            match fi {
434                Some(i) if !reachable.insert(i) => continue,
435                None if main_scanned => continue,
436                None => main_scanned = true,
437                _ => {}
438            }
439            let (s, e) = region_of(fi);
440            for pc in s..e {
441                if let Op::Call { func, .. } = code[pc] {
442                    work.push(Some(func as usize));
443                }
444            }
445        }
446        // Lift the entry region FIRST (so its entry becomes global block 0), then the reachable rest by entry_pc.
447        let mut order: Vec<Option<usize>> = vec![entry_fi];
448        let mut rest: Vec<usize> = reachable
449            .iter()
450            .copied()
451            .filter(|&i| Some(i) != entry_fi)
452            .collect();
453        rest.sort_by_key(|&fi| program.functions[fi].entry_pc);
454        order.extend(rest.into_iter().map(Some));
455
456        // Per-function block leaders (absolute pcs): the entry, every jump target, the op after a
457        // jump/return, and the op after a CALL (calls end their block, the return point starts a new one).
458        struct Lift {
459            fi: Option<usize>,
460            end_pc: usize,
461            leaders: Vec<usize>,
462        }
463        let mut lifts: Vec<Lift> = Vec::new();
464        for &fi in order.iter() {
465            // Bound this region's code range against ALL functions' entry_pcs (not just the lifted ones), so a
466            // reachable function never swallows a skipped dead function's code that sits between them.
467            let (entry_pc, end_pc) = region_of(fi);
468            let mut leaders: BTreeSet<usize> = BTreeSet::new();
469            leaders.insert(entry_pc);
470            for pc in entry_pc..end_pc {
471                match code[pc] {
472                    Op::Jump { target }
473                    | Op::JumpIfFalse { target, .. }
474                    | Op::JumpIfTrue { target, .. } => {
475                        leaders.insert(target);
476                        if pc + 1 < end_pc {
477                            leaders.insert(pc + 1);
478                        }
479                    }
480                    // `IterNext` is a CONDITIONAL BRANCH (next value → fall through; exhausted → `exit`), so it
481                    // ends its block and both successors are leaders — exactly like a `JumpIfFalse`.
482                    Op::IterNext { exit, .. } => {
483                        leaders.insert(exit);
484                        if pc + 1 < end_pc {
485                            leaders.insert(pc + 1);
486                        }
487                    }
488                    Op::Call { .. } | Op::Return { .. } | Op::ReturnNothing | Op::Halt => {
489                        if pc + 1 < end_pc {
490                            leaders.insert(pc + 1);
491                        }
492                    }
493                    _ => {}
494                }
495            }
496            let leaders: Vec<usize> = leaders
497                .into_iter()
498                .filter(|&l| l >= entry_pc && l < end_pc)
499                .collect();
500            lifts.push(Lift {
501                fi,
502                end_pc,
503                leaders,
504            });
505        }
506
507        // Global block offsets + per-function entry block.
508        let mut offset: Vec<usize> = Vec::new();
509        let mut total = 0usize;
510        for l in &lifts {
511            offset.push(total);
512            total += l.leaders.len();
513        }
514        let mut fn_entry: HashMap<usize, usize> = HashMap::new();
515        for (li, l) in lifts.iter().enumerate() {
516            if let Some(fi) = l.fi {
517                fn_entry.insert(fi, offset[li]);
518            }
519        }
520        // Global block index of the block containing `pc` in lift `li`.
521        let block_of = |li: usize, pc: usize| -> usize {
522            let ls = &lifts[li].leaders;
523            offset[li] + (ls.partition_point(|&l| l <= pc) - 1)
524        };
525
526        let get = |reg: &mut HashMap<u16, WSrc>, inputs: &mut Vec<u16>, r: u16| -> WSrc {
527            if let Some(s) = reg.get(&r) {
528                return *s;
529            }
530            let k = inputs.len();
531            inputs.push(r);
532            let s = WSrc {
533                mode: 0,
534                reference: k as u16,
535                konst: 0,
536            };
537            reg.insert(r, s);
538            s
539        };
540        let record = |wo: &mut Vec<u16>, dst: u16| {
541            if !wo.contains(&dst) {
542                wo.push(dst);
543            }
544        };
545
546        use crate::vm::BoundaryType;
547        let mut blocks: Vec<WBlock> = Vec::new();
548        for (li, l) in lifts.iter().enumerate() {
549            // Per-function FLOAT tracking (registers are per-function under the frame-stack model). Seed it from
550            // the DECLARED param types — a `Float` param is a float register, a `Seq of Float` a float collection
551            // — so float-ness flows across recursive calls WITHOUT propagation (the callee re-derives it). Float
552            // arithmetic then routes through the float functional units, exactly like `program_cfg_fabric`.
553            let mut float_regs: std::collections::HashSet<u16> = std::collections::HashSet::new();
554            let mut float_colls: std::collections::HashSet<u16> = std::collections::HashSet::new();
555            // WORD32 (ℤ/2³²) tracking, mirroring the float lane: a `word32` value in an i64 slot, so arithmetic
556            // must WRAP mod 2³² (mask to 32 bits). Seeded from `Word32` / `Seq of Word32` params. What sha256 needs.
557            let mut w32_regs: std::collections::HashSet<u16> = std::collections::HashSet::new();
558            let mut w32_colls: std::collections::HashSet<u16> = std::collections::HashSet::new();
559            // Main has no parameters; a function's declared types seed the lanes below.
560            let param_types: &[Option<BoundaryType>] = match l.fi {
561                Some(fi) => &program.functions[fi].param_types,
562                None => &[],
563            };
564            for (r, pt) in param_types.iter().enumerate() {
565                match pt {
566                    Some(BoundaryType::Float) => {
567                        float_regs.insert(r as u16);
568                    }
569                    Some(BoundaryType::Word32) => {
570                        w32_regs.insert(r as u16);
571                    }
572                    Some(BoundaryType::Seq(inner)) if matches!(**inner, BoundaryType::Float) => {
573                        float_colls.insert(r as u16);
574                    }
575                    Some(BoundaryType::Seq(inner)) if matches!(**inner, BoundaryType::Word32) => {
576                        w32_colls.insert(r as u16);
577                    }
578                    _ => {}
579                }
580            }
581            // PARAMETER-COLLECTION tracking (value semantics across a call boundary). A `Move` of a collection
582            // that came in as a PARAMETER must DEEP-COPY its heap block (the VM's copy-on-write) — else
583            // `Let mutable next be queens; Push v to next` grows into the CALLER's shared reserved region and
584            // corrupts it (the nqueens divergence). A LOCAL builder Seq is NOT copied on Move: LOGOS keeps such
585            // locals aliased (the COW-exempt in-place-write path heap_sort/mergesort rely on), matching the VM.
586            let mut param_colls: std::collections::HashSet<u16> = std::collections::HashSet::new();
587            for (r, pt) in param_types.iter().enumerate() {
588                if matches!(pt, Some(BoundaryType::Seq(_))) {
589                    param_colls.insert(r as u16);
590                }
591            }
592            // TEXT tracking: registers holding a Text (a char-list `[len, byte0..]` on the heap). Set at a Text
593            // `LoadConst`, at a text `Add` (concat), and at a text `item i of` (char-at); propagated by `Move`.
594            // Routes `Add`→concat, `Index`→char-at, `Eq`→text-equality — mirroring the decision-fabric text lane.
595            let mut text_regs: std::collections::HashSet<u16> = std::collections::HashSet::new();
596            // BOOL tracking: registers holding a 0/1 predicate — a `Bool` literal or a comparison result — so a
597            // `Show` renders `true`/`false` rather than `1`/`0`. Cleared wherever a register takes a non-Bool
598            // value (alongside the float clears below); propagated by `Move`. Only `Show` reads it: the cells a
599            // Bool feeds are unchanged (it is a 0/1 Int on the datapath, exactly as before).
600            let mut bool_regs: std::collections::HashSet<u16> = std::collections::HashSet::new();
601            // ITERATOR (range / for-each) tracking. A range iterator is a heap object `[cursor, limit]`; a Seq
602            // iterator is `[index, coll_ptr]`. Loops are STRUCTURED, so the active iterator for each `IterNext`
603            // is known at lowering — a lowering-time stack (pushed at `IterPrepare`, popped at `IterPop`). Range
604            // iterators reuse the `NewRange` dst register; Seq iterators get a fresh register (their object ptr).
605            let mut range_regs: std::collections::HashSet<u16> = std::collections::HashSet::new();
606            // (is_seq, iterator-object REGISTER).
607            let mut iter_stack: Vec<(bool, u16)> = Vec::new();
608            // A fresh register base for synthesized Seq-iterator object pointers (for-each). Well above any real
609            // register a function uses; each function has its own slot space under the frame stack, so per-function
610            // is fine. (Model-only — the RTL's fixed NREG rejects it via the capacity guard, and no benchmark hits
611            // it since range/for-each loops are otherwise While-shaped.)
612            let mut fresh_reg: u16 = 60000;
613            // A register an op WRITES without READING holds nothing it held before: every lane mark on it is
614            // dropped before the op is lowered, so the op alone decides its kind — a register the VM reuses for
615            // an Int after a Text or a comparison no longer reads as either. A register both read and written
616            // keeps its marks for the op to read.
617            macro_rules! forget_overwritten {
618                ($op:expr) => {{
619                    let mut read = Vec::new();
620                    $op.for_each_read_reg(
621                        |func| {
622                            program
623                                .functions
624                                .get(func as usize)
625                                .map_or(0, |f| f.captures.len())
626                        },
627                        |r| read.push(r),
628                    );
629                    let mut written = Vec::new();
630                    $op.for_each_written_reg(|r| written.push(r));
631                    for r in written.into_iter().filter(|r| !read.contains(r)) {
632                        float_regs.remove(&r);
633                        float_colls.remove(&r);
634                        w32_regs.remove(&r);
635                        w32_colls.remove(&r);
636                        param_colls.remove(&r);
637                        text_regs.remove(&r);
638                        bool_regs.remove(&r);
639                        range_regs.remove(&r);
640                    }
641                }};
642            }
643            for (bk, &bstart) in l.leaders.iter().enumerate() {
644                let bend = if bk + 1 < l.leaders.len() {
645                    l.leaders[bk + 1]
646                } else {
647                    l.end_pc
648                };
649                let ops = &code[bstart..bend];
650                let (body_ops, term_op): (&[Op], Option<Op>) = match ops.last() {
651                    Some(
652                        t @ (Op::Jump { .. }
653                        | Op::JumpIfFalse { .. }
654                        | Op::JumpIfTrue { .. }
655                        | Op::Return { .. }
656                        | Op::ReturnNothing
657                        | Op::Call { .. }
658                        | Op::IterNext { .. }
659                        | Op::Halt),
660                    ) => (&ops[..ops.len() - 1], Some(*t)),
661                    _ => (ops, None),
662                };
663                let mut reg: HashMap<u16, WSrc> = HashMap::new();
664                let mut inputs: Vec<u16> = Vec::new();
665                let mut cells: Vec<WCell> = Vec::new();
666                let mut wo: Vec<u16> = Vec::new();
667                let z = WSrc {
668                    mode: 1,
669                    reference: 0,
670                    konst: 0,
671                };
672                // A prior cell's value (mode 2) / a signed constant (mode 1) — for the heap-cell sequences below.
673                let val = |k: usize| WSrc {
674                    mode: 2,
675                    reference: k as u16,
676                    konst: 0,
677                };
678                let con = |c: i64| WSrc {
679                    mode: 1,
680                    reference: 0,
681                    konst: c,
682                };
683                for op in body_ops {
684                    forget_overwritten!(op);
685                    match op {
686                        Op::LoadConst { dst, idx } => match &program.constants[*idx as usize] {
687                            Constant::Int(n) => {
688                                reg.insert(*dst, WSrc { mode: 1, reference: 0, konst: *n });
689                                float_regs.remove(dst);
690                                bool_regs.remove(dst);
691                                float_colls.remove(dst);
692                                w32_regs.remove(dst);
693                                w32_colls.remove(dst);
694                                text_regs.remove(dst); // register REUSE: a former Text reg now holds an Int
695                                record(&mut wo, *dst);
696                            }
697                            // A Bool rides a scalar slot as 0/1 (exactly as `exec_ops` lowers it).
698                            Constant::Bool(b) => {
699                                reg.insert(*dst, WSrc { mode: 1, reference: 0, konst: i64::from(*b) });
700                                float_regs.remove(dst);
701                                bool_regs.remove(dst);
702                                float_colls.remove(dst);
703                                w32_regs.remove(dst);
704                                w32_colls.remove(dst);
705                                text_regs.remove(dst);
706                                bool_regs.insert(*dst);
707                                record(&mut wo, *dst);
708                            }
709                            // A Float literal → its f64 BIT PATTERN, with `dst` marked float so downstream
710                            // arithmetic routes to the float functional units.
711                            Constant::Float(f) => {
712                                reg.insert(*dst, WSrc { mode: 1, reference: 0, konst: f.to_bits() as i64 });
713                                float_regs.insert(*dst);
714                                float_colls.remove(dst);
715                                text_regs.remove(dst);
716                                record(&mut wo, *dst);
717                            }
718                            // A Text literal → a char-list `[len, byte0, byte1, ...]` on the heap (UTF-8 bytes,
719                            // so `length of` = byte count, matching the VM), and `dst` marked TEXT.
720                            Constant::Text(s) => {
721                                let bytes = s.as_bytes();
722                                let n = bytes.len();
723                                let ka = cells.len();
724                                cells.push(WCell { kind: 5, code: 0, ops: [con(n as i64 + 1), z, z, z] });
725                                let ptr = val(ka);
726                                cells.push(WCell { kind: 4, code: 0, ops: [ptr, con(n as i64), z, z] });
727                                for (i, b) in bytes.iter().enumerate() {
728                                    let kadd = cells.len();
729                                    cells.push(WCell { kind: 0, code: 0, ops: [ptr, con(i as i64 + 1), z, z] });
730                                    cells.push(WCell { kind: 4, code: 0, ops: [val(kadd), con(*b as i64), z, z] });
731                                }
732                                reg.insert(*dst, ptr);
733                                text_regs.insert(*dst);
734                                float_regs.remove(dst);
735                                bool_regs.remove(dst);
736                                w32_regs.remove(dst);
737                                record(&mut wo, *dst);
738                            }
739                            c => panic!("calls fabric: constant {c:?} unsupported (integer-recursion scope)"),
740                        },
741                        Op::Move { dst, src } => {
742                            let s = get(&mut reg, &mut inputs, *src);
743                            // VALUE SEMANTICS: a `Move` of a PARAMETER collection deep-copies its heap block (the
744                            // VM's COW), so a later `Push`/`Set item` on the copy never mutates the caller's Seq.
745                            // A scalar or local-collection Move is a plain register copy (aliasing a local is the
746                            // COW-exempt path the in-place sorts rely on).
747                            if param_colls.contains(src) {
748                                let k = cells.len();
749                                cells.push(WCell { kind: 6, code: 0, ops: [s, z, z, z] }); // deep-copy collection
750                                reg.insert(*dst, val(k));
751                            } else {
752                                reg.insert(*dst, s);
753                            }
754                            // A copy carries float-ness / word32-ness (scalar and collection-element).
755                            if float_regs.contains(src) { float_regs.insert(*dst); } else { float_regs.remove(dst); }
756                            if float_colls.contains(src) { float_colls.insert(*dst); } else { float_colls.remove(dst); }
757                            if w32_regs.contains(src) { w32_regs.insert(*dst); } else { w32_regs.remove(dst); }
758                            if bool_regs.contains(src) { bool_regs.insert(*dst); } else { bool_regs.remove(dst); }
759                            if w32_colls.contains(src) { w32_colls.insert(*dst); } else { w32_colls.remove(dst); }
760                            if text_regs.contains(src) { text_regs.insert(*dst); } else { text_regs.remove(dst); }
761                            record(&mut wo, *dst);
762                        }
763                        // `RevSwap a, b` exchanges two registers in place: each value, and everything the tracking
764                        // knows of it, moves to the other register. No copy — a parameter collection stays one
765                        // value under its new name. Block writes commit together, so the exchange is simultaneous.
766                        Op::RevSwap { a, b } => {
767                            let va = get(&mut reg, &mut inputs, *a);
768                            let vb = get(&mut reg, &mut inputs, *b);
769                            reg.insert(*a, vb);
770                            reg.insert(*b, va);
771                            for set in [
772                                &mut float_regs,
773                                &mut float_colls,
774                                &mut w32_regs,
775                                &mut w32_colls,
776                                &mut param_colls,
777                                &mut text_regs,
778                                &mut bool_regs,
779                                &mut range_regs,
780                            ] {
781                                let (ha, hb) = (set.contains(a), set.contains(b));
782                                if ha != hb {
783                                    let (from, to) = if ha { (*a, *b) } else { (*b, *a) };
784                                    set.remove(&from);
785                                    set.insert(to);
786                                }
787                            }
788                            record(&mut wo, *a);
789                            record(&mut wo, *b);
790                        }
791                        Op::Add { dst, lhs, rhs }
792                        | Op::Sub { dst, lhs, rhs }
793                        | Op::Mul { dst, lhs, rhs } => {
794                            let a = get(&mut reg, &mut inputs, *lhs);
795                            let b = get(&mut reg, &mut inputs, *rhs);
796                            let (lt, rt) = (text_regs.contains(lhs), text_regs.contains(rhs));
797                            // TEXT CONCAT: `text + x` / `x + text` (only `Add`) builds a fresh char-list, rendering
798                            // a non-text operand to its decimal bytes (the VM's `value + text` coercion).
799                            if (lt || rt) && matches!(op, Op::Add { .. }) {
800                                let code = (u8::from(lt)) | (u8::from(rt) << 1);
801                                let k = cells.len();
802                                cells.push(WCell { kind: 7, code, ops: [a, b, z, z] });
803                                reg.insert(*dst, val(k));
804                                text_regs.insert(*dst);
805                                float_regs.remove(dst);
806                                bool_regs.remove(dst);
807                                w32_regs.remove(dst);
808                                record(&mut wo, *dst);
809                                continue;
810                            }
811                            let (lf, rf) = (float_regs.contains(lhs), float_regs.contains(rhs));
812                            let (lw, rw) = (w32_regs.contains(lhs), w32_regs.contains(rhs));
813                            let cc = match op {
814                                Op::Add { .. } => 0,
815                                Op::Sub { .. } => 1,
816                                _ => 2,
817                            };
818                            if lf || rf {
819                                let a2 = wfpromote(&mut cells, a, lf);
820                                let b2 = wfpromote(&mut cells, b, rf);
821                                let fc = func_code(match op {
822                                    Op::Add { .. } => FuncUnit::FAddF,
823                                    Op::Sub { .. } => FuncUnit::FSubF,
824                                    _ => FuncUnit::FMulF,
825                                });
826                                let k = cells.len();
827                                cells.push(WCell { kind: 2, code: fc, ops: [a2, b2, z, z] });
828                                reg.insert(*dst, WSrc { mode: 2, reference: k as u16, konst: 0 });
829                                float_regs.insert(*dst);
830                                float_colls.remove(dst);
831                                w32_regs.remove(dst);
832                            } else if lw || rw {
833                                // WORD32 arithmetic wraps mod 2³²: the raw ALU op, then a 32-bit mask.
834                                let k = cells.len();
835                                cells.push(WCell { kind: 0, code: cc, ops: [a, b, z, z] });
836                                let masked = wmask32(&mut cells, WSrc { mode: 2, reference: k as u16, konst: 0 });
837                                reg.insert(*dst, masked);
838                                w32_regs.insert(*dst);
839                                float_regs.remove(dst);
840                                bool_regs.remove(dst);
841                                float_colls.remove(dst);
842                                w32_colls.remove(dst);
843                            } else {
844                                let k = cells.len();
845                                cells.push(WCell { kind: 0, code: cc, ops: [a, b, z, z] });
846                                reg.insert(*dst, WSrc { mode: 2, reference: k as u16, konst: 0 });
847                                float_regs.remove(dst);
848                                bool_regs.remove(dst);
849                                float_colls.remove(dst);
850                                w32_regs.remove(dst);
851                            }
852                            text_regs.remove(dst); // an int/float/word32 arithmetic result is not Text
853                            record(&mut wo, *dst);
854                        }
855                        Op::AddAssign { dst, src } => {
856                            let a = get(&mut reg, &mut inputs, *dst);
857                            let b = get(&mut reg, &mut inputs, *src);
858                            let (dt, st) = (text_regs.contains(dst), text_regs.contains(src));
859                            // TEXT CONCAT in place: `result += x` where `result` is Text (the `Set result to result
860                            // + …` string builder) — append `x`'s chars (a non-text `x` is rendered to decimal).
861                            if dt || st {
862                                let code = (u8::from(dt)) | (u8::from(st) << 1);
863                                let k = cells.len();
864                                cells.push(WCell { kind: 7, code, ops: [a, b, z, z] });
865                                reg.insert(*dst, val(k));
866                                text_regs.insert(*dst);
867                                float_regs.remove(dst);
868                                bool_regs.remove(dst);
869                                w32_regs.remove(dst);
870                                record(&mut wo, *dst);
871                                continue;
872                            }
873                            let (df, sf) = (float_regs.contains(dst), float_regs.contains(src));
874                            if df || sf {
875                                let a2 = wfpromote(&mut cells, a, df);
876                                let b2 = wfpromote(&mut cells, b, sf);
877                                let k = cells.len();
878                                cells.push(WCell { kind: 2, code: func_code(FuncUnit::FAddF), ops: [a2, b2, z, z] });
879                                reg.insert(*dst, WSrc { mode: 2, reference: k as u16, konst: 0 });
880                                float_regs.insert(*dst);
881                                float_colls.remove(dst);
882                            } else {
883                                let k = cells.len();
884                                cells.push(WCell { kind: 0, code: 0, ops: [a, b, z, z] });
885                                reg.insert(*dst, WSrc { mode: 2, reference: k as u16, konst: 0 });
886                                float_regs.remove(dst);
887                                bool_regs.remove(dst);
888                                float_colls.remove(dst);
889                            }
890                            text_regs.remove(dst); // an int/float `+=` result is not Text
891                            record(&mut wo, *dst);
892                        }
893                        Op::Div { dst, lhs, rhs }
894                        | Op::Mod { dst, lhs, rhs }
895                        | Op::FloorDiv { dst, lhs, rhs } => {
896                            let a = get(&mut reg, &mut inputs, *lhs);
897                            let b = get(&mut reg, &mut inputs, *rhs);
898                            let (lf, rf) = (float_regs.contains(lhs), float_regs.contains(rhs));
899                            if lf || rf {
900                                let a2 = wfpromote(&mut cells, a, lf);
901                                let b2 = wfpromote(&mut cells, b, rf);
902                                let k = cells.len();
903                                cells.push(WCell { kind: 2, code: func_code(FuncUnit::FDivF), ops: [a2, b2, z, z] });
904                                reg.insert(*dst, WSrc { mode: 2, reference: k as u16, konst: 0 });
905                                float_regs.insert(*dst);
906                                float_colls.remove(dst);
907                            } else {
908                                let cc = match op {
909                                    Op::Mod { .. } => 1,
910                                    Op::FloorDiv { .. } => 2,
911                                    _ => 0,
912                                };
913                                let k = cells.len();
914                                cells.push(WCell { kind: 2, code: cc, ops: [a, b, z, z] });
915                                reg.insert(*dst, WSrc { mode: 2, reference: k as u16, konst: 0 });
916                                float_regs.remove(dst);
917                                bool_regs.remove(dst);
918                                float_colls.remove(dst);
919                            }
920                            record(&mut wo, *dst);
921                        }
922                        Op::Lt { dst, lhs, rhs }
923                        | Op::Gt { dst, lhs, rhs }
924                        | Op::LtEq { dst, lhs, rhs }
925                        | Op::GtEq { dst, lhs, rhs }
926                        | Op::Eq { dst, lhs, rhs }
927                        | Op::NotEq { dst, lhs, rhs } => {
928                            let a = get(&mut reg, &mut inputs, *lhs);
929                            let b = get(&mut reg, &mut inputs, *rhs);
930                            // TEXT EQUALITY: `text == text` / `text != text` compares char-lists byte-for-byte
931                            // (only `Eq`/`NotEq` apply to Text). Yields the 0/1 Int predicate.
932                            if (text_regs.contains(lhs) || text_regs.contains(rhs))
933                                && matches!(op, Op::Eq { .. } | Op::NotEq { .. })
934                            {
935                                let code = if matches!(op, Op::Eq { .. }) { 0 } else { 1 };
936                                let k = cells.len();
937                                cells.push(WCell { kind: 9, code, ops: [a, b, z, z] });
938                                reg.insert(*dst, val(k));
939                                float_regs.remove(dst);
940                                bool_regs.remove(dst);
941                                w32_regs.remove(dst);
942                                text_regs.remove(dst);
943                                bool_regs.insert(*dst);
944                                record(&mut wo, *dst);
945                                continue;
946                            }
947                            let (lf, rf) = (float_regs.contains(lhs), float_regs.contains(rhs));
948                            if lf || rf {
949                                // A genuine IEEE-754 float comparison (float-compare functional units), promoting
950                                // an integer operand first. Yields the 0/1 predicate (an Int).
951                                let a2 = wfpromote(&mut cells, a, lf);
952                                let b2 = wfpromote(&mut cells, b, rf);
953                                let fc = func_code(match op {
954                                    Op::Lt { .. } => FuncUnit::FLtF,
955                                    Op::Gt { .. } => FuncUnit::FGtF,
956                                    Op::LtEq { .. } => FuncUnit::FLeF,
957                                    Op::GtEq { .. } => FuncUnit::FGeF,
958                                    Op::Eq { .. } => FuncUnit::FEqF,
959                                    _ => FuncUnit::FNeF,
960                                });
961                                let k = cells.len();
962                                cells.push(WCell { kind: 2, code: fc, ops: [a2, b2, z, z] });
963                                reg.insert(*dst, WSrc { mode: 2, reference: k as u16, konst: 0 });
964                            } else {
965                                let cc = match op {
966                                    Op::Lt { .. } => 0,
967                                    Op::Gt { .. } => 1,
968                                    Op::LtEq { .. } => 2,
969                                    Op::GtEq { .. } => 3,
970                                    Op::Eq { .. } => 4,
971                                    _ => 5,
972                                };
973                                let k = cells.len();
974                                let one = WSrc { mode: 1, reference: 0, konst: 1 };
975                                cells.push(WCell { kind: 1, code: cc, ops: [a, b, one, z] });
976                                reg.insert(*dst, WSrc { mode: 2, reference: k as u16, konst: 0 });
977                            }
978                            float_regs.remove(dst); // a 0/1 predicate is an Int on the datapath …
979                            text_regs.remove(dst);
980                            float_colls.remove(dst);
981                            w32_regs.remove(dst);
982                            w32_colls.remove(dst);
983                            bool_regs.insert(*dst); // … and a Bool to `Show`
984                            record(&mut wo, *dst);
985                        }
986                        // bitwise / shift → the same combinational ALU codes (And=3/Or=4/Xor=5/Shl=6/Shr=7);
987                        // `~x` already lowers to `x ^ -1` upstream, so no separate NOT is needed. This is what
988                        // `nqueens`'s bitmask recursion needs.
989                        Op::BitAnd { dst, lhs, rhs }
990                        | Op::BitOr { dst, lhs, rhs }
991                        | Op::BitXor { dst, lhs, rhs }
992                        | Op::Shl { dst, lhs, rhs }
993                        | Op::Shr { dst, lhs, rhs } => {
994                            let a = get(&mut reg, &mut inputs, *lhs);
995                            let b = get(&mut reg, &mut inputs, *rhs);
996                            let cc = match op {
997                                Op::BitAnd { .. } => 3,
998                                Op::BitOr { .. } => 4,
999                                Op::BitXor { .. } => 5,
1000                                Op::Shl { .. } => 6,
1001                                _ => 7,
1002                            };
1003                            let w32 = w32_regs.contains(lhs) || w32_regs.contains(rhs);
1004                            let k = cells.len();
1005                            cells.push(WCell { kind: 0, code: cc, ops: [a, b, z, z] });
1006                            // WORD32 propagation: xor/and/or of 32-bit values stay 32-bit; a left shift can carry
1007                            // past bit 31, so it re-masks.
1008                            let out = if w32 && matches!(op, Op::Shl { .. }) {
1009                                wmask32(&mut cells, WSrc { mode: 2, reference: k as u16, konst: 0 })
1010                            } else {
1011                                WSrc { mode: 2, reference: k as u16, konst: 0 }
1012                            };
1013                            reg.insert(*dst, out);
1014                            float_regs.remove(dst);
1015                            bool_regs.remove(dst);
1016                            float_colls.remove(dst);
1017                            if w32 { w32_regs.insert(*dst); } else { w32_regs.remove(dst); }
1018                            record(&mut wo, *dst);
1019                        }
1020                        // ── collections on the shared heap. The heap is GLOBAL across call frames (matching the
1021                        // VM's single heap), so an array built before a recursive call and mutated IN PLACE
1022                        // through it (quicksort/heap_sort — one array threaded by pointer) runs. Same `[len,
1023                        // e1..eN]` layout (1-based) as the straight-line fabric. Allocating a FRESH collection
1024                        // inside the recursion (mergesort's left/right/result) hits the multi-grow-Seq heap
1025                        // limit — that stays the follow-on.
1026                        Op::Length { dst, collection } => {
1027                            let ptr = get(&mut reg, &mut inputs, *collection);
1028                            let k = cells.len();
1029                            cells.push(WCell { kind: 3, code: 0, ops: [ptr, z, z, z] });
1030                            reg.insert(*dst, val(k));
1031                            float_regs.remove(dst); // a length is an Int
1032                            text_regs.remove(dst);
1033                            float_colls.remove(dst);
1034                            w32_regs.remove(dst);
1035                            w32_colls.remove(dst);
1036                            record(&mut wo, *dst);
1037                        }
1038                        Op::Index { dst, collection, index }
1039                        | Op::IndexUnchecked { dst, collection, index } => {
1040                            let ptr = get(&mut reg, &mut inputs, *collection);
1041                            let idx = get(&mut reg, &mut inputs, *index);
1042                            // `item i of TEXT` → a fresh 1-char Text (the VM returns a 1-char string, compared by
1043                            // text-equality), NOT the raw byte.
1044                            if text_regs.contains(collection) {
1045                                let k = cells.len();
1046                                cells.push(WCell { kind: 8, code: 0, ops: [ptr, idx, z, z] });
1047                                reg.insert(*dst, val(k));
1048                                text_regs.insert(*dst);
1049                                float_regs.remove(dst);
1050                                bool_regs.remove(dst);
1051                                w32_regs.remove(dst);
1052                                record(&mut wo, *dst);
1053                                continue;
1054                            }
1055                            let ka = cells.len();
1056                            cells.push(WCell { kind: 0, code: 0, ops: [ptr, idx, z, z] });
1057                            let kr = cells.len();
1058                            cells.push(WCell { kind: 3, code: 0, ops: [val(ka), z, z, z] });
1059                            reg.insert(*dst, val(kr));
1060                            // `item i of v` off a float / word32 Seq carries that element type.
1061                            if float_colls.contains(collection) { float_regs.insert(*dst); } else { float_regs.remove(dst); }
1062                            if w32_colls.contains(collection) { w32_regs.insert(*dst); } else { w32_regs.remove(dst); }
1063                            float_colls.remove(dst);
1064                            w32_colls.remove(dst);
1065                            record(&mut wo, *dst);
1066                        }
1067                        Op::SetIndex { collection, index, value }
1068                        | Op::SetIndexUnchecked { collection, index, value } => {
1069                            if float_regs.contains(value) { float_colls.insert(*collection); }
1070                            if w32_regs.contains(value) { w32_colls.insert(*collection); }
1071                            let ptr = get(&mut reg, &mut inputs, *collection);
1072                            let idx = get(&mut reg, &mut inputs, *index);
1073                            let v = get(&mut reg, &mut inputs, *value);
1074                            let ka = cells.len();
1075                            cells.push(WCell { kind: 0, code: 0, ops: [ptr, idx, z, z] });
1076                            cells.push(WCell { kind: 4, code: 0, ops: [val(ka), v, z, z] });
1077                        }
1078                        // `args()`: argv, program name first, as on every tier (`semantics::argv_with_program_name`).
1079                        // The engine has no host argv, so the list is the program name alone — `[1, name]`, the name
1080                        // a heap Text laid out as a Text literal is, in a collection block with its growth reserve.
1081                        Op::Args { dst } => {
1082                            let bytes = crate::semantics::PROGRAM_NAME.as_bytes();
1083                            let kt = cells.len();
1084                            cells.push(WCell { kind: 5, code: 0, ops: [con(bytes.len() as i64 + 1), z, z, z] });
1085                            let name = val(kt);
1086                            cells.push(WCell { kind: 4, code: 0, ops: [name, con(bytes.len() as i64), z, z] });
1087                            for (i, b) in bytes.iter().enumerate() {
1088                                let kadd = cells.len();
1089                                cells.push(WCell { kind: 0, code: 0, ops: [name, con(i as i64 + 1), z, z] });
1090                                cells.push(WCell { kind: 4, code: 0, ops: [val(kadd), con(*b as i64), z, z] });
1091                            }
1092                            let ka = cells.len();
1093                            cells.push(WCell {
1094                                kind: 5,
1095                                code: 0,
1096                                ops: [con(1 + crate::codegen_sva::fabric_rtl::SEQ_RESERVE), z, z, z],
1097                            });
1098                            let ptr = val(ka);
1099                            cells.push(WCell { kind: 4, code: 0, ops: [ptr, con(1), z, z] });
1100                            let kadd = cells.len();
1101                            cells.push(WCell { kind: 0, code: 0, ops: [ptr, con(1), z, z] });
1102                            cells.push(WCell { kind: 4, code: 0, ops: [val(kadd), name, z, z] });
1103                            reg.insert(*dst, ptr);
1104                            text_regs.remove(dst);
1105                            float_regs.remove(dst);
1106                            float_colls.remove(dst);
1107                            bool_regs.remove(dst);
1108                            w32_regs.remove(dst);
1109                            record(&mut wo, *dst);
1110                        }
1111                        Op::NewEmptyList { dst }
1112                        | Op::NewEmptyListWithCapacity { dst, .. }
1113                        | Op::NewEmptyListFixed { dst, .. }
1114                        | Op::NewEmptyListI32 { dst }
1115                        | Op::NewEmptySet { dst }
1116                        | Op::NewEmptyMap { dst }
1117                        | Op::NewEmptyMapWithCapacity { dst, .. } => {
1118                            // Reserve the collection's own capacity block (see `SEQ_RESERVE`), so an array built
1119                            // and grown across recursive frames doesn't collide with a sibling collection.
1120                            let ka = cells.len();
1121                            cells.push(WCell {
1122                                kind: 5,
1123                                code: 0,
1124                                ops: [con(1 + crate::codegen_sva::fabric_rtl::SEQ_RESERVE), z, z, z],
1125                            });
1126                            let ptr = val(ka);
1127                            cells.push(WCell { kind: 4, code: 0, ops: [ptr, con(0), z, z] });
1128                            reg.insert(*dst, ptr);
1129                            record(&mut wo, *dst);
1130                        }
1131                        Op::NewList { dst, start, count } | Op::NewTuple { dst, start, count } => {
1132                            let n = *count;
1133                            if (0..n).any(|j| float_regs.contains(&(*start + j))) {
1134                                float_colls.insert(*dst);
1135                            } else {
1136                                float_colls.remove(dst);
1137                            }
1138                            if (0..n).any(|j| w32_regs.contains(&(*start + j))) {
1139                                w32_colls.insert(*dst);
1140                            } else {
1141                                w32_colls.remove(dst);
1142                            }
1143                            w32_regs.remove(dst);
1144                            let ka = cells.len();
1145                            cells.push(WCell { kind: 5, code: 0, ops: [con(n as i64 + 1), z, z, z] });
1146                            let ptr = val(ka);
1147                            cells.push(WCell { kind: 4, code: 0, ops: [ptr, con(n as i64), z, z] });
1148                            for j in 0..n {
1149                                let elem = get(&mut reg, &mut inputs, *start + j);
1150                                let kadd = cells.len();
1151                                cells.push(WCell { kind: 0, code: 0, ops: [ptr, con(j as i64 + 1), z, z] });
1152                                cells.push(WCell { kind: 4, code: 0, ops: [val(kadd), elem, z, z] });
1153                            }
1154                            reg.insert(*dst, ptr);
1155                            float_regs.remove(dst);
1156                            bool_regs.remove(dst);
1157                            record(&mut wo, *dst);
1158                        }
1159                        Op::DestructureTuple { src, start, count } => {
1160                            // The inverse of `NewTuple`: spread a tuple `heap[ptr] = len; heap[ptr+1..=ptr+len]`
1161                            // into consecutive result registers `start..start+count` — element j is
1162                            // `heap[ptr + j + 1]`. The source surface is `Repeat for (a, b) in list`.
1163                            let ptr = get(&mut reg, &mut inputs, *src);
1164                            for j in 0..*count {
1165                                let kadd = cells.len();
1166                                cells.push(WCell { kind: 0, code: 0, ops: [ptr, con(j as i64 + 1), z, z] });
1167                                let kr = cells.len();
1168                                cells.push(WCell { kind: 3, code: 0, ops: [val(kadd), z, z, z] });
1169                                reg.insert(*start + j, val(kr));
1170                                float_regs.remove(&(*start + j));
1171                                w32_regs.remove(&(*start + j));
1172                                record(&mut wo, *start + j);
1173                            }
1174                        }
1175                        Op::ListPush { list, value } => {
1176                            if float_regs.contains(value) { float_colls.insert(*list); }
1177                            if w32_regs.contains(value) { w32_colls.insert(*list); }
1178                            let ptr = get(&mut reg, &mut inputs, *list);
1179                            let v = get(&mut reg, &mut inputs, *value);
1180                            let klen = cells.len();
1181                            cells.push(WCell { kind: 3, code: 0, ops: [ptr, z, z, z] });
1182                            let len = val(klen);
1183                            let kp1 = cells.len();
1184                            cells.push(WCell { kind: 0, code: 0, ops: [ptr, con(1), z, z] });
1185                            let kaddr = cells.len();
1186                            cells.push(WCell { kind: 0, code: 0, ops: [val(kp1), len, z, z] });
1187                            cells.push(WCell { kind: 4, code: 0, ops: [val(kaddr), v, z, z] });
1188                            let knl = cells.len();
1189                            cells.push(WCell { kind: 0, code: 0, ops: [len, con(1), z, z] });
1190                            cells.push(WCell { kind: 4, code: 0, ops: [ptr, val(knl), z, z] });
1191                        }
1192                        Op::ListPop { list, dst } => {
1193                            let ptr = get(&mut reg, &mut inputs, *list);
1194                            let klen = cells.len();
1195                            cells.push(WCell { kind: 3, code: 0, ops: [ptr, z, z, z] });
1196                            let len = val(klen);
1197                            let kaddr = cells.len();
1198                            cells.push(WCell { kind: 0, code: 0, ops: [ptr, len, z, z] });
1199                            let kr = cells.len();
1200                            cells.push(WCell { kind: 3, code: 0, ops: [val(kaddr), z, z, z] });
1201                            let kdec = cells.len();
1202                            cells.push(WCell { kind: 0, code: 1, ops: [len, con(1), z, z] });
1203                            cells.push(WCell { kind: 4, code: 0, ops: [ptr, val(kdec), z, z] });
1204                            reg.insert(*dst, val(kr));
1205                            if float_colls.contains(list) { float_regs.insert(*dst); } else { float_regs.remove(dst); }
1206                            if w32_colls.contains(list) { w32_regs.insert(*dst); } else { w32_regs.remove(dst); }
1207                            float_colls.remove(dst);
1208                            w32_colls.remove(dst);
1209                            record(&mut wo, *dst);
1210                        }
1211                        // `floor`/`sqrt` over a float register run on the float functional units (the float→int
1212                        // bridge for `floor`, float→float for `sqrt`) — what spectral_norm/nbody-in-recursion need.
1213                        Op::CallBuiltin { dst, builtin, args_start, .. } => {
1214                            use crate::semantics::builtins::BuiltinId;
1215                            let a0 = get(&mut reg, &mut inputs, *args_start);
1216                            let is_f = float_regs.contains(args_start);
1217                            let mut mk_w32 = false; // does this builtin PRODUCE a word32?
1218                            match builtin {
1219                                BuiltinId::Sqrt if is_f => {
1220                                    let k = cells.len();
1221                                    cells.push(WCell { kind: 2, code: func_code(FuncUnit::FSqrtF), ops: [a0, z, z, z] });
1222                                    reg.insert(*dst, val(k));
1223                                    float_regs.insert(*dst);
1224                                }
1225                                BuiltinId::Floor if is_f => {
1226                                    let k = cells.len();
1227                                    cells.push(WCell { kind: 2, code: func_code(FuncUnit::FFloorF), ops: [a0, z, z, z] });
1228                                    reg.insert(*dst, val(k));
1229                                    float_regs.remove(dst);
1230                                    bool_regs.remove(dst);
1231                                }
1232                                BuiltinId::Floor => {
1233                                    reg.insert(*dst, a0);
1234                                    float_regs.remove(dst);
1235                                    bool_regs.remove(dst);
1236                                }
1237                                // WORD32 construction: `word32(n) = n & 0xFFFFFFFF`.
1238                                BuiltinId::Word32 => {
1239                                    let m = wmask32(&mut cells, a0);
1240                                    reg.insert(*dst, m);
1241                                    mk_w32 = true;
1242                                }
1243                                // `rotr(w, n) = ((w >> n) | (w << (32 - n))) & 0xFFFFFFFF` (logical right-rotate).
1244                                BuiltinId::Rotr => {
1245                                    let n = get(&mut reg, &mut inputs, *args_start + 1);
1246                                    let ksh1 = cells.len();
1247                                    cells.push(WCell { kind: 0, code: 7, ops: [a0, n, z, z] }); // w >> n
1248                                    let kcomp = cells.len();
1249                                    cells.push(WCell { kind: 0, code: 1, ops: [con(32), n, z, z] }); // 32 - n
1250                                    let ksh2 = cells.len();
1251                                    cells.push(WCell { kind: 0, code: 6, ops: [a0, val(kcomp), z, z] }); // w << (32-n)
1252                                    let kor = cells.len();
1253                                    cells.push(WCell { kind: 0, code: 4, ops: [val(ksh1), val(ksh2), z, z] }); // |
1254                                    let m = wmask32(&mut cells, val(kor));
1255                                    reg.insert(*dst, m);
1256                                    mk_w32 = true;
1257                                }
1258                                // `word32Shr(w, n) = w >> n` (w is a 32-bit non-negative value → logical == signed).
1259                                BuiltinId::Word32Shr => {
1260                                    let n = get(&mut reg, &mut inputs, *args_start + 1);
1261                                    let k = cells.len();
1262                                    cells.push(WCell { kind: 0, code: 7, ops: [a0, n, z, z] });
1263                                    reg.insert(*dst, val(k));
1264                                    mk_w32 = true;
1265                                }
1266                                // `intOfWord32(w) = w` — the unsigned value as an Int (identity; `dst` is an Int).
1267                                BuiltinId::IntOfWord32 => {
1268                                    reg.insert(*dst, a0);
1269                                }
1270                                // WORD32 bitwise: `word_and`/`word_or` stay 32-bit (no re-mask); `word_not = ~a &
1271                                // 0xFFFFFFFF` (the 32-bit complement).
1272                                BuiltinId::Wand => {
1273                                    let b1 = get(&mut reg, &mut inputs, *args_start + 1);
1274                                    let k = cells.len();
1275                                    cells.push(WCell { kind: 0, code: 3, ops: [a0, b1, z, z] });
1276                                    reg.insert(*dst, val(k));
1277                                    mk_w32 = true;
1278                                }
1279                                BuiltinId::Wor => {
1280                                    let b1 = get(&mut reg, &mut inputs, *args_start + 1);
1281                                    let k = cells.len();
1282                                    cells.push(WCell { kind: 0, code: 4, ops: [a0, b1, z, z] });
1283                                    reg.insert(*dst, val(k));
1284                                    mk_w32 = true;
1285                                }
1286                                BuiltinId::Wnot => {
1287                                    let kx = cells.len();
1288                                    cells.push(WCell { kind: 0, code: 5, ops: [a0, con(-1), z, z] }); // a ^ -1
1289                                    let m = wmask32(&mut cells, val(kx));
1290                                    reg.insert(*dst, m);
1291                                    mk_w32 = true;
1292                                }
1293                                // FLOAT abs: clear the IEEE-754 sign bit (`a & 0x7FFF_FFFF_FFFF_FFFF`).
1294                                BuiltinId::Abs if is_f => {
1295                                    let k = cells.len();
1296                                    cells.push(WCell { kind: 0, code: 3, ops: [a0, con(0x7FFF_FFFF_FFFF_FFFF), z, z] });
1297                                    reg.insert(*dst, val(k));
1298                                    float_regs.insert(*dst);
1299                                }
1300                                // INTEGER abs: `a < 0 ? (0 - a) : a` — a negate cell feeding a select.
1301                                BuiltinId::Abs => {
1302                                    let kneg = cells.len();
1303                                    cells.push(WCell { kind: 0, code: 1, ops: [con(0), a0, z, z] }); // 0 - a
1304                                    let ksel = cells.len();
1305                                    cells.push(WCell { kind: 1, code: 0, ops: [a0, con(0), val(kneg), a0] }); // a<0 ? -a : a
1306                                    reg.insert(*dst, val(ksel));
1307                                    float_regs.remove(dst);
1308                                    bool_regs.remove(dst);
1309                                }
1310                                other => panic!(
1311                                    "calls fabric: builtin `{other:?}` unsupported (float/word32 lanes wired: \
1312                                     sqrt/floor/word32/rotr/word32Shr/intOfWord32)"
1313                                ),
1314                            }
1315                            float_colls.remove(dst);
1316                            w32_colls.remove(dst);
1317                            if mk_w32 { w32_regs.insert(*dst); } else { w32_regs.remove(dst); }
1318                            record(&mut wo, *dst);
1319                        }
1320                        // ── native range / for-each iterators (the `Repeat for …` loops). NewRange builds a heap
1321                        // range object `[cursor, limit]`; IterPrepare pushes the active iterator (a Seq iterable
1322                        // gets a fresh `[index, coll_ptr]` object); IterNext (the terminator, above) steps it.
1323                        Op::NewRange { dst, start, end } => {
1324                            let s = get(&mut reg, &mut inputs, *start);
1325                            let e = get(&mut reg, &mut inputs, *end);
1326                            let ka = cells.len();
1327                            cells.push(WCell { kind: 5, code: 0, ops: [con(2), z, z, z] }); // alloc [cursor, limit]
1328                            let ptr = val(ka);
1329                            cells.push(WCell { kind: 4, code: 0, ops: [ptr, s, z, z] }); // heap[ptr] = start
1330                            let kp1 = cells.len();
1331                            cells.push(WCell { kind: 0, code: 0, ops: [ptr, con(1), z, z] });
1332                            cells.push(WCell { kind: 4, code: 0, ops: [val(kp1), e, z, z] }); // heap[ptr+1] = end
1333                            reg.insert(*dst, ptr);
1334                            range_regs.insert(*dst);
1335                            record(&mut wo, *dst);
1336                        }
1337                        Op::IterPrepare { iterable } => {
1338                            if range_regs.contains(iterable) {
1339                                iter_stack.push((false, *iterable)); // the NewRange dst IS the range object
1340                            } else {
1341                                // A Seq iterable: build a fresh iterator object `[index=0, coll_ptr]`.
1342                                let coll = get(&mut reg, &mut inputs, *iterable);
1343                                let ka = cells.len();
1344                                cells.push(WCell { kind: 5, code: 0, ops: [con(2), z, z, z] });
1345                                let ptr = val(ka);
1346                                cells.push(WCell { kind: 4, code: 0, ops: [ptr, con(0), z, z] }); // index = 0
1347                                let kp1 = cells.len();
1348                                cells.push(WCell { kind: 0, code: 0, ops: [ptr, con(1), z, z] });
1349                                cells.push(WCell { kind: 4, code: 0, ops: [val(kp1), coll, z, z] }); // coll_ptr
1350                                let ir = fresh_reg;
1351                                fresh_reg += 1;
1352                                reg.insert(ir, ptr);
1353                                record(&mut wo, ir);
1354                                iter_stack.push((true, ir));
1355                            }
1356                        }
1357                        Op::IterPop => {
1358                            iter_stack.pop();
1359                        }
1360                        // `Show v` — a SHOW cell (kind 10): the value flows on as the cell's result, and the engine
1361                        // records/streams it with the type tag the tracking sets assign — the exact per-register
1362                        // type at this point, so a reused register shows as what it holds now.
1363                        Op::Show { src } => {
1364                            let s = get(&mut reg, &mut inputs, *src);
1365                            let tag = if text_regs.contains(src) {
1366                                TAG_TEXT
1367                            } else if float_regs.contains(src) {
1368                                TAG_FLOAT
1369                            } else if w32_regs.contains(src) {
1370                                TAG_WORD32
1371                            } else if bool_regs.contains(src) {
1372                                TAG_BOOL
1373                            } else {
1374                                TAG_INT
1375                            };
1376                            cells.push(WCell { kind: 10, code: tag, ops: [s, z, z, z] });
1377                        }
1378                        other => panic!(
1379                            "calls fabric: op `{}` unsupported (integer-recursion scope — collections-in-\
1380                             recursion is the follow-on)",
1381                            other.name()
1382                        ),
1383                    }
1384                }
1385                if let Some(t) = &term_op {
1386                    forget_overwritten!(t);
1387                }
1388                let term = match term_op {
1389                    Some(Op::Return { src }) => WTerm::Return(get(&mut reg, &mut inputs, src)),
1390                    Some(Op::ReturnNothing) => WTerm::Return(WSrc {
1391                        mode: 1,
1392                        reference: 0,
1393                        konst: 0,
1394                    }),
1395                    // `Halt` ends Main: a top-level return of 0 (the frame stack is empty, so the engine finishes).
1396                    Some(Op::Halt) => WTerm::Return(WSrc {
1397                        mode: 1,
1398                        reference: 0,
1399                        konst: 0,
1400                    }),
1401                    Some(Op::Jump { target }) => WTerm::Jump(block_of(li, target)),
1402                    Some(Op::JumpIfFalse { cond, target }) => {
1403                        let c = get(&mut reg, &mut inputs, cond);
1404                        WTerm::Branch {
1405                            cond: c,
1406                            then_blk: block_of(li, bend),
1407                            else_blk: block_of(li, target),
1408                        }
1409                    }
1410                    Some(Op::JumpIfTrue { cond, target }) => {
1411                        let c = get(&mut reg, &mut inputs, cond);
1412                        WTerm::Branch {
1413                            cond: c,
1414                            then_blk: block_of(li, target),
1415                            else_blk: block_of(li, bend),
1416                        }
1417                    }
1418                    Some(Op::Call {
1419                        dst,
1420                        func,
1421                        args_start,
1422                        arg_count,
1423                    }) => {
1424                        let args: Vec<WSrc> = (0..arg_count)
1425                            .map(|i| get(&mut reg, &mut inputs, args_start + i))
1426                            .collect();
1427                        // The return value's type flows from the callee's DECLARED return type: mark `dst` float /
1428                        // float-collection so the continuation block reads it through the right lane (e.g.
1429                        // `Set v to mulAtav(...)` returning a `Seq of Float`, or `aVal(..)` returning a `Float`).
1430                        match &program.functions[func as usize].return_type {
1431                            Some(BoundaryType::Float) => {
1432                                float_regs.insert(dst);
1433                                float_colls.remove(&dst);
1434                                w32_regs.remove(&dst);
1435                                w32_colls.remove(&dst);
1436                                text_regs.remove(&dst);
1437                            }
1438                            Some(BoundaryType::Text) => {
1439                                text_regs.insert(dst);
1440                                float_regs.remove(&dst);
1441                                bool_regs.remove(&dst);
1442                                float_colls.remove(&dst);
1443                                w32_regs.remove(&dst);
1444                                w32_colls.remove(&dst);
1445                            }
1446                            Some(BoundaryType::Word32) => {
1447                                w32_regs.insert(dst);
1448                                w32_colls.remove(&dst);
1449                                float_regs.remove(&dst);
1450                                bool_regs.remove(&dst);
1451                                float_colls.remove(&dst);
1452                                text_regs.remove(&dst);
1453                            }
1454                            Some(BoundaryType::Seq(inner))
1455                                if matches!(**inner, BoundaryType::Float) =>
1456                            {
1457                                float_colls.insert(dst);
1458                                float_regs.remove(&dst);
1459                                bool_regs.remove(&dst);
1460                                w32_regs.remove(&dst);
1461                                w32_colls.remove(&dst);
1462                                text_regs.remove(&dst);
1463                            }
1464                            Some(BoundaryType::Seq(inner))
1465                                if matches!(**inner, BoundaryType::Word32) =>
1466                            {
1467                                w32_colls.insert(dst);
1468                                w32_regs.remove(&dst);
1469                                float_regs.remove(&dst);
1470                                bool_regs.remove(&dst);
1471                                float_colls.remove(&dst);
1472                                text_regs.remove(&dst);
1473                            }
1474                            _ => {
1475                                float_regs.remove(&dst);
1476                                bool_regs.remove(&dst);
1477                                float_colls.remove(&dst);
1478                                w32_regs.remove(&dst);
1479                                w32_colls.remove(&dst);
1480                                text_regs.remove(&dst);
1481                            }
1482                        }
1483                        WTerm::Call {
1484                            callee_block: fn_entry[&(func as usize)],
1485                            args,
1486                            dst_reg: dst,
1487                            ret_block: block_of(li, bend),
1488                        }
1489                    }
1490                    // `IterNext { dst, exit }` — the loop step of a `Repeat for …` range/for-each. The active
1491                    // iterator (top of the lowering-time stack) is a heap object: RANGE `[cursor, limit]` or SEQ
1492                    // `[index, coll_ptr]`. Read its state, produce `dst`, advance the cursor in the heap, then
1493                    // branch: has-next → the body block (fall-through); exhausted → `exit`. The unconditional
1494                    // `dst`/advance are harmless when exhausted (the body doesn't run and the iterator is popped).
1495                    Some(Op::IterNext { dst, exit }) => {
1496                        let (is_seq, iter_reg) =
1497                            *iter_stack.last().expect("IterNext with no active iterator");
1498                        let iptr = get(&mut reg, &mut inputs, iter_reg);
1499                        let kcur = cells.len();
1500                        cells.push(WCell {
1501                            kind: 3,
1502                            code: 0,
1503                            ops: [iptr, z, z, z],
1504                        }); // cursor/index = heap[iptr]
1505                        let cursor = val(kcur);
1506                        let kp1 = cells.len();
1507                        cells.push(WCell {
1508                            kind: 0,
1509                            code: 0,
1510                            ops: [iptr, con(1), z, z],
1511                        }); // iptr + 1
1512                        let ksec = cells.len();
1513                        cells.push(WCell {
1514                            kind: 3,
1515                            code: 0,
1516                            ops: [val(kp1), z, z, z],
1517                        }); // limit / coll_ptr
1518                        let (has_next, value) = if is_seq {
1519                            // SEQ: len = heap[coll]; has_next = index < len; value = heap[coll + index + 1].
1520                            let klen = cells.len();
1521                            cells.push(WCell {
1522                                kind: 3,
1523                                code: 0,
1524                                ops: [val(ksec), z, z, z],
1525                            });
1526                            let khn = cells.len();
1527                            cells.push(WCell {
1528                                kind: 1,
1529                                code: 0,
1530                                ops: [cursor, val(klen), con(1), con(0)],
1531                            }); // <
1532                            let kidx = cells.len();
1533                            cells.push(WCell {
1534                                kind: 0,
1535                                code: 0,
1536                                ops: [val(ksec), cursor, z, z],
1537                            }); // coll + index
1538                            let kaddr = cells.len();
1539                            cells.push(WCell {
1540                                kind: 0,
1541                                code: 0,
1542                                ops: [val(kidx), con(1), z, z],
1543                            }); // + 1
1544                            let kval = cells.len();
1545                            cells.push(WCell {
1546                                kind: 3,
1547                                code: 0,
1548                                ops: [val(kaddr), z, z, z],
1549                            });
1550                            (val(khn), val(kval))
1551                        } else {
1552                            // RANGE: has_next = cursor <= limit; value = cursor.
1553                            let khn = cells.len();
1554                            cells.push(WCell {
1555                                kind: 1,
1556                                code: 2,
1557                                ops: [cursor, val(ksec), con(1), con(0)],
1558                            }); // <=
1559                            (val(khn), cursor)
1560                        };
1561                        reg.insert(dst, value);
1562                        // whichever lane the elements are, the read value inherits it (Seq iterator over a float/
1563                        // word32 collection). Cleared here; the collection-typed tracking is a follow-on for typed
1564                        // for-each — the range case (the common one) is an Int.
1565                        float_regs.remove(&dst);
1566                        bool_regs.remove(&dst);
1567                        w32_regs.remove(&dst);
1568                        record(&mut wo, dst);
1569                        let kinc = cells.len();
1570                        cells.push(WCell {
1571                            kind: 0,
1572                            code: 0,
1573                            ops: [cursor, con(1), z, z],
1574                        }); // cursor + 1
1575                        cells.push(WCell {
1576                            kind: 4,
1577                            code: 0,
1578                            ops: [iptr, val(kinc), z, z],
1579                        }); // heap[iptr] = cursor+1
1580                        WTerm::Branch {
1581                            cond: has_next,
1582                            then_blk: block_of(li, bend),
1583                            else_blk: block_of(li, exit),
1584                        }
1585                    }
1586                    None => WTerm::Jump(block_of(li, bend)),
1587                    _ => unreachable!(),
1588                };
1589                let writes: Vec<(u16, WSrc)> = wo.iter().map(|r| (*r, reg[r])).collect();
1590                blocks.push(WBlock {
1591                    livein: inputs,
1592                    cells,
1593                    writes,
1594                    term,
1595                });
1596            }
1597        }
1598
1599        WProgConfig {
1600            n_params: entry_fi.map_or(0, |fi| program.functions[fi].param_count as usize),
1601            blocks,
1602        }
1603    })
1604}
1605
1606/// One `Show` the engine executed: the value tag the lowering assigned in the SHOW cell's `code` (from its exact
1607/// per-register type tracking, so a reused register is shown as what it holds now) and the raw 64-bit value. A
1608/// Text needs the heap to render — see [`render_show`]. This is exactly the record the RTL streams over the byte
1609/// channel ([`REC_SHOW`](super::chip_protocol::REC_SHOW), tag, payload) the moment the cell runs, so the tape the
1610/// model builds is the byte stream the host reads.
1611#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1612pub struct WShow {
1613    pub tag: u8,
1614    pub value: i64,
1615}
1616
1617/// Everything one engine run produces: the entry's return value, the block-step count, the final heap, the peak
1618/// call-frame depth, and the output tape (every `Show`, in program order).
1619#[derive(Clone, Debug)]
1620pub struct WRun {
1621    pub result: i64,
1622    pub steps: u64,
1623    pub heap: Vec<i64>,
1624    pub peak_depth: usize,
1625    pub shows: Vec<WShow>,
1626    pub work: WWork,
1627}
1628
1629/// What a run did, in the units the engine's cycle count is made of: every executed cell's cycles with its
1630/// data-dependent part priced from the data (a divide's 64 steps, a power's multiplies, a text's or a copy's
1631/// length), and the blocks, register writes, calls and returns around them.
1632#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1633pub struct WWork {
1634    pub cell_cycles: u64,
1635    pub blocks: u64,
1636    pub writes: u64,
1637    pub calls: u64,
1638    pub returns: u64,
1639    /// Bytes of Show records streamed, which the engine's run-cycle count leaves out.
1640    pub shown_bytes: u64,
1641}
1642
1643impl WWork {
1644    /// An upper bound on the engine's counted run cycles for `nreg` registers and `maxarg` call arguments,
1645    /// under every memory backend: each block's fetch and terminator settle, each write's read-before-write
1646    /// pass, and each call's frame save and argument gather and each return's restore, which walk every
1647    /// register of the frame.
1648    pub fn rtl_cycle_bound(&self, nreg: usize, maxarg: usize) -> u64 {
1649        let (nreg, maxarg) = (nreg as u64, maxarg as u64);
1650        self.cell_cycles
1651            + 16 * self.blocks
1652            + 8 * self.writes
1653            + (3 * nreg + 3 * maxarg + 16) * self.calls
1654            + (3 * nreg + 16) * self.returns
1655    }
1656}
1657
1658/// Cycles one cell may take outside its data-dependent part: its operands fetched one at a time through the
1659/// registered config and value ports, the compute, and the settle before the next cell.
1660const CELL_CYCLES: u64 = 16;
1661/// Cycles the sequential integer divider takes: one per dividend bit, and the sign fix.
1662const DIVIDE_CYCLES: u64 = 70;
1663/// Cycles a multiply spends on the multiplier unit after its issue: the tree's three stages and the write
1664/// (ST_MU..ST_MUW). A power's step adds the loop state that issues it.
1665const MUL_CYCLES: u64 = 4;
1666/// Cycles the widest floating-point unit takes (the iterative divide and square root).
1667const FLOAT_CYCLES: u64 = 256;
1668/// Cycles the decimal conversion of one Int takes: a double-dabble step per bit, then a byte per digit.
1669const DECIMAL_CYCLES: u64 = 96;
1670
1671/// Render one shown value the way the VM prints it: signed Int, `fmt_f64` Float, the Text's bytes, unsigned
1672/// Word32, `true`/`false`.
1673pub fn render_show(s: WShow, heap: &[i64]) -> String {
1674    match s.tag {
1675        TAG_FLOAT => logicaffeine_data::fmt::fmt_f64(f64::from_bits(s.value as u64)),
1676        TAG_TEXT => {
1677            let ptr = s.value.max(0) as usize;
1678            let len = heap.get(ptr).copied().unwrap_or(0).max(0) as usize;
1679            let bytes: Vec<u8> = (0..len)
1680                .map(|i| heap.get(ptr + 1 + i).copied().unwrap_or(0) as u8)
1681                .collect();
1682            String::from_utf8_lossy(&bytes).into_owned()
1683        }
1684        TAG_WORD32 => (s.value as u64 & 0xFFFF_FFFF).to_string(),
1685        TAG_BOOL => (s.value != 0).to_string(),
1686        _ => s.value.to_string(),
1687    }
1688}
1689
1690/// A whole program's OUTPUT on the engine model — every `Show` rendered, one line each — the form that compares
1691/// directly against `vm_outcome(src).output`.
1692pub fn program_output(cfg: &WProgConfig, params: &[i64]) -> Vec<String> {
1693    let run = eval_wprog_config_run(cfg, params, &[]);
1694    run.shows
1695        .iter()
1696        .map(|s| render_show(*s, &run.heap))
1697        .collect()
1698}
1699
1700/// Interpret the flat config exactly as the RTL will: a register file seeded with `params`, a block-walk over
1701/// the terminator graph, each block computing its cells (Arith/Select) over its live-in registers + prior
1702/// cell values, committing its register writes, and branching on its terminator. Proven bit-identical to
1703/// `run_program_cfg_on_grid` (≡ the VM) — the reference the whole-program RTL is checked against.
1704pub fn eval_wprog_config(cfg: &WProgConfig, params: &[i64]) -> i64 {
1705    eval_wprog_config_counted(cfg, params).0
1706}
1707
1708/// Like [`eval_wprog_config`] but also returns the number of BLOCK-STEPS executed. Each block is one spatial
1709/// tile group evaluated in a single fabric cycle, so this count IS the whole-program fabric's cycle count.
1710/// ONE authoritative evaluator shared by the result path and any cycle measurement — no drifting hand-copy.
1711pub fn eval_wprog_config_counted(cfg: &WProgConfig, params: &[i64]) -> (i64, u64) {
1712    let (r, c, _heap) = eval_wprog_config_counted_heap(cfg, params);
1713    (r, c)
1714}
1715
1716/// As [`eval_wprog_config_counted`] but also returns the final HEAP, so a caller can render a Text result (a
1717/// char-list `[len, byte0..]` at the returned pointer) back to a string for verification.
1718pub fn eval_wprog_config_counted_heap(cfg: &WProgConfig, params: &[i64]) -> (i64, u64, Vec<i64>) {
1719    eval_wprog_config_counted_heap_preloaded(cfg, params, &[])
1720}
1721
1722/// As [`eval_wprog_config_counted_heap`] but the heap starts PRE-LOADED with `heap_init` — the DATA-segment
1723/// model of a chip that receives its weights streamed into memory separately from the kernel. `loaded()`
1724/// lowers to heap pointer 0, so `item k of loaded()` reads `heap_init[k]` (`heap_init[0]` = the blob length,
1725/// `heap_init[1..]` = the weights). The bump allocator is simply `heap.len()`, so runtime collections
1726/// allocate PAST the pre-loaded region automatically — no separate `hp` seeding needed. This is what lets the
1727/// streamed transformer run ≡VM on the engine without Push-building its ~3,136 weights into the config.
1728pub fn eval_wprog_config_counted_heap_preloaded(
1729    cfg: &WProgConfig,
1730    params: &[i64],
1731    heap_init: &[i64],
1732) -> (i64, u64, Vec<i64>) {
1733    let (result, steps, heap, _peak_depth) = eval_wprog_config_probed(cfg, params, heap_init);
1734    (result, steps, heap)
1735}
1736
1737/// As [`eval_wprog_config_counted_heap_preloaded`] but also returns the PEAK call-frame depth the run reached.
1738///
1739/// Every other config dimension (blocks, cells, registers, live-ins, konsts) is a STATIC property the fit-check
1740/// reads straight off the config. Frame depth is not: it depends on the arguments, so the only way to size the
1741/// engine's `DEPTH` against a program is to run the program. Without this the sizing oracle is blind in exactly
1742/// the dimension the hardware corrupts silently — the engine advances `sp` past `DEPTH` with no bound check, so
1743/// an over-deep call returns a plausible wrong number rather than failing.
1744pub fn eval_wprog_config_probed(
1745    cfg: &WProgConfig,
1746    params: &[i64],
1747    heap_init: &[i64],
1748) -> (i64, u64, Vec<i64>, usize) {
1749    let r = eval_wprog_config_run(cfg, params, heap_init);
1750    (r.result, r.steps, r.heap, r.peak_depth)
1751}
1752
1753/// THE golden interpreter of the flat config — the one body every result / step / heap / depth / output-tape
1754/// query is a view of. Returns everything a run produces ([`WRun`]), including the `Show` tape.
1755pub fn eval_wprog_config_run(cfg: &WProgConfig, params: &[i64], heap_init: &[i64]) -> WRun {
1756    use std::collections::HashMap;
1757    let mut shows: Vec<WShow> = Vec::new();
1758    let mut file: HashMap<u16, i64> = (0..cfg.n_params).map(|p| (p as u16, params[p])).collect();
1759    // ONE heap threaded across every block (the state plane) — collections persist across reconfiguration.
1760    let mut heap: Vec<i64> = heap_init.to_vec();
1761    let mut blk = 0usize;
1762    // The CALL FRAME STACK: each frame is (return block, destination register in the caller, the caller's
1763    // saved register file). The heap is GLOBAL (not per-frame), matching the VM's single heap.
1764    let mut stack: Vec<(usize, u16, HashMap<u16, i64>)> = Vec::new();
1765    // The deepest this run drives the frame stack — the engine's `DEPTH` cap must cover it.
1766    let mut peak_depth = 0usize;
1767
1768    let read = |src: &WSrc, li: &[i64], vals: &[i64]| -> i64 {
1769        match src.mode {
1770            0 => li[src.reference as usize],
1771            1 => src.konst,
1772            _ => vals[src.reference as usize],
1773        }
1774    };
1775
1776    let mut steps = 0u64;
1777    let mut work = WWork::default();
1778    for _ in 0..10_000_000 {
1779        steps += 1;
1780        let b = &cfg.blocks[blk];
1781        work.blocks += 1;
1782        work.writes += b.writes.len() as u64;
1783        let li: Vec<i64> = b
1784            .livein
1785            .iter()
1786            .map(|r| *file.get(r).unwrap_or(&0))
1787            .collect();
1788        let mut vals: Vec<i64> = Vec::with_capacity(b.cells.len());
1789        for c in &b.cells {
1790            work.cell_cycles += CELL_CYCLES;
1791            let v = match c.kind {
1792                0 => {
1793                    let a = read(&c.ops[0], &li, &vals);
1794                    let b2 = read(&c.ops[1], &li, &vals);
1795                    if c.code == 2 {
1796                        work.cell_cycles += MUL_CYCLES;
1797                    }
1798                    match c.code {
1799                        0 => a.wrapping_add(b2),
1800                        1 => a.wrapping_sub(b2),
1801                        2 => a.wrapping_mul(b2),
1802                        3 => a & b2,
1803                        4 => a | b2,
1804                        5 => a ^ b2,
1805                        6 => ((a as u64) << (b2 as u64 & 63)) as i64,
1806                        _ => a >> (b2 as u64 & 63),
1807                    }
1808                }
1809                1 => {
1810                    let s0 = read(&c.ops[0], &li, &vals);
1811                    let s1 = read(&c.ops[1], &li, &vals);
1812                    let hit = match c.code {
1813                        0 => s0 < s1,
1814                        1 => s0 > s1,
1815                        2 => s0 <= s1,
1816                        3 => s0 >= s1,
1817                        4 => s0 == s1,
1818                        _ => s0 != s1,
1819                    };
1820                    if hit {
1821                        read(&c.ops[2], &li, &vals)
1822                    } else {
1823                        read(&c.ops[3], &li, &vals)
1824                    }
1825                }
1826                3 => {
1827                    let a = read(&c.ops[0], &li, &vals);
1828                    heap.get(a.max(0) as usize).copied().unwrap_or(0)
1829                }
1830                4 => {
1831                    let a = read(&c.ops[0], &li, &vals);
1832                    let bv = read(&c.ops[1], &li, &vals);
1833                    let ai = a.max(0) as usize;
1834                    if ai >= heap.len() {
1835                        heap.resize(ai + 1, 0);
1836                    }
1837                    heap[ai] = bv;
1838                    bv
1839                }
1840                5 => {
1841                    let a = read(&c.ops[0], &li, &vals);
1842                    let ptr = heap.len() as i64;
1843                    heap.resize(heap.len() + a.max(0) as usize, 0);
1844                    ptr
1845                }
1846                6 => {
1847                    // DEEP-COPY a collection (value semantics / COW): read `len = heap[src]`, allocate a fresh
1848                    // block `[len, e0..e_{len-1}]` plus reserve headroom, copy the elements, return the new ptr.
1849                    let src = read(&c.ops[0], &li, &vals).max(0) as usize;
1850                    let len = heap.get(src).copied().unwrap_or(0).max(0) as usize;
1851                    work.cell_cycles += 3 * (len as u64 + 1);
1852                    let newptr = heap.len() as i64;
1853                    let reserve = crate::codegen_sva::fabric_rtl::seq_reserve() as usize;
1854                    heap.resize(heap.len() + 1 + len + reserve, 0);
1855                    for j in 0..=len {
1856                        let v = heap.get(src + j).copied().unwrap_or(0);
1857                        let dst_idx = newptr as usize + j;
1858                        heap[dst_idx] = v;
1859                    }
1860                    newptr
1861                }
1862                7 => {
1863                    // TEXT CONCAT: build a fresh char-list `[len, bytes…]` = chars(a) ++ chars(b). Each operand is
1864                    // either TEXT (its char-list, `code` bit set) or an INT (rendered to its decimal bytes — the
1865                    // VM's `value + text` coercion). Immutable, so no reserve headroom.
1866                    let mut chars: Vec<i64> = Vec::new();
1867                    for (slot, bit) in [(0usize, 1u8), (1usize, 2u8)] {
1868                        let v = read(&c.ops[slot], &li, &vals);
1869                        if c.code & bit != 0 {
1870                            let p = v.max(0) as usize;
1871                            let ln = heap.get(p).copied().unwrap_or(0).max(0) as usize;
1872                            for j in 0..ln {
1873                                chars.push(heap.get(p + 1 + j).copied().unwrap_or(0));
1874                            }
1875                        } else {
1876                            for byte in v.to_string().into_bytes() {
1877                                chars.push(byte as i64);
1878                            }
1879                        }
1880                    }
1881                    work.cell_cycles += 3 * chars.len() as u64 + 2 * DECIMAL_CYCLES;
1882                    let ptr = heap.len() as i64;
1883                    heap.push(chars.len() as i64);
1884                    heap.extend(chars);
1885                    ptr
1886                }
1887                8 => {
1888                    // TEXT CHAR-AT: `item i of text` → a fresh 1-char Text `[1, byte]` (the VM returns a 1-char
1889                    // string, so it compares by text-equality). `idx` is 1-based over the char-list body.
1890                    let p = read(&c.ops[0], &li, &vals).max(0) as usize;
1891                    let idx = read(&c.ops[1], &li, &vals).max(0) as usize;
1892                    let ch = heap.get(p + idx).copied().unwrap_or(0);
1893                    let ptr = heap.len() as i64;
1894                    heap.push(1);
1895                    heap.push(ch);
1896                    ptr
1897                }
1898                9 => {
1899                    // TEXT EQUALITY: compare two char-lists byte-for-byte → 0/1 (`code` 0 = eq, 1 = ne).
1900                    let a = read(&c.ops[0], &li, &vals).max(0) as usize;
1901                    let b = read(&c.ops[1], &li, &vals).max(0) as usize;
1902                    let la = heap.get(a).copied().unwrap_or(0);
1903                    let lb = heap.get(b).copied().unwrap_or(0);
1904                    work.cell_cycles += 6 * la.max(0) as u64 + 8;
1905                    let eq = la == lb
1906                        && (0..la as usize).all(|j| heap.get(a + 1 + j) == heap.get(b + 1 + j));
1907                    i64::from(if c.code == 0 { eq } else { !eq })
1908                }
1909                2 => {
1910                    // Functional unit (division / mod / floor-div / power / magic-high-mul) — the VM-exact
1911                    // closed form, reusing the same `FuncUnit::eval` the CFG model uses.
1912                    let a = read(&c.ops[0], &li, &vals);
1913                    let b2 = read(&c.ops[1], &li, &vals);
1914                    work.cell_cycles += match c.code {
1915                        0..=2 => DIVIDE_CYCLES,
1916                        3 => (MUL_CYCLES + 1) * b2.max(0) as u64 + 4,
1917                        4 | 12 => MUL_CYCLES,
1918                        _ => FLOAT_CYCLES,
1919                    };
1920                    func_from_code(c.code).eval(a, b2)
1921                }
1922                10 => {
1923                    // SHOW: record the value on the output tape with the type tag the lowering assigned; the
1924                    // value itself flows on as the cell's result. The RTL streams the same record over the wire.
1925                    let v = read(&c.ops[0], &li, &vals);
1926                    // The record: sync, tag, then eight value bytes — or, for Text, four length bytes and the text.
1927                    work.shown_bytes += 2 + if c.code == TAG_TEXT {
1928                        4 + heap.get(v.max(0) as usize).copied().unwrap_or(0).max(0) as u64
1929                    } else {
1930                        8
1931                    };
1932                    shows.push(WShow {
1933                        tag: c.code,
1934                        value: v,
1935                    });
1936                    v
1937                }
1938                _ => panic!("wprog eval: unknown cell kind {}", c.kind),
1939            };
1940            vals.push(v);
1941        }
1942        for (r, s) in &b.writes {
1943            file.insert(*r, read(s, &li, &vals));
1944        }
1945        match &b.term {
1946            WTerm::Jump(t) => blk = *t,
1947            WTerm::Branch {
1948                cond,
1949                then_blk,
1950                else_blk,
1951            } => {
1952                blk = if read(cond, &li, &vals) != 0 {
1953                    *then_blk
1954                } else {
1955                    *else_blk
1956                }
1957            }
1958            WTerm::Call {
1959                callee_block,
1960                args,
1961                dst_reg,
1962                ret_block,
1963            } => {
1964                // Bind the args (read in the CALLER's context) to the callee's parameter registers 0..k, push
1965                // a frame, and jump to the callee's entry — a fresh register file for the callee.
1966                let arg_vals: Vec<i64> = args.iter().map(|a| read(a, &li, &vals)).collect();
1967                work.calls += 1;
1968                stack.push((*ret_block, *dst_reg, std::mem::take(&mut file)));
1969                peak_depth = peak_depth.max(stack.len());
1970                file = arg_vals
1971                    .iter()
1972                    .enumerate()
1973                    .map(|(i, v)| (i as u16, *v))
1974                    .collect();
1975                blk = *callee_block;
1976            }
1977            WTerm::Return(s) => {
1978                let ret_val = read(s, &li, &vals);
1979                work.returns += 1;
1980                match stack.pop() {
1981                    None => {
1982                        return WRun {
1983                            result: ret_val,
1984                            steps,
1985                            heap,
1986                            peak_depth,
1987                            shows,
1988                            work,
1989                        }
1990                    } // top-level return
1991                    Some((ret_block, dst_reg, saved_file)) => {
1992                        file = saved_file;
1993                        file.insert(dst_reg, ret_val);
1994                        blk = ret_block;
1995                    }
1996                }
1997            }
1998        }
1999    }
2000    panic!("wprog: did not return within the step budget (unbounded recursion or a non-terminating loop)")
2001}
2002
2003/// A hardware fault model injected into the golden interpreter. Running a program through the model under each
2004/// fault and comparing the result against a value observed on silicon localizes a synthesis/place-and-route
2005/// divergence to one state memory + one address bit in pure Rust: `eval` is the ground truth, so a fault whose
2006/// result equals the observed value characterizes the hardware defect.
2007#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2008pub enum WFault {
2009    /// No fault — byte-identical to `eval_wprog_config_counted_heap_preloaded`.
2010    None,
2011    /// Register file `file` address decode is `bits` wide: logical index `r` reads/writes physical
2012    /// `r & ((1<<bits)-1)` (both ports share the decode, so two logical registers alias onto one cell).
2013    RegDecodeBits(u32),
2014    /// Register file high half is dead: a read of `r >= thresh` yields 0.
2015    RegZeroAbove(u16),
2016    /// Heap load/store address decode is `bits` wide: `heap[addr]` accesses physical `addr & ((1<<bits)-1)`.
2017    /// The allocator's length counter is a register and stays exact; only the RAM addressing folds.
2018    HeapDecodeBits(u32),
2019    /// Heap high half is dead: a load from `addr >= thresh` yields 0.
2020    HeapZeroAbove(usize),
2021    /// Cell-value plane `v` address decode is `bits` wide: an earlier-cell reference `ci` reads physical
2022    /// `ci & ((1<<bits)-1)`.
2023    VDecodeBits(u32),
2024    /// A `heap[X] = v` (kind-4) store becomes visible to heap reads only after `steps` further block-steps,
2025    /// modelling a registered read/write pipeline. Allocations (bump-pointer) commit immediately so addressing
2026    /// stays valid; only explicit stores lag.
2027    HeapStoreLatency(u32),
2028}
2029
2030impl WFault {
2031    #[inline]
2032    fn reg(self, r: u16) -> u16 {
2033        match self {
2034            WFault::RegDecodeBits(b) => r & ((1u16 << b) - 1),
2035            _ => r,
2036        }
2037    }
2038    #[inline]
2039    fn reg_read_zero(self, r: u16) -> bool {
2040        matches!(self, WFault::RegZeroAbove(t) if r >= t)
2041    }
2042    #[inline]
2043    fn heap(self, a: usize) -> usize {
2044        match self {
2045            WFault::HeapDecodeBits(b) => a & ((1usize << b) - 1),
2046            _ => a,
2047        }
2048    }
2049    #[inline]
2050    fn heap_read_zero(self, a: usize) -> bool {
2051        matches!(self, WFault::HeapZeroAbove(t) if a >= t)
2052    }
2053    #[inline]
2054    fn vref(self, ci: usize) -> usize {
2055        match self {
2056            WFault::VDecodeBits(b) => ci & ((1usize << b) - 1),
2057            _ => ci,
2058        }
2059    }
2060}
2061
2062// Fold a whole fault set over each access point (compound faults compose: every decode mask is applied, and a
2063// zero-read fires if any variant matches). An empty slice is the identity — byte-identical to the golden model.
2064#[inline]
2065fn f_reg(faults: &[WFault], r: u16) -> u16 {
2066    faults.iter().fold(r, |r, f| f.reg(r))
2067}
2068#[inline]
2069fn f_reg_zero(faults: &[WFault], r: u16) -> bool {
2070    faults.iter().any(|f| f.reg_read_zero(r))
2071}
2072#[inline]
2073fn f_heap(faults: &[WFault], a: usize) -> usize {
2074    faults.iter().fold(a, |a, f| f.heap(a))
2075}
2076#[inline]
2077fn f_heap_zero(faults: &[WFault], a: usize) -> bool {
2078    faults.iter().any(|f| f.heap_read_zero(a))
2079}
2080#[inline]
2081fn f_vref(faults: &[WFault], ci: usize) -> usize {
2082    faults.iter().fold(ci, |ci, f| f.vref(ci))
2083}
2084#[inline]
2085fn f_store_latency(faults: &[WFault]) -> usize {
2086    faults
2087        .iter()
2088        .filter_map(|f| {
2089            if let WFault::HeapStoreLatency(n) = f {
2090                Some(*n as usize)
2091            } else {
2092                None
2093            }
2094        })
2095        .max()
2096        .unwrap_or(0)
2097}
2098
2099/// The golden interpreter under an injected hardware fault (see [`WFault`]). An empty fault set is
2100/// byte-identical to [`eval_wprog_config_counted_heap_preloaded`].
2101pub fn eval_wprog_config_faulted(
2102    cfg: &WProgConfig,
2103    params: &[i64],
2104    heap_init: &[i64],
2105    faults: &[WFault],
2106) -> (i64, u64) {
2107    use std::collections::HashMap;
2108    let mut file: HashMap<u16, i64> = (0..cfg.n_params)
2109        .map(|p| (f_reg(faults, p as u16), params[p]))
2110        .collect();
2111    let mut heap: Vec<i64> = heap_init.to_vec();
2112    let mut blk = 0usize;
2113    let mut stack: Vec<(usize, u16, HashMap<u16, i64>)> = Vec::new();
2114
2115    // Read an operand. Cell-value references (mode 2) route through the faulty `v` decode; live-in reads
2116    // already went through the faulty register decode when `li` was built.
2117    let read = |src: &WSrc, li: &[i64], vals: &[i64]| -> i64 {
2118        match src.mode {
2119            0 => li[src.reference as usize],
2120            1 => src.konst,
2121            _ => vals[f_vref(faults, src.reference as usize).min(vals.len().saturating_sub(1))],
2122        }
2123    };
2124
2125    // Heap stores held in flight, made visible to reads only after `store_lat` steps.
2126    let store_lat = f_store_latency(faults);
2127    let mut store_pipe: std::collections::VecDeque<Vec<(usize, i64)>> =
2128        std::iter::repeat_with(Vec::new).take(store_lat).collect();
2129
2130    let mut steps = 0u64;
2131    for _ in 0..10_000_000 {
2132        steps += 1;
2133        // Commit the stores that have aged `store_lat` steps, then open a fresh in-flight bucket for this step.
2134        if store_lat > 0 {
2135            if let Some(commit) = store_pipe.pop_front() {
2136                for (a, v) in commit {
2137                    if a < heap.len() {
2138                        heap[a] = v;
2139                    }
2140                }
2141            }
2142            store_pipe.push_back(Vec::new());
2143        }
2144        let b = &cfg.blocks[blk];
2145        let li: Vec<i64> = b
2146            .livein
2147            .iter()
2148            .map(|r| {
2149                if f_reg_zero(faults, *r) {
2150                    0
2151                } else {
2152                    *file.get(&f_reg(faults, *r)).unwrap_or(&0)
2153                }
2154            })
2155            .collect();
2156        let mut vals: Vec<i64> = Vec::with_capacity(b.cells.len());
2157        for c in &b.cells {
2158            let v = match c.kind {
2159                0 => {
2160                    let a = read(&c.ops[0], &li, &vals);
2161                    let b2 = read(&c.ops[1], &li, &vals);
2162                    match c.code {
2163                        0 => a.wrapping_add(b2),
2164                        1 => a.wrapping_sub(b2),
2165                        2 => a.wrapping_mul(b2),
2166                        3 => a & b2,
2167                        4 => a | b2,
2168                        5 => a ^ b2,
2169                        6 => ((a as u64) << (b2 as u64 & 63)) as i64,
2170                        _ => a >> (b2 as u64 & 63),
2171                    }
2172                }
2173                1 => {
2174                    let s0 = read(&c.ops[0], &li, &vals);
2175                    let s1 = read(&c.ops[1], &li, &vals);
2176                    let hit = match c.code {
2177                        0 => s0 < s1,
2178                        1 => s0 > s1,
2179                        2 => s0 <= s1,
2180                        3 => s0 >= s1,
2181                        4 => s0 == s1,
2182                        _ => s0 != s1,
2183                    };
2184                    if hit {
2185                        read(&c.ops[2], &li, &vals)
2186                    } else {
2187                        read(&c.ops[3], &li, &vals)
2188                    }
2189                }
2190                3 => {
2191                    let a = read(&c.ops[0], &li, &vals).max(0) as usize;
2192                    if f_heap_zero(faults, a) {
2193                        0
2194                    } else {
2195                        heap.get(f_heap(faults, a)).copied().unwrap_or(0)
2196                    }
2197                }
2198                4 => {
2199                    let a = read(&c.ops[0], &li, &vals);
2200                    let bv = read(&c.ops[1], &li, &vals);
2201                    let ai = a.max(0) as usize;
2202                    if ai >= heap.len() {
2203                        heap.resize(ai + 1, 0);
2204                    }
2205                    let wi = f_heap(faults, ai).min(heap.len() - 1);
2206                    if store_lat > 0 {
2207                        store_pipe.back_mut().unwrap().push((wi, bv));
2208                    } else {
2209                        heap[wi] = bv;
2210                    }
2211                    bv
2212                }
2213                5 => {
2214                    let a = read(&c.ops[0], &li, &vals);
2215                    let ptr = heap.len() as i64;
2216                    heap.resize(heap.len() + a.max(0) as usize, 0);
2217                    ptr
2218                }
2219                6 => {
2220                    let src = read(&c.ops[0], &li, &vals).max(0) as usize;
2221                    let len = heap.get(src).copied().unwrap_or(0).max(0) as usize;
2222                    let newptr = heap.len() as i64;
2223                    let reserve = crate::codegen_sva::fabric_rtl::seq_reserve() as usize;
2224                    heap.resize(heap.len() + 1 + len + reserve, 0);
2225                    for j in 0..=len {
2226                        let v = heap.get(src + j).copied().unwrap_or(0);
2227                        let dst_idx = newptr as usize + j;
2228                        heap[dst_idx] = v;
2229                    }
2230                    newptr
2231                }
2232                7 => {
2233                    let mut chars: Vec<i64> = Vec::new();
2234                    for (slot, bit) in [(0usize, 1u8), (1usize, 2u8)] {
2235                        let v = read(&c.ops[slot], &li, &vals);
2236                        if c.code & bit != 0 {
2237                            let p = v.max(0) as usize;
2238                            let ln = heap.get(p).copied().unwrap_or(0).max(0) as usize;
2239                            for j in 0..ln {
2240                                chars.push(heap.get(p + 1 + j).copied().unwrap_or(0));
2241                            }
2242                        } else {
2243                            for byte in v.to_string().into_bytes() {
2244                                chars.push(byte as i64);
2245                            }
2246                        }
2247                    }
2248                    let ptr = heap.len() as i64;
2249                    heap.push(chars.len() as i64);
2250                    heap.extend(chars);
2251                    ptr
2252                }
2253                8 => {
2254                    let p = read(&c.ops[0], &li, &vals).max(0) as usize;
2255                    let idx = read(&c.ops[1], &li, &vals).max(0) as usize;
2256                    let ch = heap.get(p + idx).copied().unwrap_or(0);
2257                    let ptr = heap.len() as i64;
2258                    heap.push(1);
2259                    heap.push(ch);
2260                    ptr
2261                }
2262                9 => {
2263                    let a = read(&c.ops[0], &li, &vals).max(0) as usize;
2264                    let b = read(&c.ops[1], &li, &vals).max(0) as usize;
2265                    let la = heap.get(a).copied().unwrap_or(0);
2266                    let lb = heap.get(b).copied().unwrap_or(0);
2267                    let eq = la == lb
2268                        && (0..la as usize).all(|j| heap.get(a + 1 + j) == heap.get(b + 1 + j));
2269                    i64::from(if c.code == 0 { eq } else { !eq })
2270                }
2271                2 => {
2272                    let a = read(&c.ops[0], &li, &vals);
2273                    let b2 = read(&c.ops[1], &li, &vals);
2274                    func_from_code(c.code).eval(a, b2)
2275                }
2276                10 => read(&c.ops[0], &li, &vals),
2277                _ => panic!("wprog eval: unknown cell kind {}", c.kind),
2278            };
2279            vals.push(v);
2280        }
2281        for (r, s) in &b.writes {
2282            file.insert(f_reg(faults, *r), read(s, &li, &vals));
2283        }
2284        match &b.term {
2285            WTerm::Jump(t) => blk = *t,
2286            WTerm::Branch {
2287                cond,
2288                then_blk,
2289                else_blk,
2290            } => {
2291                blk = if read(cond, &li, &vals) != 0 {
2292                    *then_blk
2293                } else {
2294                    *else_blk
2295                }
2296            }
2297            WTerm::Call {
2298                callee_block,
2299                args,
2300                dst_reg,
2301                ret_block,
2302            } => {
2303                let arg_vals: Vec<i64> = args.iter().map(|a| read(a, &li, &vals)).collect();
2304                stack.push((*ret_block, *dst_reg, std::mem::take(&mut file)));
2305                file = arg_vals
2306                    .iter()
2307                    .enumerate()
2308                    .map(|(i, v)| (f_reg(faults, i as u16), *v))
2309                    .collect();
2310                blk = *callee_block;
2311            }
2312            WTerm::Return(s) => {
2313                let ret_val = read(s, &li, &vals);
2314                match stack.pop() {
2315                    None => return (ret_val, steps),
2316                    Some((ret_block, dst_reg, saved_file)) => {
2317                        file = saved_file;
2318                        file.insert(f_reg(faults, dst_reg), ret_val);
2319                        blk = ret_block;
2320                    }
2321                }
2322            }
2323        }
2324    }
2325    panic!("wprog: did not return within the step budget (unbounded recursion or a non-terminating loop)")
2326}
2327
2328/// The register-file width for a whole-program config: one 64-bit word per distinct register touched.
2329pub fn wprog_nregs(cfg: &WProgConfig) -> usize {
2330    let mut m = 0u16;
2331    for b in &cfg.blocks {
2332        for r in &b.livein {
2333            m = m.max(*r);
2334        }
2335        for (r, _) in &b.writes {
2336            m = m.max(*r);
2337        }
2338        if let WTerm::Call { dst_reg, .. } = &b.term {
2339            m = m.max(*dst_reg);
2340        }
2341    }
2342    (m as usize + 1).max(cfg.n_params).max(1)
2343}
2344
2345// ── The GENERIC config-driven engine's fixed capacities ──────────────────────────────────────────────────────
2346// One netlist runs EVERY program that fits these bounds (recursion/collections/division/floats included). They
2347// are shared between the emitter (`emit_wprog_engine_verilog`) and the loud capacity guard
2348// (`wprog_config_fits_engine`) so a program too big for the netlist is REJECTED at image build, never silently
2349// mis-run — an over-`NREG` register access hits a nonexistent file slot and reads 0 (exactly the failure the
2350// Mandelbrot kernel first exposed). Sized to hold the real benchmark kernels while still targeting the Nexys A7.
2351// The generic (sim/iverilog) engine caps. Widened to hold a whole int8 transformer PROJECTION (matmul +
2352// requant + argmax) — the RDU's core workload — end to end on the config-driven engine (proven ≡VM by
2353// wpe_transformer_lock). These size only the generic engine (emit_wprog_engine_verilog) used under iverilog;
2354// the deployed silicon bitstream uses the separate WPE_HW_* profile, so this widening does not bloat the chip.
2355/// Register-file depth of the generic config-driven whole-program engine (per-register, `file[0:NREG-1]`).
2356pub const WPE_NREG: usize = 64;
2357/// Max basic blocks per program (`blk_*[0:MAXBLK-1]`, indexed by the block PC).
2358pub const WPE_MAXBLK: usize = 32;
2359/// Max TOTAL cells across the whole program — the flat `cell_*` config memory (indexed by `blk_cell_ptr[b]+ci`,
2360/// an accumulating pointer) AND the per-block value array `v[0:MAXCELL-1]`.
2361pub const WPE_MAXCELL: usize = 128;
2362/// Max TOTAL register writes across the whole program — the flat `wr_*` config memory (accumulating pointer).
2363pub const WPE_MAXWR: usize = 128;
2364/// Max TOTAL call arguments across the whole program — the flat `arg_op` config memory.
2365pub const WPE_MAXARG: usize = 16;
2366/// Max TOTAL live-in registers across the whole program — the flat `liv_reg` config memory.
2367pub const WPE_MAXLIV: usize = 96;
2368/// Max distinct constants in the program's konst table (the flat `konst` memory).
2369pub const WPE_MAXK: usize = 128;
2370/// Heap state-plane depth (words) — a projection builds a weight Seq + activation Seqs.
2371pub const WPE_HEAP: usize = 1024;
2372/// Frame-stack depth (max recursion).
2373pub const WPE_DEPTH: usize = 24;
2374
2375/// Does `cfg` fit the fixed generic engine? Returns `Err` naming the first exceeded dimension. A program that
2376/// overflows any bound CANNOT run on the one-size netlist, so its image must be rejected LOUDLY here rather than
2377/// producing silently-wrong results (a flat-array overflow corrupts later blocks' cells/writes; an over-`NREG`
2378/// register indexes a nonexistent file slot → 0 — the two failure modes the Mandelbrot kernel first exposed).
2379/// The `cell_*`/`wr_*`/`liv_reg`/`arg_op`/`konst` memories are FLAT with accumulating per-block pointers, so
2380/// their bounds are program TOTALS, not per-block. Called by `wprog_config_image` — every image is checked.
2381pub fn wprog_config_fits_engine(cfg: &WProgConfig) -> Result<(), String> {
2382    if cfg.blocks.len() > WPE_MAXBLK {
2383        return Err(format!(
2384            "{} blocks > engine MAXBLK={WPE_MAXBLK}",
2385            cfg.blocks.len()
2386        ));
2387    }
2388    let nreg = wprog_nregs(cfg);
2389    if nreg > WPE_NREG {
2390        return Err(format!(
2391            "program needs {nreg} registers > engine NREG={WPE_NREG}"
2392        ));
2393    }
2394    let total_cells: usize = cfg.blocks.iter().map(|b| b.cells.len()).sum();
2395    if total_cells > WPE_MAXCELL {
2396        return Err(format!(
2397            "{total_cells} total cells > engine MAXCELL={WPE_MAXCELL}"
2398        ));
2399    }
2400    let total_writes: usize = cfg.blocks.iter().map(|b| b.writes.len()).sum();
2401    if total_writes > WPE_MAXWR {
2402        return Err(format!(
2403            "{total_writes} total writes > engine MAXWR={WPE_MAXWR}"
2404        ));
2405    }
2406    let total_liveins: usize = cfg.blocks.iter().map(|b| b.livein.len()).sum();
2407    if total_liveins > WPE_MAXLIV {
2408        return Err(format!(
2409            "{total_liveins} total live-ins > engine MAXLIV={WPE_MAXLIV}"
2410        ));
2411    }
2412    let total_args: usize = cfg
2413        .blocks
2414        .iter()
2415        .map(|b| {
2416            if let WTerm::Call { args, .. } = &b.term {
2417                args.len()
2418            } else {
2419                0
2420            }
2421        })
2422        .sum();
2423    if total_args > WPE_MAXARG {
2424        return Err(format!(
2425            "{total_args} total call args > engine MAXARG={WPE_MAXARG}"
2426        ));
2427    }
2428    Ok(())
2429}
2430
2431fn wclog2(n: usize) -> usize {
2432    let mut b = 1;
2433    while (1usize << b) < n {
2434        b += 1;
2435    }
2436    b
2437}
2438
2439/// The Verilog expression for a `WSrc` in the SEQUENTIAL per-cell engine (block live-ins are `livein`). Mode 0
2440/// (`In`) is a POSITION into the block's live-in list — exactly as `eval_wprog_config`'s `li[reference]` indexes
2441/// `livein` — so it is the register-file net `fr_{livein[reference]}`; mode 1 is a signed constant; mode 2
2442/// (`Val`) is an earlier cell's REGISTERED value `v[reference]`, valid because cells evaluate one per clock.
2443fn op_expr(livein: &[u16], s: &WSrc) -> String {
2444    match s.mode {
2445        0 => format!("fr_{}", livein[s.reference as usize]),
2446        1 => format!("$signed(64'h{:016x})", s.konst as u64),
2447        _ => format!("v[{}]", s.reference),
2448    }
2449}
2450
2451/// Emit the WHOLE-PROGRAM ENGINE as synthesizable RTL: a register file + block-PC + a SEQUENTIAL per-cell
2452/// datapath (one cell per clock into a cell-value array `v`, so the ordered HEAP STATE PLANE — HeapRead /
2453/// HeapWrite / HeapAlloc against a heap BRAM — threads exactly as the model does) + a terminator FSM
2454/// (Jump/Branch/Return/CALL) + a FRAME STACK that saves the caller's register file on every `Call` and
2455/// restores it on `Return`. So the WHOLE language family — branchy control flow, loops, recursion, division,
2456/// and collections/strings/structs on the heap — runs on silicon, proven bit-identical to `eval_wprog_config`
2457/// (≡ the VM) under iverilog. Cells: Arith / Select / Func (divider) / HeapRead / HeapWrite / HeapAlloc.
2458pub fn emit_wprog_verilog(name: &str, cfg: &WProgConfig) -> String {
2459    let nb = cfg.blocks.len();
2460    let nreg = wprog_nregs(cfg);
2461    let pcw = wclog2(nb);
2462    let depth = 64usize; // frame-stack depth bound for the differential
2463    let spw = wclog2(depth + 1);
2464    let dstw = wclog2(nreg).max(1);
2465    let max_cells = cfg.blocks.iter().map(|b| b.cells.len()).max().unwrap_or(0);
2466    let vcap = max_cells.max(1);
2467    let ciw = wclog2(max_cells + 1);
2468    const HSIZE: usize = 1024; // heap BRAM depth (words) — sized for the differential's collections
2469
2470    let mut b = String::new();
2471    let mut ports: Vec<String> = vec!["input clk".into(), "input rst".into()];
2472    for p in 0..cfg.n_params {
2473        ports.push(format!("input signed [63:0] p{p}"));
2474    }
2475    ports.push("output [63:0] result".into());
2476    ports.push("output done".into());
2477    ports.push("output [63:0] cycles".into());
2478
2479    b.push_str(&format!(
2480        "  localparam NREG = {nreg};\n  localparam HSIZE = {HSIZE};\n"
2481    ));
2482    b.push_str(&format!("  reg signed [63:0] file [0:{}];\n", nreg - 1));
2483    b.push_str(&format!(
2484        "  reg signed [63:0] sfile [0:{}];\n",
2485        depth * nreg - 1
2486    ));
2487    b.push_str(&format!("  reg [{}:0] sret [0:{}];\n", pcw - 1, depth - 1));
2488    b.push_str(&format!("  reg [{}:0] sdst [0:{}];\n", dstw - 1, depth - 1));
2489    b.push_str(&format!("  reg [{}:0] sp;\n", spw - 1));
2490    b.push_str(&format!("  reg [{}:0] pc;\n", pcw - 1));
2491    b.push_str(&format!("  reg signed [63:0] v [0:{}];\n", vcap - 1)); // this block's cell values
2492    b.push_str(&format!("  reg [{}:0] ci;\n", ciw - 1)); // cell index within the current block
2493    b.push_str("  reg signed [63:0] heap [0:HSIZE-1];\n  reg signed [63:0] hp;\n"); // shared heap + length
2494    b.push_str("  reg done_r; reg [63:0] result_r; reg [63:0] cyc_r;\n");
2495    b.push_str("  assign done = done_r; assign result = result_r; assign cycles = cyc_r;\n");
2496    b.push_str("  integer gi, hj;\n\n");
2497
2498    for r in 0..nreg {
2499        b.push_str(&format!("  wire signed [63:0] fr_{r} = file[{r}];\n"));
2500    }
2501    b.push('\n');
2502
2503    b.push_str("  function signed [63:0] alu_f(input [3:0] op, input signed [63:0] a, input signed [63:0] x);\n");
2504    b.push_str("    case (op)\n");
2505    b.push_str("      4'd0: alu_f=a+x; 4'd1: alu_f=a-x; 4'd2: alu_f=a*x;\n");
2506    b.push_str("      4'd3: alu_f=a&x; 4'd4: alu_f=a|x; 4'd5: alu_f=a^x;\n");
2507    b.push_str("      4'd6: alu_f=a<<<x; 4'd7: alu_f=a>>>x; 4'd8: alu_f=$unsigned(a)>>x; default: alu_f=64'sd0;\n");
2508    b.push_str("    endcase\n  endfunction\n");
2509    b.push_str("  function cmp_f(input [3:0] op, input signed [63:0] a, input signed [63:0] x);\n");
2510    b.push_str("    case (op)\n");
2511    b.push_str("      4'd0: cmp_f=(a<x); 4'd1: cmp_f=(a>x); 4'd2: cmp_f=(a<=x);\n");
2512    b.push_str("      4'd3: cmp_f=(a>=x); 4'd4: cmp_f=(a==x); default: cmp_f=(a!=x);\n");
2513    b.push_str("    endcase\n  endfunction\n");
2514    b.push_str(func_f_verilog());
2515    b.push('\n');
2516
2517    // The FSM: sequence each block's cells one per clock (into `v`), then commit its writes + terminator.
2518    b.push_str("  always @(posedge clk) begin\n    if (rst) begin\n");
2519    b.push_str(&format!(
2520        "      pc <= {pcw}'d0; done_r <= 1'b0; result_r <= 64'd0; cyc_r <= 64'd0; sp <= {spw}'d0; ci <= {ciw}'d0; hp <= 64'sd0;\n"
2521    ));
2522    b.push_str("      for (hj=0; hj<HSIZE; hj=hj+1) heap[hj] <= 64'sd0;\n");
2523    for r in 0..nreg {
2524        if r < cfg.n_params {
2525            b.push_str(&format!("      file[{r}] <= p{r};\n"));
2526        } else {
2527            b.push_str(&format!("      file[{r}] <= 64'sd0;\n"));
2528        }
2529    }
2530    b.push_str("    end else if (!done_r) begin\n      cyc_r <= cyc_r + 64'd1;\n      case (pc)\n");
2531    for (k, blk) in cfg.blocks.iter().enumerate() {
2532        let li = &blk.livein;
2533        b.push_str(&format!("        {pcw}'d{k}: begin\n"));
2534        let commit = |b: &mut String| {
2535            // Commit register writes, then follow the terminator. Reset the cell index for the next block.
2536            b.push_str(&format!("            ci <= {ciw}'d0;\n"));
2537            for (r, s) in &blk.writes {
2538                b.push_str(&format!("            file[{r}] <= {};\n", op_expr(li, s)));
2539            }
2540            match &blk.term {
2541                WTerm::Jump(t) => b.push_str(&format!("            pc <= {pcw}'d{t};\n")),
2542                WTerm::Branch {
2543                    cond,
2544                    then_blk,
2545                    else_blk,
2546                } => b.push_str(&format!(
2547                    "            pc <= (({}) != 64'sd0) ? {pcw}'d{then_blk} : {pcw}'d{else_blk};\n",
2548                    op_expr(li, cond)
2549                )),
2550                WTerm::Return(s) => {
2551                    b.push_str(&format!(
2552                        "            if (sp == {spw}'d0) begin result_r <= {}; done_r <= 1'b1; end\n",
2553                        op_expr(li, s)
2554                    ));
2555                    b.push_str("            else begin\n");
2556                    b.push_str("              for (gi=0; gi<NREG; gi=gi+1) file[gi] <= sfile[(sp-1)*NREG+gi];\n");
2557                    b.push_str(&format!(
2558                        "              file[sdst[sp-1]] <= {};\n",
2559                        op_expr(li, s)
2560                    ));
2561                    b.push_str(&format!(
2562                        "              pc <= sret[sp-1]; sp <= sp - {spw}'d1;\n"
2563                    ));
2564                    b.push_str("            end\n");
2565                }
2566                WTerm::Call {
2567                    callee_block,
2568                    args,
2569                    dst_reg,
2570                    ret_block,
2571                } => {
2572                    b.push_str(
2573                        "            for (gi=0; gi<NREG; gi=gi+1) sfile[sp*NREG+gi] <= file[gi];\n",
2574                    );
2575                    // Registers WRITTEN in this Call block are live across the call, but the loop above snapshots
2576                    // the PRE-write file (non-blocking `<=` reads the old value). Override those slots with the NEW
2577                    // value (last-write-wins) so a value computed in the call block — e.g. `n*4` in `(n*4)+f(n-1)` —
2578                    // survives the call + its post-Return restore instead of collapsing to the stale pre-block value.
2579                    for (r, s) in &blk.writes {
2580                        b.push_str(&format!(
2581                            "            sfile[sp*NREG+{r}] <= {};\n",
2582                            op_expr(li, s)
2583                        ));
2584                    }
2585                    b.push_str(&format!(
2586                        "            sret[sp] <= {pcw}'d{ret_block}; sdst[sp] <= {dstw}'d{dst_reg}; sp <= sp + {spw}'d1;\n"
2587                    ));
2588                    for (a, s) in args.iter().enumerate() {
2589                        b.push_str(&format!("            file[{a}] <= {};\n", op_expr(li, s)));
2590                    }
2591                    for a in args.len()..nreg {
2592                        b.push_str(&format!("            file[{a}] <= 64'sd0;\n"));
2593                    }
2594                    b.push_str(&format!("            pc <= {pcw}'d{callee_block};\n"));
2595                }
2596            }
2597        };
2598        if blk.cells.is_empty() {
2599            commit(&mut b);
2600        } else {
2601            b.push_str(&format!(
2602                "          if (ci < {ciw}'d{}) begin\n",
2603                blk.cells.len()
2604            ));
2605            b.push_str("            case (ci)\n");
2606            for (i, c) in blk.cells.iter().enumerate() {
2607                b.push_str(&format!("              {ciw}'d{i}: begin\n"));
2608                match c.kind {
2609                    0 => b.push_str(&format!(
2610                        "                v[{i}] <= alu_f(4'd{}, {}, {});\n",
2611                        c.code, op_expr(li, &c.ops[0]), op_expr(li, &c.ops[1])
2612                    )),
2613                    1 => b.push_str(&format!(
2614                        "                v[{i}] <= cmp_f(4'd{}, {}, {}) ? ({}) : ({});\n",
2615                        c.code, op_expr(li, &c.ops[0]), op_expr(li, &c.ops[1]),
2616                        op_expr(li, &c.ops[2]), op_expr(li, &c.ops[3])
2617                    )),
2618                    2 => b.push_str(&format!(
2619                        "                v[{i}] <= func_f(8'd{}, {}, {});\n",
2620                        c.code, op_expr(li, &c.ops[0]), op_expr(li, &c.ops[1])
2621                    )),
2622                    // HeapRead: v[i] <= heap[addr]. The heap is reset-zeroed, so an unwritten / unallocated
2623                    // slot reads 0 — matching `eval_wprog_config`'s `heap.get(ai).unwrap_or(0)`.
2624                    3 => b.push_str(&format!(
2625                        "                v[{i}] <= heap[{}];\n",
2626                        op_expr(li, &c.ops[0])
2627                    )),
2628                    // HeapWrite: store, grow the length pointer if the address is at/beyond it, drive the value.
2629                    4 => {
2630                        let addr = op_expr(li, &c.ops[0]);
2631                        let val = op_expr(li, &c.ops[1]);
2632                        b.push_str(&format!("                heap[{addr}] <= {val};\n"));
2633                        b.push_str(&format!("                if (({addr}) >= hp) hp <= ({addr}) + 64'sd1;\n"));
2634                        b.push_str(&format!("                v[{i}] <= {val};\n"));
2635                    }
2636                    // HeapAlloc: return the base pointer (current length), grow the heap by `size` (zeroed region).
2637                    5 => {
2638                        let sz = op_expr(li, &c.ops[0]);
2639                        b.push_str(&format!("                v[{i}] <= hp; hp <= hp + ({sz});\n"));
2640                    }
2641                    10 => panic!(
2642                        "wprog RTL: a Show cell streams over the SoC's byte channel — the per-program netlist \
2643                         has no channel; run Show programs on the SoC engine (emit_wprog_engine_soc_verilog)"
2644                    ),
2645                    other => panic!("wprog RTL: unknown cell kind {other}"),
2646                }
2647                b.push_str("              end\n");
2648            }
2649            b.push_str("            endcase\n");
2650            b.push_str(&format!("            ci <= ci + {ciw}'d1;\n"));
2651            b.push_str("          end else begin\n");
2652            commit(&mut b);
2653            b.push_str("          end\n");
2654        }
2655        b.push_str("        end\n");
2656    }
2657    b.push_str("        default: done_r <= 1'b1;\n      endcase\n    end\n  end\n");
2658
2659    format!(
2660        "// wprog_{name} — the WHOLE-PROGRAM ENGINE in RTL: register file + block-PC + SEQUENTIAL per-cell\n\
2661         // datapath (Arith/Select/Func + a heap state plane: HeapRead/Write/Alloc on a heap BRAM) + terminator\n\
2662         // FSM (Jump/Branch/Return/Call) + a FRAME STACK, so recursion + collections run on silicon. == eval.\n\
2663         module wprog_{name}(\n    {}\n);\n{b}endmodule\n",
2664        ports.join(",\n    "),
2665    )
2666}
2667
2668/// A self-checking testbench for the whole-program engine: for each param vector, pulse reset, run the FSM to
2669/// `done`, and compare `result` to the `eval_wprog_config` oracle in `wprog_{name}_out.hex` (params in
2670/// `wprog_{name}_in.hex`). Proves the RTL — register file + block walk + FRAME STACK — matches the model.
2671pub fn emit_wprog_testbench(name: &str, n_params: usize, n_vectors: usize) -> String {
2672    let mut decls = String::new();
2673    if n_params > 0 {
2674        decls.push_str(&format!(
2675            "  reg signed [63:0] pin [0:{}];\n",
2676            n_params * n_vectors - 1
2677        ));
2678    }
2679    decls.push_str(&format!(
2680        "  reg signed [63:0] oout [0:{}];\n",
2681        n_vectors - 1
2682    ));
2683    for p in 0..n_params {
2684        decls.push_str(&format!("  reg signed [63:0] p{p};\n"));
2685    }
2686    let mut dut_ports: Vec<String> = vec![".clk(clk)".into(), ".rst(rst)".into()];
2687    for p in 0..n_params {
2688        dut_ports.push(format!(".p{p}(p{p})"));
2689    }
2690    dut_ports.push(".result(result)".into());
2691    dut_ports.push(".done(done)".into());
2692    dut_ports.push(".cycles(cycles)".into());
2693    let readin = if n_params > 0 {
2694        format!("    $readmemh(\"wprog_{name}_in.hex\", pin);\n")
2695    } else {
2696        String::new()
2697    };
2698    let mut load = String::new();
2699    for p in 0..n_params {
2700        load.push_str(&format!("      p{p} = pin[i*{n_params}+{p}];\n"));
2701    }
2702    format!(
2703        r#"// tb_wprog_{name} — the whole-program engine (regfile + block-walk + frame stack) vs eval_wprog_config.
2704`timescale 1ns/1ps
2705module tb_wprog_{name};
2706  reg clk, rst;
2707{decls}  wire [63:0] result, cycles; wire done;
2708  integer i, errors, guard;
2709  wprog_{name} dut({ports});
2710  initial begin clk = 0; forever #5 clk = ~clk; end
2711  initial begin
2712    errors = 0;
2713{readin}    $readmemh("wprog_{name}_out.hex", oout);
2714    for (i = 0; i < {n_vectors}; i = i + 1) begin
2715{load}      rst = 1; @(posedge clk); #1; rst = 0;
2716      guard = 0;
2717      while (!done && guard < 2000000) begin @(posedge clk); #1; guard = guard + 1; end
2718      if (result !== oout[i]) begin errors = errors + 1; $display("FAIL case %0d: got %0d exp %0d", i, result, oout[i]); end
2719    end
2720    if (errors == 0) $display("PASS: {n_vectors} whole-program (recursion) runs match eval_wprog_config");
2721    else $display("FAIL: %0d whole-program mismatches", errors);
2722    $finish;
2723  end
2724endmodule
2725"#,
2726        ports = dut_ports.join(", "),
2727    )
2728}
2729
2730// ── Config-driven whole-program processor: the config IMAGE (loaded over the wire) + the GENERIC engine that
2731// interprets it. Where `emit_wprog_verilog` specializes the RTL to ONE program, this pair is ONE fixed engine
2732// that runs ANY `WProgConfig` by loading its image into on-chip memories — the self-synthesizing bytecode XPU.
2733// The image is a set of PARALLEL per-field memories (no bit-packing) so the engine indexes fields directly.
2734
2735/// A 32-bit operand word: `mode` in bits [31:30], `ref` in [15:0]. `In` → ref is the live-in POSITION; `Const`
2736/// → ref indexes the 64-bit constant pool; `Val` → ref is the earlier cell index.
2737fn pack_op(s: &WSrc, konst_pool: &mut Vec<i64>) -> u32 {
2738    let (mode, r) = match s.mode {
2739        0 => (0u32, s.reference as u32),
2740        1 => {
2741            // Intern the constant: identical values share one konst slot, so the table holds only the DISTINCT
2742            // constants (keeping it under the engine's fixed `MAXK`). Safe — a konst index is by-value, and the
2743            // model reads `WSrc.konst` inline, so deduping the packed image changes nothing the engine computes.
2744            let idx = konst_pool
2745                .iter()
2746                .position(|&k| k == s.konst)
2747                .unwrap_or_else(|| {
2748                    konst_pool.push(s.konst);
2749                    konst_pool.len() - 1
2750                }) as u32;
2751            (1u32, idx)
2752        }
2753        _ => (2u32, s.reference as u32),
2754    };
2755    (mode << 30) | (r & 0xffff)
2756}
2757
2758/// The flat, loadable image of a whole program: parallel per-field arrays the generic engine reads directly.
2759/// Block `b`'s fields are `blk_*[b]`; its cells are `cell_*[blk_cell_ptr[b] .. + blk_n_cells[b]]`; and so on.
2760#[derive(Default)]
2761pub struct WImage {
2762    pub n_blocks: usize,
2763    pub n_params: usize,
2764    pub nreg: usize,
2765    pub blk_livein_ptr: Vec<u32>,
2766    pub blk_n_livein: Vec<u32>,
2767    pub blk_cell_ptr: Vec<u32>,
2768    pub blk_n_cells: Vec<u32>,
2769    pub blk_write_ptr: Vec<u32>,
2770    pub blk_n_writes: Vec<u32>,
2771    pub blk_term_kind: Vec<u32>, // 0 Jump, 1 Branch, 2 Return, 3 Call
2772    pub blk_t0: Vec<u32>,        // Jump target / Branch then / Call callee
2773    pub blk_t1: Vec<u32>,        // Branch else / Call ret_block
2774    pub blk_top: Vec<u32>,       // Branch cond op / Return op (32-bit operand word)
2775    pub blk_cdst: Vec<u32>,      // Call dst register
2776    pub blk_cargc: Vec<u32>,     // Call arg count
2777    pub blk_cargp: Vec<u32>,     // Call arg pointer
2778    pub cell_kind: Vec<u32>,
2779    pub cell_code: Vec<u32>,
2780    pub cell_op0: Vec<u32>,
2781    pub cell_op1: Vec<u32>,
2782    pub cell_op2: Vec<u32>,
2783    pub cell_op3: Vec<u32>,
2784    pub wr_reg: Vec<u32>,
2785    pub wr_op: Vec<u32>,
2786    pub arg_op: Vec<u32>,
2787    pub liv_reg: Vec<u32>,
2788    pub konst: Vec<i64>,
2789}
2790
2791/// Flatten a `WProgConfig` into its loadable image — the exact tables the generic engine walks: block records
2792/// with base pointers into flat cell / write / arg / live-in pools, and a constant pool for `Const` operands.
2793pub fn wprog_config_image(cfg: &WProgConfig) -> WImage {
2794    // A program that overflows the fixed netlist cannot run on it — reject it LOUDLY here instead of emitting an
2795    // image the engine would silently mis-run (the Mandelbrot-over-NREG bug).
2796    if let Err(e) = wprog_config_fits_engine(cfg) {
2797        panic!("wprog_config_image: program does not fit the generic config-driven engine ({e})");
2798    }
2799    wprog_config_image_unchecked(cfg)
2800}
2801
2802/// Build the flat wire image WITHOUT the generic-engine fit check — for streaming into a BIG-cap engine (the
2803/// backend/scale work: the pipelined BRAM engine sized to run every benchmark) whose caps exceed the generic
2804/// netlist. The caller guarantees the program fits the target cap profile; only the `MAXK` konst-table bound
2805/// (shared by every profile) is still asserted here.
2806pub fn wprog_config_image_unchecked(cfg: &WProgConfig) -> WImage {
2807    let nreg = wprog_nregs(cfg);
2808    let mut im = WImage {
2809        n_blocks: cfg.blocks.len(),
2810        n_params: cfg.n_params,
2811        nreg,
2812        blk_livein_ptr: vec![],
2813        blk_n_livein: vec![],
2814        blk_cell_ptr: vec![],
2815        blk_n_cells: vec![],
2816        blk_write_ptr: vec![],
2817        blk_n_writes: vec![],
2818        blk_term_kind: vec![],
2819        blk_t0: vec![],
2820        blk_t1: vec![],
2821        blk_top: vec![],
2822        blk_cdst: vec![],
2823        blk_cargc: vec![],
2824        blk_cargp: vec![],
2825        cell_kind: vec![],
2826        cell_code: vec![],
2827        cell_op0: vec![],
2828        cell_op1: vec![],
2829        cell_op2: vec![],
2830        cell_op3: vec![],
2831        wr_reg: vec![],
2832        wr_op: vec![],
2833        arg_op: vec![],
2834        liv_reg: vec![],
2835        konst: vec![],
2836    };
2837    for blk in &cfg.blocks {
2838        im.blk_livein_ptr.push(im.liv_reg.len() as u32);
2839        im.blk_n_livein.push(blk.livein.len() as u32);
2840        for r in &blk.livein {
2841            im.liv_reg.push(*r as u32);
2842        }
2843        im.blk_cell_ptr.push(im.cell_kind.len() as u32);
2844        im.blk_n_cells.push(blk.cells.len() as u32);
2845        for c in &blk.cells {
2846            im.cell_kind.push(c.kind as u32);
2847            im.cell_code.push(c.code as u32);
2848            im.cell_op0.push(pack_op(&c.ops[0], &mut im.konst));
2849            im.cell_op1.push(pack_op(&c.ops[1], &mut im.konst));
2850            im.cell_op2.push(pack_op(&c.ops[2], &mut im.konst));
2851            im.cell_op3.push(pack_op(&c.ops[3], &mut im.konst));
2852        }
2853        im.blk_write_ptr.push(im.wr_reg.len() as u32);
2854        im.blk_n_writes.push(blk.writes.len() as u32);
2855        for (r, s) in &blk.writes {
2856            im.wr_reg.push(*r as u32);
2857            let op = pack_op(s, &mut im.konst);
2858            im.wr_op.push(op);
2859        }
2860        let (tk, t0, t1, top, cdst, cargc, cargp) = match &blk.term {
2861            WTerm::Jump(t) => (0u32, *t as u32, 0, 0u32, 0u32, 0u32, 0u32),
2862            WTerm::Branch {
2863                cond,
2864                then_blk,
2865                else_blk,
2866            } => (
2867                1u32,
2868                *then_blk as u32,
2869                *else_blk as u32,
2870                pack_op(cond, &mut im.konst),
2871                0,
2872                0,
2873                0,
2874            ),
2875            WTerm::Return(s) => (2u32, 0, 0, pack_op(s, &mut im.konst), 0, 0, 0),
2876            WTerm::Call {
2877                callee_block,
2878                args,
2879                dst_reg,
2880                ret_block,
2881            } => {
2882                let argp = im.arg_op.len() as u32;
2883                let ops: Vec<u32> = args.iter().map(|a| pack_op(a, &mut im.konst)).collect();
2884                for op in ops {
2885                    im.arg_op.push(op);
2886                }
2887                (
2888                    3u32,
2889                    *callee_block as u32,
2890                    *ret_block as u32,
2891                    0,
2892                    *dst_reg as u32,
2893                    args.len() as u32,
2894                    argp,
2895                )
2896            }
2897        };
2898        im.blk_term_kind.push(tk);
2899        im.blk_t0.push(t0);
2900        im.blk_t1.push(t1);
2901        im.blk_top.push(top);
2902        im.blk_cdst.push(cdst);
2903        im.blk_cargc.push(cargc);
2904        im.blk_cargp.push(cargp);
2905    }
2906    // The konst table is built during packing, so its final size is checked here (the structural bounds were
2907    // checked up front). An over-`MAXK` program would read a nonexistent constant slot.
2908    assert!(
2909        im.konst.len() <= WPE_MAXK,
2910        "wprog_config_image: {} distinct constants > engine MAXK={WPE_MAXK}",
2911        im.konst.len()
2912    );
2913    im
2914}
2915
2916impl WImage {
2917    /// The `$readmemh` payloads for every field memory, as `(memory-name, hex-lines)` pairs. An EMPTY memory
2918    /// still emits one `0` line so `$readmemh` has a file to read (the engine never indexes an empty pool).
2919    pub fn hex_files(&self) -> Vec<(String, String)> {
2920        fn h32(v: &[u32]) -> String {
2921            if v.is_empty() {
2922                return "00000000\n".to_string();
2923            }
2924            v.iter().map(|x| format!("{x:08x}\n")).collect()
2925        }
2926        fn h64(v: &[i64]) -> String {
2927            if v.is_empty() {
2928                return "0000000000000000\n".to_string();
2929            }
2930            v.iter().map(|x| format!("{:016x}\n", *x as u64)).collect()
2931        }
2932        vec![
2933            ("blk_livein_ptr".into(), h32(&self.blk_livein_ptr)),
2934            ("blk_n_livein".into(), h32(&self.blk_n_livein)),
2935            ("blk_cell_ptr".into(), h32(&self.blk_cell_ptr)),
2936            ("blk_n_cells".into(), h32(&self.blk_n_cells)),
2937            ("blk_write_ptr".into(), h32(&self.blk_write_ptr)),
2938            ("blk_n_writes".into(), h32(&self.blk_n_writes)),
2939            ("blk_term_kind".into(), h32(&self.blk_term_kind)),
2940            ("blk_t0".into(), h32(&self.blk_t0)),
2941            ("blk_t1".into(), h32(&self.blk_t1)),
2942            ("blk_top".into(), h32(&self.blk_top)),
2943            ("blk_cdst".into(), h32(&self.blk_cdst)),
2944            ("blk_cargc".into(), h32(&self.blk_cargc)),
2945            ("blk_cargp".into(), h32(&self.blk_cargp)),
2946            ("cell_kind".into(), h32(&self.cell_kind)),
2947            ("cell_code".into(), h32(&self.cell_code)),
2948            ("cell_op0".into(), h32(&self.cell_op0)),
2949            ("cell_op1".into(), h32(&self.cell_op1)),
2950            ("cell_op2".into(), h32(&self.cell_op2)),
2951            ("cell_op3".into(), h32(&self.cell_op3)),
2952            ("wr_reg".into(), h32(&self.wr_reg)),
2953            ("wr_op".into(), h32(&self.wr_op)),
2954            ("arg_op".into(), h32(&self.arg_op)),
2955            ("liv_reg".into(), h32(&self.liv_reg)),
2956            ("konst".into(), h64(&self.konst)),
2957        ]
2958    }
2959}
2960
2961/// Emit the GENERIC config-driven whole-program processor: ONE fixed microcoded engine that runs ANY program
2962/// by loading its `WImage` into on-chip config memories (via `$readmemh`) and interpreting it — fetch a block
2963/// record, evaluate its cells one per clock (operands resolved from the register file / cell-value array /
2964/// constant pool through the live-in table), commit its register writes, then follow the terminator, with a
2965/// heap state plane and a frame stack for `Call`/`Return`. Proven ≡ `eval_wprog_config` (≡ the VM) under
2966/// iverilog for any loaded image. `n_params` sizes only the input ports; every other bound is a fixed generic
2967/// capacity, so the SAME netlist runs every program that fits.
2968pub fn emit_wprog_engine_verilog(name: &str, n_params: usize) -> String {
2969    // Fixed generic capacities (one netlist for all programs that fit these bounds) — shared with the loud
2970    // capacity guard (`wprog_config_fits_engine`) so a program too big for the netlist is REJECTED at image
2971    // build, never silently mis-run (an over-`NREG` register access would hit a nonexistent file slot → 0).
2972    const NREG: usize = WPE_NREG;
2973    const MAXBLK: usize = WPE_MAXBLK;
2974    const MAXCELL: usize = WPE_MAXCELL;
2975    const MAXWR: usize = WPE_MAXWR;
2976    const MAXARG: usize = WPE_MAXARG;
2977    const MAXLIV: usize = WPE_MAXLIV;
2978    const MAXK: usize = WPE_MAXK;
2979    const HEAP: usize = WPE_HEAP;
2980    const DEPTH: usize = WPE_DEPTH;
2981
2982    let mut ports: Vec<String> = vec!["input clk".into(), "input rst".into()];
2983    for p in 0..n_params {
2984        ports.push(format!("input signed [63:0] p{p}"));
2985    }
2986    ports.push("output [63:0] result".into());
2987    ports.push("output done".into());
2988
2989    let mut file_init = String::new();
2990    for r in 0..NREG {
2991        if r < n_params {
2992            file_init.push_str(&format!("      file[{r}] <= p{r};\n"));
2993        } else {
2994            file_init.push_str(&format!("      file[{r}] <= 64'sd0;\n"));
2995        }
2996    }
2997
2998    // Config memories + their $readmemh loads.
2999    let cfg_mems = [
3000        ("blk_livein_ptr", MAXBLK, 32),
3001        ("blk_n_livein", MAXBLK, 32),
3002        ("blk_cell_ptr", MAXBLK, 32),
3003        ("blk_n_cells", MAXBLK, 32),
3004        ("blk_write_ptr", MAXBLK, 32),
3005        ("blk_n_writes", MAXBLK, 32),
3006        ("blk_term_kind", MAXBLK, 32),
3007        ("blk_t0", MAXBLK, 32),
3008        ("blk_t1", MAXBLK, 32),
3009        ("blk_top", MAXBLK, 32),
3010        ("blk_cdst", MAXBLK, 32),
3011        ("blk_cargc", MAXBLK, 32),
3012        ("blk_cargp", MAXBLK, 32),
3013        ("cell_kind", MAXCELL, 32),
3014        ("cell_code", MAXCELL, 32),
3015        ("cell_op0", MAXCELL, 32),
3016        ("cell_op1", MAXCELL, 32),
3017        ("cell_op2", MAXCELL, 32),
3018        ("cell_op3", MAXCELL, 32),
3019        ("wr_reg", MAXWR, 32),
3020        ("wr_op", MAXWR, 32),
3021        ("arg_op", MAXARG, 32),
3022        ("liv_reg", MAXLIV, 32),
3023    ];
3024    let mut decls = String::new();
3025    let mut loads = String::new();
3026    for (nm, depth, w) in cfg_mems {
3027        decls.push_str(&format!("  reg [{}:0] {nm} [0:{}];\n", w - 1, depth - 1));
3028        loads.push_str(&format!("    $readmemh(\"wpe_{name}_{nm}.hex\", {nm});\n"));
3029    }
3030    decls.push_str(&format!("  reg signed [63:0] konst [0:{}];\n", MAXK - 1));
3031    loads.push_str(&format!(
3032        "    $readmemh(\"wpe_{name}_konst.hex\", konst);\n"
3033    ));
3034
3035    format!(
3036        r#"// wpe_{name} — the GENERIC config-driven WHOLE-PROGRAM PROCESSOR. One fixed microcoded engine runs ANY
3037// program by loading its image ($readmemh) and interpreting it: fetch block -> eval cells (1/clk) -> commit
3038// writes (1/clk) -> terminator, over a register file + cell-value array + heap state plane + a frame stack.
3039// == eval_wprog_config for every loaded image.
3040`timescale 1ns/1ps
3041module wpe_{name}(
3042    {ports}
3043);
3044  localparam NREG={NREG}, HEAP={HEAP}, DEPTH={DEPTH};
3045{decls}
3046  reg signed [63:0] file [0:NREG-1];
3047  reg signed [63:0] v [0:{MAXCELL_M1}];
3048  reg signed [63:0] heap [0:HEAP-1];
3049  reg signed [63:0] hp;
3050  reg signed [63:0] sfile [0:DEPTH*NREG-1];
3051  reg [15:0] sret [0:DEPTH-1];
3052  reg [15:0] sdst [0:DEPTH-1];
3053  reg [15:0] sp, pc, ci, wi, gi, ai;
3054  reg [2:0] st;
3055  reg signed [63:0] retval;
3056  reg done_r; reg signed [63:0] result_r;
3057  assign done = done_r; assign result = result_r;
3058  integer hj;
3059  initial begin
3060{loads}  end
3061
3062  // Current block record (combinational, indexed by pc).
3063  wire [31:0] c_livptr = blk_livein_ptr[pc]; wire [31:0] c_cellptr = blk_cell_ptr[pc];
3064  wire [31:0] c_ncells = blk_n_cells[pc];    wire [31:0] c_wrptr  = blk_write_ptr[pc];
3065  wire [31:0] c_nwr = blk_n_writes[pc];      wire [31:0] c_tk = blk_term_kind[pc];
3066  wire [31:0] c_t0 = blk_t0[pc]; wire [31:0] c_t1 = blk_t1[pc]; wire [31:0] c_top = blk_top[pc];
3067  wire [31:0] c_cdst = blk_cdst[pc]; wire [31:0] c_cargc = blk_cargc[pc]; wire [31:0] c_cargp = blk_cargp[pc];
3068  // Current cell (combinational, indexed by cellptr+ci).
3069  wire [31:0] cc_kind = cell_kind[c_cellptr + ci]; wire [31:0] cc_code = cell_code[c_cellptr + ci];
3070  wire [31:0] cc_op0 = cell_op0[c_cellptr + ci]; wire [31:0] cc_op1 = cell_op1[c_cellptr + ci];
3071  wire [31:0] cc_op2 = cell_op2[c_cellptr + ci]; wire [31:0] cc_op3 = cell_op3[c_cellptr + ci];
3072
3073  function signed [63:0] alu_f(input [3:0] op, input signed [63:0] a, input signed [63:0] x);
3074    case (op)
3075      4'd0: alu_f=a+x; 4'd1: alu_f=a-x; 4'd2: alu_f=a*x; 4'd3: alu_f=a&x;
3076      4'd4: alu_f=a|x; 4'd5: alu_f=a^x; 4'd6: alu_f=a<<<x; 4'd7: alu_f=a>>>x; default: alu_f=64'sd0;
3077    endcase
3078  endfunction
3079  function cmp_f(input [3:0] op, input signed [63:0] a, input signed [63:0] x);
3080    case (op)
3081      4'd0: cmp_f=(a<x); 4'd1: cmp_f=(a>x); 4'd2: cmp_f=(a<=x);
3082      4'd3: cmp_f=(a>=x); 4'd4: cmp_f=(a==x); default: cmp_f=(a!=x);
3083    endcase
3084  endfunction
3085{func_f_def}  // Resolve an operand against the LIVE register file: In (mode 0) -> file[liv_reg[lptr+ref]], Const -> konst,
3086  // Val -> v[ref].
3087  function signed [63:0] resolve(input [31:0] op, input [31:0] lptr);
3088    case (op[31:30])
3089      2'd0: resolve = file[liv_reg[lptr + op[15:0]]];
3090      2'd1: resolve = konst[op[15:0]];
3091      default: resolve = v[op[15:0]];
3092    endcase
3093  endfunction
3094  // Resolve a CALL argument against the SAVED caller frame (sfile[(sp-1)*NREG+reg]) — sp is already bumped —
3095  // so binding args does not corrupt the operands still being read. Val still reads this block's cells `v`.
3096  function signed [63:0] resolve_arg(input [31:0] idx);
3097    reg [31:0] op;
3098    begin
3099      op = arg_op[c_cargp + idx];
3100      case (op[31:30])
3101        2'd0: resolve_arg = sfile[(sp-1)*NREG + liv_reg[c_livptr + op[15:0]]];
3102        2'd1: resolve_arg = konst[op[15:0]];
3103        default: resolve_arg = v[op[15:0]];
3104      endcase
3105    end
3106  endfunction
3107
3108  // Operand resolution reads the LIVE register file, so it must be recomputed inside the clocked block each
3109  // step (a continuous-assign `wire = resolve(...)` would NOT re-trigger when `file` changes, since `file` is
3110  // read inside the function, not passed as an argument — the classic function-in-continuous-assign staleness).
3111  reg signed [63:0] r0, r1, r2, r3, cellval;
3112
3113  always @(posedge clk) begin
3114    if (rst) begin
3115      pc<=16'd0; sp<=16'd0; ci<=16'd0; wi<=16'd0; gi<=16'd0; ai<=16'd0; st<=3'd0;
3116      done_r<=1'b0; result_r<=64'sd0; hp<=64'sd0;
3117      for (hj=0; hj<HEAP; hj=hj+1) heap[hj] <= 64'sd0;
3118{file_init}    end else if (!done_r) begin
3119      case (st)
3120        3'd0: begin // EVAL cells — resolve operands FRESH (blocking) against the live file, then latch the value
3121          if (ci < c_ncells[15:0]) begin
3122            r0 = resolve(cc_op0, c_livptr);
3123            r1 = resolve(cc_op1, c_livptr);
3124            r2 = resolve(cc_op2, c_livptr);
3125            r3 = resolve(cc_op3, c_livptr);
3126            cellval = (cc_kind==32'd0) ? alu_f(cc_code[3:0], r0, r1) :
3127                      (cc_kind==32'd1) ? (cmp_f(cc_code[3:0], r0, r1) ? r2 : r3) :
3128                      (cc_kind==32'd2) ? func_f(cc_code[7:0], r0, r1) :
3129                      (cc_kind==32'd3) ? heap[r0] :
3130                      (cc_kind==32'd4) ? r1 : hp;
3131            v[ci] <= cellval;
3132            if (cc_kind==32'd4) begin heap[r0] <= r1; if (r0 >= hp) hp <= r0 + 64'sd1; end
3133            if (cc_kind==32'd5) hp <= hp + r0;
3134            ci <= ci + 16'd1;
3135          end else begin wi <= 16'd0; st <= 3'd1; end
3136        end
3137        3'd1: begin // COMMIT writes
3138          if (wi < c_nwr[15:0]) begin
3139            file[wr_reg[c_wrptr + wi][15:0]] <= resolve(wr_op[c_wrptr + wi], c_livptr);
3140            wi <= wi + 16'd1;
3141          end else st <= 3'd2;
3142        end
3143        3'd2: begin // TERMINATOR
3144          ci <= 16'd0;
3145          case (c_tk)
3146            32'd0: begin pc <= c_t0[15:0]; st <= 3'd0; end
3147            32'd1: begin pc <= (resolve(c_top, c_livptr) != 64'sd0) ? c_t0[15:0] : c_t1[15:0]; st <= 3'd0; end
3148            32'd2: begin
3149              if (sp == 16'd0) begin result_r <= resolve(c_top, c_livptr); done_r <= 1'b1; end
3150              else begin retval <= resolve(c_top, c_livptr); gi <= 16'd0; st <= 3'd5; end
3151            end
3152            default: begin gi <= 16'd0; st <= 3'd3; end
3153          endcase
3154        end
3155        3'd3: begin // SAVE caller frame
3156          if (gi < NREG) begin sfile[sp*NREG + gi] <= file[gi]; gi <= gi + 16'd1; end
3157          else begin sret[sp] <= c_t1[15:0]; sdst[sp] <= c_cdst[15:0]; sp <= sp + 16'd1; ai <= 16'd0; st <= 3'd4; end
3158        end
3159        3'd4: begin // BIND args into the callee's parameter registers
3160          if (ai < NREG) begin
3161            file[ai] <= (ai < c_cargc[15:0]) ? resolve_arg(ai) : 64'sd0;
3162            ai <= ai + 16'd1;
3163          end else begin pc <= c_t0[15:0]; st <= 3'd0; end
3164        end
3165        3'd5: begin // RESTORE caller frame on return
3166          if (gi < NREG) begin file[gi] <= sfile[(sp-1)*NREG + gi]; gi <= gi + 16'd1; end
3167          else begin file[sdst[sp-1]] <= retval; pc <= sret[sp-1]; sp <= sp - 16'd1; st <= 3'd0; end
3168        end
3169      endcase
3170    end
3171  end
3172endmodule
3173"#,
3174        ports = ports.join(",\n    "),
3175        func_f_def = func_f_verilog(),
3176        MAXCELL_M1 = MAXCELL - 1,
3177    )
3178}
3179
3180/// A self-checking testbench for the generic config-driven engine: it loads the params, pulses reset, runs to
3181/// `done`, and compares `result` to the `eval_wprog_config` oracle in `wpe_{name}_out.hex` (params in
3182/// `wpe_{name}_in.hex`; the program image in the `wpe_{name}_*.hex` config memories the engine loads).
3183pub fn emit_wprog_engine_testbench(name: &str, n_params: usize, n_vectors: usize) -> String {
3184    let mut decls = String::new();
3185    if n_params > 0 {
3186        decls.push_str(&format!(
3187            "  reg signed [63:0] pin [0:{}];\n",
3188            n_params * n_vectors - 1
3189        ));
3190    }
3191    decls.push_str(&format!(
3192        "  reg signed [63:0] oout [0:{}];\n",
3193        n_vectors - 1
3194    ));
3195    for p in 0..n_params {
3196        decls.push_str(&format!("  reg signed [63:0] p{p};\n"));
3197    }
3198    let mut dut_ports: Vec<String> = vec![".clk(clk)".into(), ".rst(rst)".into()];
3199    for p in 0..n_params {
3200        dut_ports.push(format!(".p{p}(p{p})"));
3201    }
3202    dut_ports.push(".result(result)".into());
3203    dut_ports.push(".done(done)".into());
3204    let readin = if n_params > 0 {
3205        format!("    $readmemh(\"wpe_{name}_in.hex\", pin);\n")
3206    } else {
3207        String::new()
3208    };
3209    let mut load = String::new();
3210    for p in 0..n_params {
3211        load.push_str(&format!("      p{p} = pin[i*{n_params}+{p}];\n"));
3212    }
3213    format!(
3214        r#"// tb_wpe_{name} — the GENERIC config-driven whole-program engine vs eval_wprog_config.
3215`timescale 1ns/1ps
3216module tb_wpe_{name};
3217  reg clk, rst;
3218{decls}  wire [63:0] result; wire done;
3219  integer i, errors, guard;
3220  wpe_{name} dut({ports});
3221  initial begin clk = 0; forever #5 clk = ~clk; end
3222  initial begin
3223    errors = 0;
3224{readin}    $readmemh("wpe_{name}_out.hex", oout);
3225    for (i = 0; i < {n_vectors}; i = i + 1) begin
3226{load}      // Drive reset and sample `done` on the NEGEDGE, away from the engine's posedge writes (no race).
3227      @(negedge clk); rst = 1;
3228      @(negedge clk); @(negedge clk); rst = 0;
3229      guard = 0;
3230      while (done !== 1'b1 && guard < 4000000) begin @(negedge clk); guard = guard + 1; end
3231      if (result !== oout[i]) begin errors = errors + 1; $display("FAIL case %0d: got %0d exp %0d", i, result, oout[i]); end
3232    end
3233    if (errors == 0) $display("PASS: {n_vectors} config-driven whole-program runs match eval_wprog_config");
3234    else $display("FAIL: %0d config-driven mismatches", errors);
3235    $finish;
3236  end
3237endmodule
3238"#,
3239        ports = dut_ports.join(", "),
3240    )
3241}
3242
3243impl WImage {
3244    /// The exact byte stream a host sends OVER THE WIRE to the `wpe_soc` engine: for each of the 24 config
3245    /// memories (the `hex_files` order) a 2-byte big-endian entry count then that many big-endian words (4 bytes
3246    /// for the 23 u32 memories, 8 for `konst`), then a 2-byte param count + that many 8-byte params, then the
3247    /// [`IMAGE_GO`] marker. The on-chip loader FSM reproduces the identical config in on-chip memory — so the chip
3248    /// SYNTHESIZES the program it is handed. Single source of truth for both the iverilog host and the FPGA host.
3249    pub fn wire_bytes(&self, params: &[i64]) -> Vec<u8> {
3250        self.wire_bytes_preloaded(params, &[])
3251    }
3252
3253    /// As [`Self::wire_bytes`] but with an optional HEAP PRE-LOAD segment — the streamed-weights path. `heap_init` is
3254    /// loaded into `heap[0..heap_init.len()]` before the run (the DATA-segment model), so `loaded()` (heap
3255    /// pointer 0) reads it; the engine seeds its bump allocator past the pre-load and clears only the runtime
3256    /// region. The segment is BACKWARD-COMPATIBLE and marker-delimited: when `heap_init` is empty the stream is
3257    /// byte-identical to `wire_bytes` (…params + [`IMAGE_GO`]); when non-empty, an [`IMAGE_HEAP`] marker after the
3258    /// params introduces a u16 word-count then that many u64 words, then the [`IMAGE_GO`] byte. So a non-streamed
3259    /// program's wire format is unchanged (the loader sees [`IMAGE_GO`] right after the params) and only streamed
3260    /// programs carry the extra segment.
3261    pub fn wire_bytes_preloaded(&self, params: &[i64], heap_init: &[i64]) -> Vec<u8> {
3262        let mut out = Vec::new();
3263        let u16be = |out: &mut Vec<u8>, x: u16| out.extend_from_slice(&x.to_be_bytes());
3264        let mems32: [&Vec<u32>; 23] = [
3265            &self.blk_livein_ptr,
3266            &self.blk_n_livein,
3267            &self.blk_cell_ptr,
3268            &self.blk_n_cells,
3269            &self.blk_write_ptr,
3270            &self.blk_n_writes,
3271            &self.blk_term_kind,
3272            &self.blk_t0,
3273            &self.blk_t1,
3274            &self.blk_top,
3275            &self.blk_cdst,
3276            &self.blk_cargc,
3277            &self.blk_cargp,
3278            &self.cell_kind,
3279            &self.cell_code,
3280            &self.cell_op0,
3281            &self.cell_op1,
3282            &self.cell_op2,
3283            &self.cell_op3,
3284            &self.wr_reg,
3285            &self.wr_op,
3286            &self.arg_op,
3287            &self.liv_reg,
3288        ];
3289        for m in mems32 {
3290            u16be(&mut out, m.len() as u16);
3291            for &w in m {
3292                out.extend_from_slice(&w.to_be_bytes());
3293            }
3294        }
3295        u16be(&mut out, self.konst.len() as u16);
3296        for &k in &self.konst {
3297            out.extend_from_slice(&(k as u64).to_be_bytes());
3298        }
3299        u16be(&mut out, params.len() as u16);
3300        for &p in params {
3301            out.extend_from_slice(&(p as u64).to_be_bytes());
3302        }
3303        if !heap_init.is_empty() {
3304            out.push(IMAGE_HEAP);
3305            u16be(&mut out, heap_init.len() as u16);
3306            for &h in heap_init {
3307                out.extend_from_slice(&(h as u64).to_be_bytes());
3308            }
3309        }
3310        out.push(IMAGE_GO);
3311        out
3312    }
3313}
3314
3315/// The combinational `func_f` for the over-the-wire SoC: no integer func code computes here. DivS/ModS/FloorDivS/Pow
3316/// (codes 0–3) run on the engine's sequential datapath (`wpe_soc_template.v` st==6, a magnitude-restoring divider and
3317/// a repeat-multiply), MulHiU (4) and MulOvfS (12) on its multiplier unit (ST_MU..ST_MUW), and the float lane on the
3318/// FPU, so no multiply or divide shares a cycle with the cell walk. Shared by all three SoC engines
3319/// (behavioral/synth/hw) so they agree byte-for-byte; the sequencing and results are proven ≡ `FuncUnit::eval`
3320/// (== the VM) under iverilog.
3321pub const MUL_FUNC_F: &str = r#"  // The integer multiplies (MulHiU, MulOvfS) run on the engine's multiplier unit (ST_MU..ST_MUW); every other
3322  // integer func code that reaches here answers 0.
3323  function signed [63:0] func_f(input [7:0] op, input signed [63:0] a, input signed [63:0] x);
3324    begin
3325      func_f = 64'sd0;
3326    end
3327  endfunction
3328"#;
3329
3330/// The proven bit-exact sequential IEEE-754 f64 FPU (`fpu.v` beside this module; `benchmarks/silicon/fpu.v` is
3331/// its golden copy for the FPGA flows) — `fp_addsub`/`fp_mul`/`fp_div`/
3332/// `fp_sqrt` (start/done handshake) + combinational `fp_cmp`. Appended verbatim after the engine module so the
3333/// over-the-wire SoC can instantiate the float units; the engine's 64-bit register file / heap already carry f64
3334/// bit patterns, so the FPU is the only float hardware the engine needs. Bit-exact vs Rust `f64` over 41,521
3335/// vectors (its own `verify-fpu.sh`), and the exponent path is timing-safe (see the `fp_div` pipeline lock).
3336pub const FPU_V: &str = include_str!("fpu.v");
3337
3338/// Emit the UNIVERSAL over-the-wire whole-program engine SoC: one fixed netlist that RECEIVES any fitting
3339/// program's flat image + params over a UART, fills its on-chip config memories (the loader FSM), seeds the
3340/// register file from the params (INIT), runs the interpret st-machine (== `eval_wprog_config` == the VM), and
3341/// streams the 64-bit result + run-cycle count back over the UART — then loops for the next program. The engine
3342/// core (block-record wires, `resolve`/`resolve_arg`, the `case(st)` walk) is the SAME logic proven in
3343/// `emit_wprog_engine_verilog`; only the config source (wire vs `$readmemh`) and the result path (UART vs port)
3344/// differ. Integer functional units (synthesizable — the sequential div/mod/floordiv/pow datapath lives in the
3345/// template; `func_f` is the DSP multiplies only). `CLKS_PER_BIT` sizes the UART bit period.
3346pub fn emit_wprog_engine_soc_verilog(name: &str) -> String {
3347    let template = engine_template(false);
3348    let engine = template
3349        .replace("__NAME__", name)
3350        .replace("__INTEGER_FUNC_F__", MUL_FUNC_F);
3351    // Excise the DEBUG_READBACK-only regions (this raw-replace path never declares the `DEBUG_READBACK`
3352    // parameter, so the readback FSM's `DEBUG_READBACK` references would fail elaboration otherwise). With them
3353    // gone the emitted RTL is byte-identical to before the readback feature. The CFGBRAM regions stay (their
3354    // `CFG_BRAM` parameter IS declared with default 0 and folds away) — matching this path's prior behaviour.
3355    let engine = strip_debug_readback_regions(&engine, false);
3356    // Excise the RDW_DETECT-only collision-detector regions too, so this path stays byte-identical to before the
3357    // detector feature (the `RDW_DETECT` parameter folds to 0 and its region body is a dead diagnostic cone).
3358    let engine = strip_marker_regions(
3359        &engine,
3360        "//__RDW_DETECT_ONLY_BEGIN__",
3361        "//__RDW_DETECT_ONLY_END__",
3362        false,
3363    );
3364    let engine = select_memory_regions(&engine, &[]);
3365    format!("{engine}\n{FPU_V}")
3366}
3367
3368/// A self-checking testbench that acts as the OVER-THE-WIRE HOST: it `$readmemh`s the concatenated wire bytes
3369/// (`wire_<name>.hex`), per-vector byte lengths (`lens_<name>.hex`), and the `eval_wprog_config` oracle
3370/// (`oracle_<name>.hex`), then for each vector bit-bangs its bytes onto `uart_rx`, decodes the returned frame
3371/// (`0xA5` sync + `result[63:0]` + `cycles[31:0]` + `0x5A`), and checks the result == the oracle. Proves the WHOLE
3372/// wire→synthesize→run→wire path ≡ the VM under iverilog.
3373pub fn emit_wprog_engine_soc_testbench(
3374    name: &str,
3375    n_vectors: usize,
3376    clks_per_bit: usize,
3377    max_bytes: usize,
3378) -> String {
3379    emit_wprog_engine_soc_testbench_board(name, n_vectors, clks_per_bit, max_bytes, false)
3380}
3381
3382/// [`emit_wprog_engine_soc_testbench`] on a board with the PSRAM (`psram`, for `wpe_offchip`), whose
3383/// testbench also wires the part's pads, watches every received byte and leaves `__HALF_NS__` to fill.
3384pub(crate) fn emit_wprog_engine_soc_testbench_board(
3385    name: &str,
3386    n_vectors: usize,
3387    clks_per_bit: usize,
3388    max_bytes: usize,
3389    psram: bool,
3390) -> String {
3391    let template = fill_verilog(include_str!("tb_wpe_soc_template.v"));
3392    let template = strip_marker_regions(
3393        &template,
3394        "//__OFFCHIP_ANY_BEGIN__",
3395        "//__OFFCHIP_ANY_END__",
3396        psram,
3397    );
3398    strip_marker_regions(
3399        &template,
3400        "//__ONCHIP_ALL_BEGIN__",
3401        "//__ONCHIP_ALL_END__",
3402        !psram,
3403    )
3404    .replace("__NAME__", name)
3405    .replace("__NVEC__", &n_vectors.to_string())
3406    .replace("__CLKS__", &clks_per_bit.to_string())
3407    .replace("__MAXBYTES__", &max_bytes.to_string())
3408}
3409
3410// ── The SYNTHESIZABLE over-the-wire engine (real Nexys A7 bitstream) ────────────────────────────────────────
3411// Two adjustments vs the iverilog engine make it FPGA-buildable through the open flow (yosys→nextpnr-xilinx):
3412//  1. `func_f` is `MUL_FUNC_F` — the DSP multiplies (MulHiU/MulOvfS) only. The combinational 64-bit divider
3413//     (`a/x`, `a%x` — which yosys can't map in bounded time) and the runtime-bounded `Pow` loop are GONE;
3414//     Div/Mod/FloorDiv/Pow run instead on the template's SEQUENTIAL datapath (st==6, a magnitude-restoring
3415//     divider + repeat-multiply), so the full integer arithmetic surface synthesizes. The float lane is the FPU
3416//     hardware follow-on (fitting rejects float/popcount func codes).
3417//  2. A smaller cap profile (`WPE_SYNTH_*`) so every UART-written config memory can be forced to REGISTERS
3418//     (`ram_style="registers"`) — the open flow silently drops distributed-RAM writes, so the streamed config
3419//     must live in flip-flops. Full-cap registers would exceed the device; this profile fits (~23k FF / ~31k LUT).
3420
3421pub const WPE_SYNTH_NREG: usize = 16;
3422pub const WPE_SYNTH_MAXBLK: usize = 8;
3423pub const WPE_SYNTH_MAXCELL: usize = 32;
3424pub const WPE_SYNTH_MAXWR: usize = 32;
3425pub const WPE_SYNTH_MAXARG: usize = 8;
3426pub const WPE_SYNTH_MAXLIV: usize = 16;
3427pub const WPE_SYNTH_MAXK: usize = 32;
3428pub const WPE_SYNTH_HEAP: usize = 64;
3429pub const WPE_SYNTH_DEPTH: usize = 8;
3430
3431// The DEPLOYED Nexys A7-100T cap profile — VERIFIED on real silicon. One hard synthesis constraint shapes
3432// MAXCELL: yosys `synth_xilinx` MIS-SYNTHESIZES the loop's register-vs-register compare below MAXCELL=32 (at
3433// MAXCELL=16 the loop silently corrupts once the real `func_f` DSP logic is present, even though a func_f-stubbed
3434// gate-sim passes), so MAXCELL stays 32. HEAP=64/DEPTH=8 is the FULL profile: the BRAM redesign (heap/`sfile` →
3435// block RAM, ~11.8k FF + 2 RAMB36, ≡VM, locked by `the_big_engine_fits_the_hold_safe_ff_budget_via_block_ram`)
3436// moves the FF-heavy state off flip-flops so the big engine both HOLDS (under the ~12.7k threshold) AND ROUTES on
3437// the open flow — proven on Board B streaming depth-8 recursion (f(8)=36) + loops/collatz over the wire
3438// (`wpe_soc_big.bit`, coreclk 16-24 MHz PASS). The deployed hw engine sets bram=true. (The earlier "does not
3439// route" was a mis-diagnosis: a bloated 92-RAMB build variant + a good build killed early — the correct 2-RAMB /
3440// 30%-LUT netlist routes given the full nextpnr grind.)
3441pub const WPE_HW_NREG: usize = 16;
3442pub const WPE_HW_MAXBLK: usize = 8;
3443pub const WPE_HW_MAXCELL: usize = 32;
3444pub const WPE_HW_MAXWR: usize = 32;
3445pub const WPE_HW_MAXARG: usize = 8;
3446pub const WPE_HW_MAXLIV: usize = 16;
3447pub const WPE_HW_MAXK: usize = 32;
3448pub const WPE_HW_HEAP: usize = 64; // block RAM (routes + holds on silicon — verified)
3449pub const WPE_HW_DEPTH: usize = 8; // block RAM (depth-8 recursion runs on the board — verified)
3450
3451/// Returns true iff `cfg` fits the DEPLOYED hold-safe hardware engine's cap profile (`WPE_HW_*`) — the tighter
3452/// bounds the real bitstream can hold. Programs must satisfy these to run over the wire on silicon.
3453pub fn wprog_config_fits_hw_engine(cfg: &WProgConfig) -> Result<(), String> {
3454    if cfg.blocks.len() > WPE_HW_MAXBLK {
3455        return Err(format!(
3456            "{} blocks > hw MAXBLK={WPE_HW_MAXBLK}",
3457            cfg.blocks.len()
3458        ));
3459    }
3460    if wprog_nregs(cfg) > WPE_HW_NREG {
3461        return Err(format!(
3462            "{} registers > hw NREG={WPE_HW_NREG}",
3463            wprog_nregs(cfg)
3464        ));
3465    }
3466    let total_cells: usize = cfg.blocks.iter().map(|b| b.cells.len()).sum();
3467    if total_cells > WPE_HW_MAXCELL {
3468        return Err(format!("{total_cells} cells > hw MAXCELL={WPE_HW_MAXCELL}"));
3469    }
3470    // The hw ENGINE carries the IEEE-754 FPU (the sim netlist proves float programs on it); whether a given BOARD
3471    // image has it is a wrapper decision (`nexys_wpe_soc.v` WITH_FPU) recorded in the bitstream's recipe, and the
3472    // host refuses float programs for an `FPU=0` image from that record (`emit_wpe_soc --caps FPU=0`).
3473    for b in &cfg.blocks {
3474        for c in &b.cells {
3475            if c.kind == 2 && !matches!(c.code, 0..=10 | 12..=20) {
3476                return Err(format!(
3477                    "Func cell code {} (ApproxEqF/unknown) is the remaining float follow-on; the hw engine has integer + \
3478                     the IEEE-754 FPU (add/sub/mul/div/sqrt/floor/compare/convert)",
3479                    c.code
3480                ));
3481            }
3482        }
3483    }
3484    Ok(())
3485}
3486
3487// ── WPE_STREAM: the whole STREAMED-WEIGHTS transformer's cap profile ─────────────────────────────────────
3488// The whole baby transformer runs on ONE chip when its ~3,136 weights are STREAMED (pre-loaded into the heap
3489// via `loaded()`, the DATA-segment model) instead of Push-built into the config. Streaming removes the giant
3490// 18,934-cell weight-load block, so the config collapses to flip-flop scale (measured on the generated net:
3491// 158 blocks / 124 registers / 556 cells / 644 writes / 258 live-ins / 0 call-args / 41 constants) and only
3492// the runtime heap (right-sized to ~18,300 words by `with_seq_reserve(8)`) needs block RAM. Most caps size
3493// the emitted RTL's config memories with a SMALL headroom above those measured counts — MAXBLK 160 (actual
3494// 158), MAXWR 704 (actual 644), MAXLIV 288 (actual 258) — tightened so the still-combinational `wr_*`/`blk_*`/
3495// `liv` read muxes shrink, easing open-flow placement; the heap is the block-RAM state plane (`MemBackend::Bram`).
3496// Proven ≡VM as the engine model by `wpe_transformer_lock`'s streamed leg.
3497//
3498// SIZED FOR THE WHOLE FAMILY, NOT JUST THE TRANSFORMER. The engine is the ONE netlist that runs EVERY program
3499// fitting these bounds — so the register/const/per-block caps carry generous headroom above the transformer's
3500// own counts. A straight-line block N deep (e.g. N heap reads then an N-term running sum) lowers register→SSA
3501// to ~5N/2 SSA values, cells, and writes and ~N constants — with NO register coalescing (one FU per SSA value
3502// is the dataflow model), so those dimensions grow FAR past the transformer's. The `postsynth_high_register_
3503// index_matches_vm` lock's 90-value sum is such a program: 453 registers / 270 per-block cells / 452 per-block
3504// writes / 91 constants. `NREG` (register file `file` + write buffer `wbuf` depth), `MAXK` (const table), and
3505// `MAXV` (per-block value scratch `v`) are therefore sized to 512/128/512 — a clean power-of-two register file
3506// (proven to round-trip all indices distinctly at the synthesized netlist level, RAMB18 SDP-36, no mis-wire)
3507// that covers such programs. The `unchecked` image builder (used by `emit_transformer_wire` and the postsynth
3508// locks) does NOT clamp to these, so a program that OVERFLOWS a cap silently aliases its own registers/cells —
3509// which is exactly the corruption `postsynth_high_register_index_matches_vm` caught (453 > the old NREG 128).
3510pub const WPE_STREAM_NREG: usize = 512;
3511pub const WPE_STREAM_MAXBLK: usize = 160;
3512pub const WPE_STREAM_MAXCELL: usize = 1024;
3513pub const WPE_STREAM_MAXWR: usize = 704;
3514pub const WPE_STREAM_MAXARG: usize = 24;
3515pub const WPE_STREAM_MAXLIV: usize = 288;
3516/// Const-table depth (`konst[0:MAXK-1]`). The transformer uses 41; a wider straight-line program (the postsynth
3517/// high-register lock's 90-value sum) needs 91 distinct constants, so MAXK=128 covers the family. The `unchecked`
3518/// image builder skips the `konst.len() <= MAXK` guard, so an over-MAXK program would silently read a nonexistent
3519/// const slot — 91 > the old 64 was one of the four caps the high-register lock overflowed.
3520pub const WPE_STREAM_MAXK: usize = 128;
3521/// Heap depth (words) for the streamed engine. Sized to `2^15 = 32768` — a POWER OF TWO that maps to exactly ONE
3522/// RAMB36E1 depth-bank at ×1 width (a single RAMB36 is 32768×1), so the whole 15-bit `heap_ra` addresses one clean
3523/// tile with NO depth-cascade. This dodges a yosys 0.33 `synth_xilinx` mis-map: for a heap whose depth is NOT a
3524/// clean single-tile fit (e.g. the old 20480), `memory_libmap` picks a wide tile (×9 → 4096 deep) and then FAILS
3525/// to depth-cascade — it wires every tile to the SAME 12-bit address (`heap_ra[11:0]`) and drops the top address
3526/// bits (`heap_ra[14:12]`), so on silicon (and in post-synth gate-level sim) any heap read/write to an address
3527/// >= 4096 aliases into the first 4096-word tile and returns garbage. The whole-program transformer's heap grows
3528/// past 4096, so it read uninitialized cells and returned the wrong result on the XC7A200T. 32768 >= the model's
3529/// working-set (was 20480), fits the BRAM budget, and — being a single-tile power of two — maps with the full
3530/// 15-bit address on every port. Proven at the netlist level (the synthesized 20480 heap wires `heap_ra[11:0]`
3531/// and drops the top bits; the 32768 heap wires the full `heap_ra`) and by a post-synth gate-level RAMB sim.
3532///
3533/// DEPTH-BANKING (silicon fix below the netlist). A 32768-deep heap half at ×1 mapped to a RAMB36E1 in ×1-width,
3534/// 32768-deep mode — whose INTERNAL two-16K-half depth-cascade (address bit 15 tied high on the upper tiles) is
3535/// the least-fuzzed RAMB corner. prjxray/openXC7 mis-encodes it on the XC7A200T (a small read-only probe reads
3536/// wrong by ≈1.5·N) even though the gate-level netlist is ≡VM — the defect is BELOW the netlist. The template
3537/// therefore DEPTH-BANKS the heap into two 16384-deep banks (`heap_b0_*`/`heap_b1_*`, selected by heap-address
3538/// bit 14; in-bank index = bits `[13:0]`) so each maps to a 14-bit-indexed tile with NO 32K depth-cascade — a
3539/// better-fuzzed primitive. Behaviorally IDENTICAL to a flat 32768 heap. HEAP stays 32768 (two 16384 banks).
3540pub const WPE_STREAM_HEAP: usize = 32768;
3541/// Call-frame depth. Sized from the MEASURED peak across the benchmark suite (26, driven by `fib`), rounded to
3542/// a power of two — not from the transformer, which is call-free and needs none. This was 4 while the smaller
3543/// XC7A100T profile was 8, and because the wire image carries no record of the profile it was built for, a
3544/// depth-8 program streamed to the depth-4 engine ran and returned a plausible wrong number: on silicon
3545/// `bench_fib` gave 8 instead of 21, `wpe_rec8` 20 instead of 36, `wpe_recursion` 11 instead of 15. Both
3546/// `wpe_cap_profile_lock` (no dimension may shrink between profiles) and
3547/// `the_stream_engine_profile_covers_every_benchmark_in_the_suite` (the suite's measured ceiling) now pin it.
3548pub const WPE_STREAM_DEPTH: usize = 32;
3549/// Depth of the per-block cell-value scratch array `v` (`v[0:MAXV-1]`). `v` is indexed ONLY by a cell's position
3550/// WITHIN ITS BLOCK (mode-2 operands + the `v[ci]` write), never by the flat `blk_cell_ptr + ci`, so it needs to
3551/// hold only the MAX cells in any SINGLE block — not the whole-program `MAXCELL` total. The streamed transformer's
3552/// largest block is 116 cells; a wider straight-line block (the postsynth high-register lock's 90-value sum lowers
3553/// to a single 270-cell block) needs more, so MAXV=512 covers the family. Under `ConfigBram` (the streamed engine
3554/// + every postsynth lock) `v` is BLOCK RAM with a registered read (`v_ra`/`v_rd`), so the depth costs a RAMB tile,
3555/// not a combinational mux; the flip-flop-backed combinational `resolve`/`resolve0` reads only exist under the
3556/// `Registers`/`Bram` backends, which use the SMALL `WPE_SYNTH_*` profile — untouched by this. A block deeper than
3557/// MAXV aliases its own cells (the corruption `postsynth_high_register_index_matches_vm` caught: 270 > the old 128).
3558pub const WPE_STREAM_MAXV: usize = 512;
3559
3560/// The `WPE_STREAM_*` profile as the `[usize; 10]` caps array `emit_wprog_engine_soc_verilog_backend` expects
3561/// (order: NREG, MAXBLK, MAXCELL, MAXWR, MAXARG, MAXLIV, MAXK, HEAP, DEPTH, MAXV).
3562pub const WPE_STREAM_CAPS: [usize; 10] = [
3563    WPE_STREAM_NREG,
3564    WPE_STREAM_MAXBLK,
3565    WPE_STREAM_MAXCELL,
3566    WPE_STREAM_MAXWR,
3567    WPE_STREAM_MAXARG,
3568    WPE_STREAM_MAXLIV,
3569    WPE_STREAM_MAXK,
3570    WPE_STREAM_HEAP,
3571    WPE_STREAM_DEPTH,
3572    WPE_STREAM_MAXV,
3573];
3574
3575/// Returns `Ok` iff `cfg` fits the `WPE_STREAM_*` profile (the whole streamed transformer's engine). Every
3576/// dimension is a program TOTAL (the flat config memories accumulate across blocks). Named-loudly on overflow
3577/// so a growing model is caught before it silently corrupts the flat config image.
3578pub fn wprog_config_fits_stream_engine(cfg: &WProgConfig) -> Result<(), String> {
3579    let checks: [(usize, usize, &str); 5] = [
3580        (cfg.blocks.len(), WPE_STREAM_MAXBLK, "blocks"),
3581        (wprog_nregs(cfg), WPE_STREAM_NREG, "registers"),
3582        (
3583            cfg.blocks.iter().map(|b| b.cells.len()).sum(),
3584            WPE_STREAM_MAXCELL,
3585            "cells",
3586        ),
3587        (
3588            cfg.blocks.iter().map(|b| b.writes.len()).sum(),
3589            WPE_STREAM_MAXWR,
3590            "writes",
3591        ),
3592        (
3593            cfg.blocks.iter().map(|b| b.livein.len()).sum(),
3594            WPE_STREAM_MAXLIV,
3595            "live-ins",
3596        ),
3597    ];
3598    for (got, cap, what) in checks {
3599        if got > cap {
3600            return Err(format!("{got} {what} > stream engine cap {cap}"));
3601        }
3602    }
3603    // The per-block value array `v` (depth MAXV) is indexed WITHIN a block, so the binding dimension is the MAX
3604    // cells in any single block, not the whole-program total. A block deeper than MAXV would alias its own cells
3605    // and corrupt the walk — reject it loudly here, before it can silently mis-run.
3606    let max_block_cells = cfg.blocks.iter().map(|b| b.cells.len()).max().unwrap_or(0);
3607    if max_block_cells > WPE_STREAM_MAXV {
3608        return Err(format!(
3609            "a block has {max_block_cells} cells > stream engine per-block cap MAXV={WPE_STREAM_MAXV}"
3610        ));
3611    }
3612    Ok(())
3613}
3614
3615/// Returns true iff `cfg` fits the SYNTHESIZABLE engine's smaller cap profile (a superset check of the generic
3616/// engine's fit): every over-the-wire program synthesized to hardware must satisfy these or it would overflow
3617/// the register-backed config memories. The integer func units — Div/Mod/FloorDiv/Pow (sequential datapath) and
3618/// MulHiU/MulOvfS (DSP) — all synthesize; only the float/popcount codes are rejected (the FPU follow-on).
3619pub fn wprog_config_fits_synth_engine(cfg: &WProgConfig) -> Result<(), String> {
3620    if cfg.blocks.len() > WPE_SYNTH_MAXBLK {
3621        return Err(format!(
3622            "{} blocks > synth MAXBLK={WPE_SYNTH_MAXBLK}",
3623            cfg.blocks.len()
3624        ));
3625    }
3626    if wprog_nregs(cfg) > WPE_SYNTH_NREG {
3627        return Err(format!(
3628            "{} registers > synth NREG={WPE_SYNTH_NREG}",
3629            wprog_nregs(cfg)
3630        ));
3631    }
3632    let total_cells: usize = cfg.blocks.iter().map(|b| b.cells.len()).sum();
3633    if total_cells > WPE_SYNTH_MAXCELL {
3634        return Err(format!(
3635            "{total_cells} cells > synth MAXCELL={WPE_SYNTH_MAXCELL}"
3636        ));
3637    }
3638    for b in &cfg.blocks {
3639        for c in &b.cells {
3640            if c.kind == 2 && !matches!(c.code, 0..=10 | 12..=20) {
3641                return Err(format!("Func cell code {} (ApproxEqF/unknown) is the remaining float follow-on; the synth engine has integer + the IEEE-754 FPU (add/sub/mul/div/sqrt/floor/int→float/compares/popcount)", c.code));
3642            }
3643        }
3644    }
3645    Ok(())
3646}
3647
3648/// Emit the SYNTHESIZABLE over-the-wire engine SoC (real Nexys A7 bitstream): the same universal loader/INIT/
3649/// interpret/TX logic as [`emit_wprog_engine_soc_verilog`], but with `SYNTH_FUNC_F`, the `WPE_SYNTH_*` cap
3650/// profile, and `(* ram_style="registers" *)` forced on every config/state memory so the open flow keeps the
3651/// UART-streamed config in flip-flops. Proven ≡ `eval_wprog_config` for fitting programs under iverilog, then
3652/// synthesized through yosys→nextpnr-xilinx to a bitstream.
3653pub fn emit_wprog_engine_soc_verilog_synth(name: &str) -> String {
3654    // BRAM: the SYNTH engine's big HEAP=64/DEPTH=8 heap/sfile map to block RAM (11.8k FF, hold-safe in synthesis).
3655    emit_wprog_engine_soc_with_caps(
3656        name,
3657        [
3658            WPE_SYNTH_NREG,
3659            WPE_SYNTH_MAXBLK,
3660            WPE_SYNTH_MAXCELL,
3661            WPE_SYNTH_MAXWR,
3662            WPE_SYNTH_MAXARG,
3663            WPE_SYNTH_MAXLIV,
3664            WPE_SYNTH_MAXK,
3665            WPE_SYNTH_HEAP,
3666            WPE_SYNTH_DEPTH,
3667            WPE_SYNTH_MAXCELL,
3668        ],
3669        true,
3670    )
3671}
3672
3673/// Emit the DEPLOYED hold-safe over-the-wire engine SoC (the real bitstream): same logic as the synth engine,
3674/// but with the `WPE_HW_*` cap profile that both synthesizes the loop's compare correctly under yosys AND stays
3675/// under the board's hold-safe FF threshold. Runs loops/branches/writes, recursion (depth ≤3), and small
3676/// collections over the wire on real silicon; proven ≡ `eval_wprog_config` (RTL) + loop gate-sim ≡VM.
3677pub fn emit_wprog_engine_soc_verilog_hw(name: &str) -> String {
3678    // BRAM: the deployed engine keeps heap/`sfile` in BLOCK RAM (HEAP=64/DEPTH=8) so it both holds (~11.8k FF) AND
3679    // routes on the open flow — proven on Board B (depth-8 recursion + loops streamed over the wire). Same profile
3680    // as the synth engine now: the BRAM redesign made the full-corpus engine deployable on silicon.
3681    emit_wprog_engine_soc_with_caps(
3682        name,
3683        [
3684            WPE_HW_NREG,
3685            WPE_HW_MAXBLK,
3686            WPE_HW_MAXCELL,
3687            WPE_HW_MAXWR,
3688            WPE_HW_MAXARG,
3689            WPE_HW_MAXLIV,
3690            WPE_HW_MAXK,
3691            WPE_HW_HEAP,
3692            WPE_HW_DEPTH,
3693            WPE_HW_MAXCELL,
3694        ],
3695        true,
3696    )
3697}
3698
3699/// The MEMORY BACKEND for the config-driven engine's on-chip memories — a first-class, selectable choice of
3700/// substrate. The whole-program engine holds two kinds of memory: the STATE PLANE (the shared `heap` + the
3701/// recursion `sfile` frame stack) and the CONFIG memories (the UART-streamed program image: `blk_*`, `cell_*`,
3702/// live-ins, writes, constants). Each can live in LUT flip-flops or in block RAM.
3703#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3704pub enum MemBackend {
3705    /// Everything in LUT flip-flops: combinational reads, smallest latency, but the capacity is bounded by the
3706    /// FF budget (the `WPE_SYNTH_*` profile). The open-flow-safe default for tiny programs.
3707    Registers,
3708    /// The STATE PLANE (`heap` + `sfile`) in BLOCK RAM (registered reads, 1-cycle latency absorbed by the FSM's
3709    /// wait-states); config stays in flip-flops. Moves the FF-heavy state off flip-flops so a bigger program
3710    /// both HOLDS and ROUTES on the open flow — the DEPLOYED silicon backend (`wpe_soc_big.bit`, proven Board B).
3711    Bram,
3712    /// State plane AND the config image in BLOCK RAM — the large-capacity backend that lets the whole 158-block
3713    /// transformer fit one chip's BRAM. Requires registered CONFIG reads (block/cell fetch wait-states) — the
3714    /// interpret-FSM extension that is the remaining build; the BRAM budget already fits (4 Mb of 4.9 Mb).
3715    ConfigBram,
3716}
3717
3718/// Shared body: instantiate the engine template with `MUL_FUNC_F` (DSP multiplies; div/mod/floordiv/pow are the
3719/// template's sequential datapath), a cap profile `[NREG, MAXBLK, MAXCELL, MAXWR, MAXARG, MAXLIV, MAXK, HEAP,
3720/// DEPTH, MAXV]`, and the chosen memory backend (which config/state memories map to block RAM vs flip-flops).
3721///
3722/// A caller that wants an explicit backend uses [`emit_wprog_engine_soc_verilog_backend`]. The `bram: bool`
3723/// overload is kept as a thin shim (`true` = [`MemBackend::Bram`], `false` = [`MemBackend::Registers`]).
3724fn emit_wprog_engine_soc_with_caps(name: &str, caps: [usize; 10], bram: bool) -> String {
3725    emit_wprog_engine_soc_with_backend(
3726        name,
3727        caps,
3728        if bram {
3729            MemBackend::Bram
3730        } else {
3731            MemBackend::Registers
3732        },
3733        false,
3734        false,
3735        &[],
3736    )
3737}
3738
3739/// Emit the over-the-wire engine with an EXPLICIT memory backend — the first-class "choose the substrate" entry
3740/// point. `Registers`/`Bram` are the proven paths; `ConfigBram` additionally maps the config image to block RAM
3741/// (its registered-read FSM is the in-progress large-capacity build). `caps[9]` (MAXV) sizes the per-block
3742/// value array `v` independently of the whole-program `MAXCELL` total.
3743pub fn emit_wprog_engine_soc_verilog_backend(
3744    name: &str,
3745    caps: [usize; 10],
3746    backend: MemBackend,
3747) -> String {
3748    emit_wprog_engine_soc_with_backend(name, caps, backend, false, false, &[])
3749}
3750
3751/// Emit the over-the-wire engine with an EXPLICIT backend AND the SIM-ONLY read-during-write collision detectors
3752/// active (`RDW_DETECT=1`). The detectors compare each memory's registered-read address (`heap_ra`/`sf_ra`/
3753/// `file_ra`/`v_ra`) against every same-cycle write address during the interpret phase and `$display` +
3754/// count a `RDW-COLLISION` whenever they coincide — a cross-port read-during-write that a real RAMB36 returns
3755/// UNDEFINED for but this behavioral sim idealizes as READ_FIRST (old data). This is a DIAGNOSTIC build only;
3756/// `rdw_detect=false` (every other entry point) EXCISES the whole detector region so the shipped netlist is
3757/// BYTE-IDENTICAL to before this feature (the `RDW_DETECT` parameter is not even injected into the header).
3758pub fn emit_wprog_engine_soc_verilog_backend_rdw(
3759    name: &str,
3760    caps: [usize; 10],
3761    backend: MemBackend,
3762) -> String {
3763    emit_wprog_engine_soc_with_backend(name, caps, backend, false, true, &[])
3764}
3765
3766/// Emit the over-the-wire engine with an EXPLICIT memory backend AND an on-chip READBACK debug mode. When
3767/// `debug_readback` is true the engine, at the load→run boundary (P_CLR done), FIRST streams a DEBUG FRAME back
3768/// over the SAME UART tx — a fixed diagnostic slice of each major memory — then hands off to the interpret FSM,
3769/// so real silicon can be compared against the known config-image/heap values byte-for-byte. The DEBUG FRAME is:
3770/// sync `0xD8`, then for each memory in order {cell_kind, cell_op0, cell_op1, blk_term_kind, heap} entries
3771/// `[0..8)`, each as a 64-bit BIG-ENDIAN word (8 bytes MSB-first, matching the result frame's byte order), then
3772/// end byte `0x8D` — 1 + 5*8*8 + 1 = 322 bytes. The debug FSM walks each address through a REGISTERED BRAM read
3773/// (issue → wait the latency → capture) and reuses the existing tx byte-sender. `debug_readback=false` emits RTL
3774/// BYTE-IDENTICAL to [`emit_wprog_engine_soc_verilog_backend`] (the whole debug region is excised + the parameter
3775/// is not injected). Readback is only meaningful on the `ConfigBram` streamed engine — asserted here.
3776pub fn emit_wprog_engine_soc_verilog_backend_debug(
3777    name: &str,
3778    caps: [usize; 10],
3779    backend: MemBackend,
3780    debug_readback: bool,
3781) -> String {
3782    assert!(
3783        !debug_readback || matches!(backend, MemBackend::ConfigBram),
3784        "DEBUG_READBACK is a diagnostic for the ConfigBram streamed engine (its config/heap memories are the \
3785         block RAMs a synthesis mis-map corrupts); it is not defined for the flip-flop backends"
3786    );
3787    emit_wprog_engine_soc_with_backend(name, caps, backend, debug_readback, false, &[])
3788}
3789
3790/// The engine with an explicit backend and the memories in `off_chip` behind the PSRAM edge, for
3791/// `wpe_offchip`, which fills the `//__OFFCHIP_GLUE__` line and appends the modules it names.
3792pub(crate) fn emit_wprog_engine_soc_offchip_body(
3793    name: &str,
3794    caps: [usize; 10],
3795    backend: MemBackend,
3796    off_chip: &[ArrayRole],
3797) -> String {
3798    emit_wprog_engine_soc_with_backend(name, caps, backend, false, false, off_chip)
3799}
3800
3801fn emit_wprog_engine_soc_with_backend(
3802    name: &str,
3803    caps: [usize; 10],
3804    backend: MemBackend,
3805    debug_readback: bool,
3806    rdw_detect: bool,
3807    off_chip: &[ArrayRole],
3808) -> String {
3809    emit_wprog_engine_soc_with_probes(name, caps, backend, debug_readback, rdw_detect, off_chip, false)
3810}
3811
3812/// The engine template with its protocol bytes filled, and its `PROBE_ONLY` regions — the observer ports
3813/// `probe_{running, st, pc, cell, sp, hp, cycles}` and their assignments — kept or excised. Excised, the engine is
3814/// byte-identical to one emitted before the probes existed.
3815fn engine_template(probes: bool) -> String {
3816    strip_marker_regions(
3817        &fill_verilog(include_str!("wpe_soc_template.v")),
3818        "//__PROBE_ONLY_BEGIN__",
3819        "//__PROBE_ONLY_END__",
3820        probes,
3821    )
3822}
3823
3824/// The deployed hold-safe engine ([`emit_wprog_engine_soc_verilog_hw`]) with its observer ports: the same logic and
3825/// caps, plus `probe_{running, st, pc, cell, sp, hp, cycles}` outputs wired straight from the engine's own registers, for
3826/// the Fabric Terminal's activity panel.
3827pub fn emit_wprog_engine_soc_verilog_hw_probed(name: &str) -> String {
3828    emit_wprog_engine_soc_with_probes(
3829        name,
3830        [
3831            WPE_HW_NREG,
3832            WPE_HW_MAXBLK,
3833            WPE_HW_MAXCELL,
3834            WPE_HW_MAXWR,
3835            WPE_HW_MAXARG,
3836            WPE_HW_MAXLIV,
3837            WPE_HW_MAXK,
3838            WPE_HW_HEAP,
3839            WPE_HW_DEPTH,
3840            WPE_HW_MAXCELL,
3841        ],
3842        MemBackend::Bram,
3843        false,
3844        false,
3845        &[],
3846        true,
3847    )
3848}
3849
3850fn emit_wprog_engine_soc_with_probes(
3851    name: &str,
3852    caps: [usize; 10],
3853    backend: MemBackend,
3854    debug_readback: bool,
3855    rdw_detect: bool,
3856    off_chip: &[ArrayRole],
3857    probes: bool,
3858) -> String {
3859    let [nreg, maxblk, maxcell, maxwr, maxarg, maxliv, maxk, heap, depth, maxv] = caps;
3860    let base = engine_template(probes);
3861    let mut s = base
3862        .replace("__NAME__", name)
3863        .replace("__INTEGER_FUNC_F__", MUL_FUNC_F)
3864        .replace(
3865            "localparam NREG=48, MAXBLK=16, MAXCELL=64, MAXWR=64, MAXARG=16, MAXLIV=48, MAXK=64, HEAP=128, DEPTH=24, MAXV=64;",
3866            &format!(
3867                "localparam NREG={nreg}, MAXBLK={maxblk}, MAXCELL={maxcell}, MAXWR={maxwr}, MAXARG={maxarg}, MAXLIV={maxliv}, MAXK={maxk}, HEAP={heap}, DEPTH={depth}, MAXV={maxv};"
3868            ),
3869        );
3870    // Under `ConfigBram` the config image (`cell_*`) is block RAM with a REGISTERED read, so the interpret FSM
3871    // must route the cell walk through the one-cycle cell-fetch wait-state (`ST_CF`). That path is guarded by the
3872    // module parameter `CFG_BRAM`; bake its DEFAULT to 1 so every instantiation (incl. the testbench, which does
3873    // not override it) takes the registered-fetch path. `Registers`/`Bram` leave the default 0 → identical to today.
3874    if matches!(backend, MemBackend::ConfigBram) {
3875        s = s.replace("parameter CFG_BRAM = 0", "parameter CFG_BRAM = 1");
3876    }
3877    // DEBUG_READBACK: inject the compile-time parameter into the module header (mirroring how the ConfigBram
3878    // default is flipped by `.replace`), so a normal build's header stays byte-identical (no injection). Baked to
3879    // 1 as the default so a testbench that does not override it still takes the readback path. Only ConfigBram
3880    // reaches here (asserted in the public entry), so the injection anchors on the CFG_BRAM=1 header text.
3881    if debug_readback {
3882        s = s.replace(
3883            "parameter WITH_FPU = 1, parameter CFG_BRAM = 1, parameter RDW_DETECT = 0) (",
3884            "parameter WITH_FPU = 1, parameter CFG_BRAM = 1, parameter RDW_DETECT = 0, parameter DEBUG_READBACK = 1) (",
3885        );
3886    }
3887    // RDW_DETECT: flip the sim-only read-during-write collision-detector parameter's default to 1 for a
3888    // diagnostic build (so a testbench that does not override it takes the detector path). A normal build leaves
3889    // it 0 and — crucially — has the whole detector region EXCISED below, so its header + body stay byte-identical.
3890    if rdw_detect {
3891        s = s.replace("parameter RDW_DETECT = 0", "parameter RDW_DETECT = 1");
3892    }
3893    // Excise the RDW_DETECT-only regions (the collision-detector shadow regs, counters, per-site shadow sets, and
3894    // the bottom-of-always check) unless this is an RDW diagnostic build — keeping the shipped netlist
3895    // byte-identical to before this feature (and never referencing `RDW_DETECT`).
3896    let s = strip_marker_regions(
3897        &s,
3898        "//__RDW_DETECT_ONLY_BEGIN__",
3899        "//__RDW_DETECT_ONLY_END__",
3900        rdw_detect,
3901    );
3902    // Keep one home for each memory that can leave the die: its on-chip array, or the port to its cache
3903    // behind the PSRAM edge, frozen with the rest of the FSM by `mstall`.
3904    let s = select_memory_regions(&s, off_chip);
3905    // Excise the DEBUG_READBACK-only regions (the debug read ports, state regs, P_DBG phase, and P_CLR detour)
3906    // unless this is a debug build — keeping the normal netlist byte-identical to before this feature.
3907    let s = strip_debug_readback_regions(&s, debug_readback);
3908    // The register-file/cell-value BRAM machinery (the `file_ra`/`file_rd`/`v_ra`/`v_rd` read ports, the `wbuf`
3909    // read-before-write buffer, the registered `file_rd`/`v_rd` reads, and the ST_OP.../ST_AG. fetch wait-states)
3910    // is used ONLY under CFG_BRAM=1. Under the other backends every one of those states is unreachable and every
3911    // one of those regs is dead — but leaving the DECL + logic in place gives the open synthesis flow's tech
3912    // mapper (ABC) a large dead cone to chew on before DCE removes it, blowing the CFG_BRAM=0 synth from ~2 min
3913    // to 10+ min. So we physically EXCISE the `//__CFGBRAM_ONLY_BEGIN__`…`//__CFGBRAM_ONLY_END__` regions from
3914    // the emitted RTL for non-ConfigBram backends — making the CFG_BRAM=0 netlist byte-identical to before this
3915    // feature. Under ConfigBram we keep the region bodies and only drop the marker comment lines. The shared FSM's
3916    // `if (CFG_BRAM) st <= ST_*` branches reference these states, but those localparams are OUTSIDE the excised
3917    // regions (kept) and the branches themselves fold away under CFG_BRAM=0, so the excision leaves valid RTL.
3918    let config_bram = matches!(backend, MemBackend::ConfigBram);
3919    let s = strip_cfgbram_regions(&s, config_bram);
3920    // Choose each memory array's substrate from the backend — no hard-wiring. The STATE PLANE (`heap`/`sfile`)
3921    // reads through registered ports (`heap_rd`/`sf_rd`) whose 1-cycle latency the FSM already absorbs, so it
3922    // maps to BLOCK RAM under `Bram`/`ConfigBram`. The CONFIG image (`blk_*`/`cell_*`/live-ins/writes/consts) is
3923    // registered-read only under `ConfigBram` (the block/cell fetch wait-states in the template) — under the
3924    // other backends it stays flip-flops so its combinational reads are sound.
3925    let state_plane_bram = matches!(backend, MemBackend::Bram | MemBackend::ConfigBram);
3926    let engine = s
3927        .lines()
3928        .map(|line| {
3929            let t = line.trim_start();
3930            if t.starts_with("reg ") && line.contains("[0:") && line.trim_end().ends_with("];") {
3931                let indent = &line[..line.len() - t.len()];
3932                // The array NAME is the identifier just before `[0:` (declarations are column-padded, so match
3933                // the token, not a fixed-space substring).
3934                let arr = t[..t.find("[0:").unwrap()]
3935                    .split_whitespace()
3936                    .last()
3937                    .unwrap_or("");
3938                // `heap` and `sfile` are each split into two 32-bit halves (`sfile_lo`/`sfile_hi`; heap into
3939                // `heap_b*_lo`/`heap_b*_hi`) so neither maps to a single 64-bit RAMB36 in SDP-72 mode (whose
3940                // parity-lane write yosys 0.33 mis-wires — heap reproduced on real Artix-7 silicon, sfile at
3941                // gate level). The heap is ADDITIONALLY depth-banked into two 16384-deep banks (`heap_b0_*`/
3942                // `heap_b1_*`, selected by heap-address bit 14) so it never maps to a RAMB36E1 in ×1-width,
3943                // 32768-deep mode — the internal two-16K-half depth-cascade that prjxray/openXC7 mis-encodes on
3944                // the XC7A200T (correct at gate level, wrong on silicon). Every half of every bank is the SAME
3945                // state-plane substrate the un-split array was.
3946                let is_state_plane = arr == "heap"
3947                    || arr == "heap_lo"
3948                    || arr == "heap_hi"
3949                    || arr == "heap_b0_lo"
3950                    || arr == "heap_b0_hi"
3951                    || arr == "heap_b1_lo"
3952                    || arr == "heap_b1_hi"
3953                    || arr == "sfile"
3954                    || arr == "sfile_lo"
3955                    || arr == "sfile_hi";
3956                // `cell_*` (the `ST_CF` cell-fetch wait-state), `wr_*` (the `ST_WF` write-fetch wait-state), and the
3957                // 13 `blk_*` block-record arrays (the `ST_BF` block-fetch wait-state) each have a REGISTERED read,
3958                // so they may become block RAM under ConfigBram. The remaining config memories (`arg_op`/`liv_reg`/
3959                // `konst`) are still read COMBINATIONALLY — a block-tagged combinational-read array has no valid
3960                // RAMB mapping (yosys errors), and `liv_reg`/`konst` deliberately stay flip-flops to keep `resolve`
3961                // unchanged. They fit in FF once `cell_*`/`wr_*`/`blk_*` (the dominant config memories) are off
3962                // flip-flops; a registered read for `arg_op` is the remaining follow-on.
3963                // The register file `file` and per-block cell-value array `v` are the STATE-PLANE tail of this
3964                // group: under ConfigBram they carry a REGISTERED read (file_rd/v_rd, consumed a cycle later by
3965                // the ST_OP/ST_WR/ST_TR/ST_FS/ST_AG fetch wait-states), so they map to block RAM — dissolving the
3966                // 128:1×64 operand-mux hub the open router could not legalize. Under Registers/Bram they stay
3967                // flip-flops (their reads are combinational there, and `file0` — dead under ConfigBram — always
3968                // stays flip-flops since nothing reads it there).
3969                // `file`/`v` are each split into two 32-bit halves (`file_lo`/`file_hi`, `v_lo`/`v_hi`) so
3970                // neither maps to a single 64-bit RAMB36 in SDP-72 mode (whose parity-lane write yosys 0.33
3971                // mis-wires — gate-level reproduced). Both halves get the SAME registered-config substrate the
3972                // un-split `file`/`v` had (block RAM under ConfigBram, flip-flops otherwise).
3973                let is_registered_config = matches!(
3974                    arr,
3975                    "file_lo"
3976                        | "file_hi"
3977                        | "v_lo"
3978                        | "v_hi"
3979                        | "cell_kind"
3980                        | "cell_code"
3981                        | "cell_op0"
3982                        | "cell_op1"
3983                        | "cell_op2"
3984                        | "cell_op3"
3985                        | "wr_reg"
3986                        | "wr_op"
3987                        | "blk_livein_ptr"
3988                        | "blk_n_livein"
3989                        | "blk_cell_ptr"
3990                        | "blk_n_cells"
3991                        | "blk_write_ptr"
3992                        | "blk_n_writes"
3993                        | "blk_term_kind"
3994                        | "blk_t0"
3995                        | "blk_t1"
3996                        | "blk_top"
3997                        | "blk_cdst"
3998                        | "blk_cargc"
3999                        | "blk_cargp"
4000                );
4001                let to_block = if is_state_plane {
4002                    state_plane_bram
4003                } else if is_registered_config {
4004                    config_bram
4005                } else {
4006                    false
4007                };
4008                let style = if to_block { "block" } else { "registers" };
4009                format!("{indent}(* ram_style = \"{style}\" *) {t}")
4010            } else {
4011                line.to_string()
4012            }
4013        })
4014        .collect::<Vec<_>>()
4015        .join("\n");
4016    // Append the FPU modules raw (the ram_style transform is only for the engine's UART-streamed config memories).
4017    format!("{engine}\n{FPU_V}\n")
4018}
4019
4020/// Excise (or, under ConfigBram, unwrap) the `//__CFGBRAM_ONLY_BEGIN__`…`//__CFGBRAM_ONLY_END__` regions of the
4021/// template. These regions hold the register-file/cell-value BRAM read machinery that only the ConfigBram backend
4022/// exercises. For non-ConfigBram backends we DROP the region bodies entirely (the states are unreachable and the
4023/// regs are dead there) so the CFG_BRAM=0 netlist is byte-identical to before this feature and its open-flow
4024/// synthesis stays fast; for ConfigBram we keep the bodies and drop only the two marker comment lines. The
4025/// markers must nest zero-deep (each BEGIN closed by its END before the next BEGIN) — asserted here so a
4026/// mis-paired marker fails loudly at emit time rather than silently emitting broken RTL.
4027fn strip_cfgbram_regions(src: &str, config_bram: bool) -> String {
4028    strip_marker_regions(
4029        src,
4030        "//__CFGBRAM_ONLY_BEGIN__",
4031        "//__CFGBRAM_ONLY_END__",
4032        config_bram,
4033    )
4034}
4035
4036/// Excise (or, when enabled, unwrap) the `//__DEBUG_READBACK_ONLY_BEGIN__`…`//__DEBUG_READBACK_ONLY_END__`
4037/// regions of the template. These regions hold the on-chip memory-readback debug FSM (the `DEBUG_READBACK`
4038/// parameter's registered debug read ports, its state registers, the `P_DBG` phase, and the P_CLR→P_DBG detour)
4039/// which only a `DEBUG_READBACK=1` diagnostic build exercises. For a normal build we DROP the region bodies
4040/// entirely so the emitted RTL is BYTE-IDENTICAL to before this feature (and never references `DEBUG_READBACK`);
4041/// for a debug build we keep the bodies and drop only the marker comment lines — the SAME discipline as the
4042/// CFG_BRAM gating. The parameter itself is injected into the module header by a `.replace` (mirroring how the
4043/// ConfigBram default is flipped), so a normal build's header is unchanged too.
4044fn strip_debug_readback_regions(src: &str, debug_readback: bool) -> String {
4045    strip_marker_regions(
4046        src,
4047        "//__DEBUG_READBACK_ONLY_BEGIN__",
4048        "//__DEBUG_READBACK_ONLY_END__",
4049        debug_readback,
4050    )
4051}
4052
4053/// The template's region tag for each memory that can leave the die.
4054const OFFCHIP_MEMORIES: [(ArrayRole, &str); 3] = [
4055    (ArrayRole::Heap, "HEAP"),
4056    (ArrayRole::FrameStack, "FRAMES"),
4057    (ArrayRole::ConfigImage, "CELLS"),
4058];
4059
4060/// Keep, for each memory that can leave the die, its `OFFCHIP_<TAG>` regions when `off_chip` names its
4061/// role and its `ONCHIP_<TAG>` regions otherwise; `OFFCHIP_ANY` (the PSRAM pins, the held UART byte, the
4062/// stall gate, the glue site) when anything is off-chip and `ONCHIP_ALL` when nothing is.
4063fn select_memory_regions(src: &str, off_chip: &[ArrayRole]) -> String {
4064    assert!(
4065        off_chip
4066            .iter()
4067            .all(|r| OFFCHIP_MEMORIES.iter().any(|(m, _)| m == r)),
4068        "only the heap, the frame stack and the config image can leave the die: {off_chip:?}"
4069    );
4070    let mut s = src.to_string();
4071    for (role, tag) in OFFCHIP_MEMORIES {
4072        let off = off_chip.contains(&role);
4073        s = strip_marker_regions(
4074            &s,
4075            &format!("//__OFFCHIP_{tag}_BEGIN__"),
4076            &format!("//__OFFCHIP_{tag}_END__"),
4077            off,
4078        );
4079        s = strip_marker_regions(
4080            &s,
4081            &format!("//__ONCHIP_{tag}_BEGIN__"),
4082            &format!("//__ONCHIP_{tag}_END__"),
4083            !off,
4084        );
4085    }
4086    let any = !off_chip.is_empty();
4087    let s = strip_marker_regions(&s, "//__OFFCHIP_ANY_BEGIN__", "//__OFFCHIP_ANY_END__", any);
4088    strip_marker_regions(&s, "//__ONCHIP_ALL_BEGIN__", "//__ONCHIP_ALL_END__", !any)
4089}
4090
4091/// Shared marker-region excision: DROP every line strictly between a `begin`/`end` marker pair when `keep` is
4092/// false (the marker lines themselves are never emitted); when `keep` is true, unwrap the bodies and drop only
4093/// the two marker comment lines. Markers must nest zero-deep (each begin closed by its end before the next
4094/// begin) — asserted so a mis-paired marker fails loudly at emit time rather than silently emitting broken RTL.
4095fn strip_marker_regions(src: &str, begin: &str, end: &str, keep: bool) -> String {
4096    let mut out = Vec::new();
4097    let mut in_region = false;
4098    for line in src.lines() {
4099        let trimmed = line.trim();
4100        if trimmed == begin {
4101            assert!(!in_region, "nested {begin} marker in wpe_soc_template.v");
4102            in_region = true;
4103            continue; // never emit the marker line itself
4104        }
4105        if trimmed == end {
4106            assert!(in_region, "unmatched {end} marker in wpe_soc_template.v");
4107            in_region = false;
4108            continue;
4109        }
4110        if in_region && !keep {
4111            continue; // drop the gated body when the feature is off
4112        }
4113        out.push(line);
4114    }
4115    assert!(
4116        !in_region,
4117        "unterminated {begin} region in wpe_soc_template.v"
4118    );
4119    out.join("\n")
4120}