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}