Skip to main content

logicaffeine_compile/spatial/lower/fabric/
engine.rs

1//! A fabric engine: one micro-program's sequencer, driving one of each datapath.
2//!
3//! An engine runs its micro-ops one after another, so it needs one datapath of each kind, not one per op. The
4//! sequencer walks the micro-program as the twin does — `pc`, the phase `ph`, the loop counters — and each fixed op's
5//! state sets, as constants, the parameters of the unit it runs on:
6//!
7//! - the tile walk, which steps a tile read's or write's element offset;
8//! - the accumulator walk, which steps an element of the accumulator block through its row and column;
9//! - the Generic unit: its axes, one offset per operand slot, the body's shape class, the contraction and the fold;
10//! - the matmul unit: its load, weight, stream and output phases;
11//! - the unary unit: its input and operand reads and its one multiplier.
12//!
13//! Those parameters are the op table, a register loaded on the clock that enters a state, so no decode of the state
14//! stands in front of a datapath. Each state's successors are constants and the conditions choosing among them read
15//! registers, so the table loads the row of the successor taken from a decode of the registered state, with those
16//! conditions selecting among a few rows; the next state the sequencer computes from the same conditions is decoded by
17//! nothing. Each unit holds the counters its addresses advance by, so no address divides, and keeps the cycle the op's
18//! own datapath kept, so the twin's count is still the RTL's.
19//!
20//! A Generic body is a pipeline: each node registered once its operands are, a product twice, a floor division through
21//! a divider a stage per quotient bit. A point enters it in its body cycle with a control word — whether it writes,
22//! where, how it folds — that travels beside its value, so the contraction, the fold and the write happen where the
23//! value leaves, as deep as its class. An op takes its points' periods and that depth (`Fixed::Generic::depth`).
24//!
25//! The accumulator is banked by column: bank `c` holds column `c` of every row of the block, so a matmul writes a
26//! whole output row in one cycle, one bank each. Where the site's memories are banked, the walks move runs: a tile walk
27//! a contiguous row's run a cycle, an acquire, a pack and a matmul's reload a run of a row's column banks, a matmul's A
28//! and B loads a run of an operand row — each lane landing in the bank or register of its residue, so no lane is
29//! selected by more than the run's start. Every other access is a single element at its row and column.
30
31use std::collections::{BTreeMap, BTreeSet};
32
33use super::{
34    addr_v, bits_for, buffer_uses, fold_step, from_phase, gather_lanes, index_read, item_coord_names, lit64, log2_banks, loop_vars,
35    mcast_sends, mcast_valids, ph_less, resize_lanes, sem_incs, sems_local, tensor_width, tensors_read, tensors_written,
36    var_name, Ctx, Emit, EnginePlan, Fixed, GenUnit, Guard, Micro, UnaryKind,
37};
38use crate::codegen_sva::systolic::pipelined_latency;
39use crate::spatial::ir::*;
40use crate::spatial::lower::SpatialLowerError;
41use crate::spatial::passes::{LV_ITEM, LV_SUB_H, LV_SUB_W};
42use crate::spatial::target::precision_int_bits;
43use crate::spatial::value_range::{signed_bits, Interval};
44use crate::tensor_recognize::ElementwiseOp;
45
46/// `log2(n)` of a power of two.
47fn log2(n: usize) -> u32 {
48    n.trailing_zeros()
49}
50
51/// `v`, `width` bits wide, as a signed operand of a product kept at `keep` bits: its low `keep` bits where it is wider,
52/// since no bit of an operand above the product's width reaches the product's bits.
53fn fit(v: &str, width: u32, keep: u32) -> String {
54    if width > keep {
55        format!("$signed({v}[{}:0])", keep - 1)
56    } else {
57        format!("$signed({v})")
58    }
59}
60
61/// The accumulator block every op of an acquired region works in, as tiles per row: a matmul's output block, else one
62/// row of the region's tiles. Element `e = (i·cols + j)·te + r·tc + cc` of the block, tile-major, is the accumulator's
63/// row `i·tr + r` and column `j·tc + cc`.
64pub(super) fn acc_regions(micro: &[Micro]) -> Vec<Option<usize>> {
65    let mut out = vec![None; micro.len()];
66    let mut i = 0;
67    while i < micro.len() {
68        if let Micro::Fixed(Fixed::AcquireAcc { tiles, .. }) = micro[i] {
69            let end = (i..micro.len())
70                .find(|&k| matches!(micro[k], Micro::Fixed(Fixed::ReleaseAcc) | Micro::Then(Fixed::ReleaseAcc)))
71                .unwrap_or(micro.len());
72            let cols = micro[i..end]
73                .iter()
74                .find_map(|m| match m {
75                    Micro::Fixed(Fixed::Matmul { block, .. }) => Some(block.osw),
76                    _ => None,
77                })
78                .unwrap_or(tiles);
79            for slot in out.iter_mut().take(end).skip(i) {
80                *slot = Some(cols);
81            }
82            i = end;
83        }
84        i += 1;
85    }
86    out
87}
88
89/// A Generic body with its constants lifted out: ops of one engine whose bodies differ only in constants share one
90/// datapath, their constants its parameters.
91struct BodyClass {
92    key: String,
93    /// The first op of the class: its body and its axes, which every op of the class shares.
94    unit: GenUnit,
95}
96
97/// The shape key of `b` (structure, operand slots, axis roles, comparison kinds) and its constants in walk order.
98fn body_shape(b: &TensorBody, role: &dyn Fn(LoopVar) -> &'static str, params: &mut Vec<i64>) -> Result<String, SpatialLowerError> {
99    let two = |tag: &str, x: &TensorBody, y: &TensorBody, params: &mut Vec<i64>| -> Result<String, SpatialLowerError> {
100        Ok(format!("{tag}({},{})", body_shape(x, role, params)?, body_shape(y, role, params)?))
101    };
102    Ok(match b {
103        TensorBody::Operand(i) => format!("o{i}"),
104        TensorBody::Const(bits) => {
105            params.push(f64::from_bits(*bits) as i64);
106            "k".into()
107        }
108        TensorBody::Add(x, y) => two("add", x, y, params)?,
109        TensorBody::Sub(x, y) => two("sub", x, y, params)?,
110        TensorBody::Mul(x, y) => two("mul", x, y, params)?,
111        TensorBody::Min(x, y) => two("min", x, y, params)?,
112        TensorBody::Max(x, y) => two("max", x, y, params)?,
113        TensorBody::FloorDiv(x, y) => two("fdiv", x, y, params)?,
114        TensorBody::Shr { arg, by } => {
115            let a = body_shape(arg, role, params)?;
116            params.push(*by as i64);
117            format!("shr({a},k)")
118        }
119        TensorBody::Select { cond, then, otherwise } => {
120            let rhs = match cond.rhs() {
121                PredRhs::Const(k) => {
122                    params.push(k as i64);
123                    "k".to_string()
124                }
125                PredRhs::Operand(i) => format!("o{i}"),
126            };
127            let cmp = match cond {
128                Pred::AxisLt(..) => "lt",
129                Pred::AxisGe(..) => "ge",
130            };
131            let t = body_shape(then, role, params)?;
132            let o = body_shape(otherwise, role, params)?;
133            format!("sel({cmp},{},{rhs},{t},{o})", role(cond.var()))
134        }
135        TensorBody::Unary { .. } => {
136            return Err(SpatialLowerError::Unsupported("a transcendental in an integer body"))
137        }
138    })
139}
140
141/// Cycles from a pipelined floor divider's operands to its quotient: a stage for the magnitudes and the quotient's
142/// sign, a stage per quotient bit — the `wq` bits the proof bounds the quotient to, never more than the dividend's `wa` —
143/// and a stage for the floor.
144pub fn floor_divider_latency(wa: u32, wq: u32) -> u64 {
145    wa.min(wq) as u64 + 2
146}
147
148/// A pipelined signed floor divider `q = ⌊a / b⌋` — `a` of `wa` bits, `b` of `wb`, `q` of `wq` — taking a pair a cycle
149/// and returning each quotient [`floor_divider_latency`] cycles after its operands.
150///
151/// It divides the magnitudes by restoring division, a quotient bit a stage, then floors: where the signs differ the
152/// quotient is negated, and one less again where a remainder is left. A quotient proven to fit `wq` bits has no bit above
153/// them, so the dividend's bits above them start the remainder and the stages walk only the `wq` below. A divisor of zero
154/// is the evaluator's error; its quotient is whatever the stages make of it.
155pub fn floor_divider_verilog(name: &str, wa: u32, wb: u32, wq: u32) -> String {
156    let n = wa.min(wq);
157    let mut e = Emit { s: String::new() };
158    e.l(format!(
159        "module {name}(input clk, input signed [{}:0] a, input signed [{}:0] b, output reg signed [{}:0] q);",
160        wa - 1,
161        wb - 1,
162        wq - 1
163    ));
164    e.l(format!("  wire [{}:0] am = a[{}] ? -a : a;", wa - 1, wa - 1));
165    e.l(format!("  wire [{}:0] bm = b[{}] ? -b : b;", wb - 1, wb - 1));
166    e.l(format!("  reg [{}:0] r0, d0; reg [{}:0] x0; reg s0;", wb - 1, n - 1));
167    let top = if wa > n { format!("am[{}:{n}]", wa - 1) } else { "0".to_string() };
168    e.l(format!(
169        "  always @(posedge clk) begin r0 <= {top}; x0 <= am[{}:0]; d0 <= bm; s0 <= a[{}] ^ b[{}]; end",
170        n - 1,
171        wa - 1,
172        wb - 1
173    ));
174    for k in 1..=n {
175        let p = k - 1;
176        e.l(format!("  wire [{wb}:0] t{k} = {{r{p}, x{p}[{}]}};", n - 1));
177        e.l(format!("  wire g{k} = t{k} >= {{1'b0, d{p}}};"));
178        e.l(format!("  reg [{}:0] r{k}, d{k}; reg [{}:0] x{k}; reg s{k};", wb - 1, n - 1));
179        let shift = if n == 1 { format!("g{k}") } else { format!("{{x{p}[{}:0], g{k}}}", n - 2) };
180        e.l(format!(
181            "  always @(posedge clk) begin r{k} <= g{k} ? t{k} - {{1'b0, d{p}}} : t{k}; x{k} <= {shift}; d{k} <= d{p}; s{k} <= s{p}; end"
182        ));
183    }
184    e.l(format!(
185        "  always @(posedge clk) q <= s{n} ? ((r{n} != 0) ? -$signed({{1'b0, x{n}}}) - 1 : -$signed({{1'b0, x{n}}})) : $signed({{1'b0, x{n}}});"
186    ));
187    e.l("endmodule");
188    e.s
189}
190
191/// A body node's value in the pipeline: its wire, its width, and the stage it is ready at — counted from the body's
192/// cycle, `None` for a parameter, which every op holds fixed.
193struct Staged {
194    wire: String,
195    width: u32,
196    ready: Option<u32>,
197}
198
199/// A class body printed as a pipeline, one wire `gn{class}_{j}{suffix}` per node `j` of [`TensorBody::post_order`] at its
200/// width in `widths`. An operand leaf reads its slot's operand (`operand`) in the body's cycle (stage 0) and a constant its
201/// parameter, in walk order; every other node is computed from its operands brought to one stage — a value ready
202/// earlier through a register a stage — and registered: once, twice for a product, through a pipelined divider for a
203/// floor division. A select's predicate is compared in the body's cycle, against the point's own axis. A lane of a
204/// vector unit is a pipeline of its own, its wires suffixed with the lane, its parameters the class's.
205///
206/// ★ A SUM A PRODUCT WIDER THAN ONE MULTIPLIER SLICE READS STAYS OUT OF THE SLICE'S PRE-ADDER (`(* keep *)` on its
207/// register). yosys 0.33 folds a registered sum into the pre-adder of a product's DSP slice; where the product is wider
208/// than one slice ([`SiteSpec::multiplier`](crate::spatial::target::SiteSpec::multiplier)) and it splits it over a
209/// cascade, it folds the sum into the top slice alone and wires the bottom slice's A operand to `x`: the bitstream then
210/// multiplies a constant there. MEASURED on chip 0 of the split step: `(o_a_c0 + o_a_c1) · 144532`, two 19-bit operands
211/// past the slice's 18-bit B port, computed `hi` wrong on the board while its RTL was the evaluator's. A product one
212/// slice holds keeps its pre-adder: the sum costs no adder of its own.
213struct Pipe<'a> {
214    class: usize,
215    axis: &'a dyn Fn(LoopVar) -> String,
216    operand: &'a dyn Fn(usize) -> String,
217    suffix: &'a str,
218    widths: &'a [u32],
219    /// The prefix of each divider module's name, unique to the engine.
220    prefix: &'a str,
221    /// The widest product one multiplier slice takes, `(wide, narrow)` signed operand bits.
222    slice: Option<(u32, u32)>,
223    next_param: usize,
224    node: usize,
225    lines: Vec<String>,
226    modules: Vec<String>,
227}
228
229impl Pipe<'_> {
230    fn param(&mut self) -> String {
231        let n = self.next_param;
232        self.next_param += 1;
233        format!("gp{}_{n}", self.class)
234    }
235
236    fn take(&mut self) -> (usize, String, u32) {
237        let j = self.node;
238        self.node += 1;
239        (j, format!("gn{}_{j}{}", self.class, self.suffix), self.widths[j])
240    }
241
242    /// The register of node wire `wire`, kept: synthesis leaves it a register of its own.
243    fn keep(&mut self, wire: &str) {
244        let decl = format!(" {wire}; always @(posedge clk) {wire} <= ");
245        let line = self.lines.iter_mut().find(|l| l.starts_with("  reg signed [") && l.contains(&decl)).expect("a node's register");
246        line.insert_str(2, "(* keep *) ");
247    }
248
249    /// `v` brought to stage `to` through a register a stage; a parameter as it is.
250    fn at(&mut self, v: &Staged, to: u32) -> String {
251        let Some(from) = v.ready else { return v.wire.clone() };
252        let mut cur = v.wire.clone();
253        for s in from + 1..=to {
254            let d = format!("{}_d{s}", v.wire);
255            self.lines.push(format!("  reg signed [{}:0] {d}; always @(posedge clk) {d} <= {cur};", v.width - 1));
256            cur = d;
257        }
258        cur
259    }
260
261    fn walk(&mut self, b: &TensorBody) -> Staged {
262        match b {
263            TensorBody::Operand(i) => {
264                let (_, wire, width) = self.take();
265                self.lines.push(format!("  wire signed [{}:0] {wire} = $signed({});", width - 1, (self.operand)(*i)));
266                Staged { wire, width, ready: Some(0) }
267            }
268            TensorBody::Const(_) => {
269                let p = self.param();
270                let (_, wire, width) = self.take();
271                self.lines.push(format!("  wire signed [{}:0] {wire} = $signed({p});", width - 1));
272                Staged { wire, width, ready: None }
273            }
274            TensorBody::Add(xb, yb) | TensorBody::Sub(xb, yb) | TensorBody::Mul(xb, yb) | TensorBody::Min(xb, yb) | TensorBody::Max(xb, yb) => {
275                let (x, y) = (self.walk(xb), self.walk(yb));
276                let (j, wire, width) = self.take();
277                let s = x.ready.max(y.ready).unwrap_or(0);
278                let (a, c) = (self.at(&x, s), self.at(&y, s));
279                let w = width - 1;
280                let (expr, ready) = match b {
281                    TensorBody::Add(..) => (format!("$signed({a}) + $signed({c})"), s + 1),
282                    TensorBody::Sub(..) => (format!("$signed({a}) - $signed({c})"), s + 1),
283                    TensorBody::Mul(..) => {
284                        let (wa, wb) = (x.width.min(width), y.width.min(width));
285                        if self.slice.is_some_and(|(wide, narrow)| wa.max(wb) > wide || wa.min(wb) > narrow) {
286                            for (child, operand) in [(xb, &x), (yb, &y)] {
287                                if matches!(**child, TensorBody::Add(..) | TensorBody::Sub(..)) {
288                                    self.keep(&operand.wire);
289                                }
290                            }
291                        }
292                        // The product is taken at its node's width: the bits of an operand past it reach none of the
293                        // node's.
294                        let m = format!("gm{}_{j}{}", self.class, self.suffix);
295                        let (a, c) = (fit(&a, x.width, width), fit(&c, y.width, width));
296                        self.lines.push(format!("  reg signed [{w}:0] {m}; always @(posedge clk) {m} <= {a} * {c};"));
297                        (m, s + 2)
298                    }
299                    TensorBody::Min(..) => (format!("($signed({a}) < $signed({c})) ? $signed({a}) : $signed({c})"), s + 1),
300                    _ => (format!("($signed({a}) > $signed({c})) ? $signed({a}) : $signed({c})"), s + 1),
301                };
302                self.lines.push(format!("  reg signed [{w}:0] {wire}; always @(posedge clk) {wire} <= {expr};"));
303                Staged { wire, width, ready: Some(ready) }
304            }
305            TensorBody::FloorDiv(x, y) => {
306                let (x, y) = (self.walk(x), self.walk(y));
307                let (j, wire, width) = self.take();
308                let s = x.ready.max(y.ready).unwrap_or(0);
309                let (a, c) = (self.at(&x, s), self.at(&y, s));
310                let module = format!("{}_fd{}_{j}", self.prefix, self.class);
311                self.modules.push(floor_divider_verilog(&module, x.width, y.width, width));
312                self.lines.push(format!("  wire signed [{}:0] {wire};", width - 1));
313                self.lines.push(format!("  {module} u_{module}(.clk(clk), .a({a}), .b({c}), .q({wire}));"));
314                Staged { wire, width, ready: Some(s + floor_divider_latency(x.width, width) as u32) }
315            }
316            TensorBody::Shr { arg, .. } => {
317                let x = self.walk(arg);
318                let p = self.param();
319                let (_, wire, width) = self.take();
320                let s = x.ready.unwrap_or(0);
321                let a = self.at(&x, s);
322                self.lines.push(format!("  reg signed [{}:0] {wire}; always @(posedge clk) {wire} <= $signed({a}) >>> {p};", width - 1));
323                Staged { wire, width, ready: Some(s + 1) }
324            }
325            TensorBody::Select { cond, then, otherwise } => {
326                let lhs = format!("$signed({{32'd0, {}}})", (self.axis)(cond.var()));
327                let rhs = match cond.rhs() {
328                    PredRhs::Const(_) => format!("$signed({})", self.param()),
329                    PredRhs::Operand(i) => format!("$signed({})", (self.operand)(i)),
330                };
331                let op = match cond {
332                    Pred::AxisLt(..) => "<",
333                    Pred::AxisGe(..) => ">=",
334                };
335                let (t, o) = (self.walk(then), self.walk(otherwise));
336                let (j, wire, width) = self.take();
337                let pred = Staged { wire: format!("gs{}_{j}{}", self.class, self.suffix), width: 1, ready: Some(0) };
338                self.lines.push(format!("  wire {} = {lhs} {op} {rhs};", pred.wire));
339                let s = t.ready.max(o.ready).unwrap_or(0);
340                let (p, a, c) = (self.at(&pred, s), self.at(&t, s), self.at(&o, s));
341                self.lines.push(format!(
342                    "  reg signed [{}:0] {wire}; always @(posedge clk) {wire} <= {p} ? $signed({a}) : $signed({c});",
343                    width - 1
344                ));
345                Staged { wire, width, ready: Some(s + 1) }
346            }
347            TensorBody::Unary { .. } => unreachable!("refused by body_shape"),
348        }
349    }
350}
351
352/// The operand register of slot `s` in a one-lane unit.
353fn scalar_operand(s: usize) -> String {
354    format!("g_op{s}")
355}
356
357/// A class body's pipeline: its lines, its divider modules, its root, and its depth — the cycles from the body's cycle
358/// to the root's value.
359#[allow(clippy::too_many_arguments)]
360fn class_pipeline(
361    b: &TensorBody,
362    class: usize,
363    axis: &dyn Fn(LoopVar) -> String,
364    operand: &dyn Fn(usize) -> String,
365    suffix: &str,
366    widths: &[u32],
367    prefix: &str,
368    slice: Option<(u32, u32)>,
369) -> (Vec<String>, Vec<String>, String, u64) {
370    let mut pipe = Pipe { class, axis, operand, suffix, widths, prefix, slice, next_param: 0, node: 0, lines: Vec::new(), modules: Vec::new() };
371    let root = pipe.walk(b);
372    (pipe.lines, pipe.modules, root.wire, root.ready.unwrap_or(0) as u64)
373}
374
375/// An engine's Generic bodies by class: ops whose bodies differ only in constants share one class.
376struct Classes {
377    classes: Vec<BodyClass>,
378    /// Each Generic op's class and its parameter values, by micro-op index.
379    op_class: BTreeMap<usize, (usize, Vec<i64>)>,
380}
381
382impl Classes {
383    fn of(micro: &[Micro]) -> Result<Classes, SpatialLowerError> {
384        let mut out = Classes { classes: Vec::new(), op_class: BTreeMap::new() };
385        for (i, m) in micro.iter().enumerate() {
386            let Micro::Fixed(Fixed::Generic { unit, .. }) = m else { continue };
387            let role = axis_role(unit);
388            let mut params = Vec::new();
389            let key = body_shape(&unit.body, &role, &mut params)?;
390            let class = match out.classes.iter().position(|c| c.key == key) {
391                Some(c) => c,
392                None => {
393                    out.classes.push(BodyClass { key, unit: (**unit).clone() });
394                    out.classes.len() - 1
395                }
396            };
397            out.op_class.insert(i, (class, params));
398        }
399        Ok(out)
400    }
401
402    fn params_of(&self, class: usize) -> usize {
403        self.op_class.values().filter(|(c, _)| *c == class).map(|(_, p)| p.len()).max().unwrap_or(0)
404    }
405}
406
407/// The cycles a Generic point spends past its body: the inner fold, then the outer fold and the write, each a stage of
408/// its own, so the contraction, the two folds and the write never share a cycle.
409pub(super) const FOLD_STAGES: u64 = 2;
410
411/// Each engine class's Generic ops given their depth: their body's pipeline, their lane tree's levels and the folds. The
412/// class of every plan of an engine class is the representative's, its widths the join over the class's plans — what
413/// [`print_engine`] prints.
414pub(super) fn set_generic_depths(ctx: &Ctx, plans: &mut [EnginePlan]) -> Result<(), SpatialLowerError> {
415    let engine_classes: BTreeSet<usize> = plans.iter().map(|p| p.class).collect();
416    for ec in engine_classes {
417        let rep = plans.iter().position(|p| p.class == ec).expect("a plan of the class");
418        let cls = Classes::of(&plans[rep].micro)?;
419        let depths: Vec<u64> = {
420            let class_plans: Vec<&EnginePlan> = plans.iter().filter(|p| p.class == ec).collect();
421            let wd = Widths::of(ctx, &class_plans, &cls);
422            cls.classes
423                .iter()
424                .enumerate()
425                .map(|(c, bc)| class_pipeline(&bc.unit.body, c, &|_| String::new(), &scalar_operand, "", &wd.nodes[c], "", ctx.multiplier).3)
426                .collect()
427        };
428        for plan in plans.iter_mut().filter(|p| p.class == ec) {
429            for (i, m) in plan.micro.iter_mut().enumerate() {
430                if let Micro::Fixed(Fixed::Generic { unit, depth, .. }) = m {
431                    *depth = depths[cls.op_class[&i].0] + unit.tree_levels() + FOLD_STAGES;
432                }
433            }
434        }
435    }
436    Ok(())
437}
438
439/// What every unit of an engine is sized to: the accumulator's banks and rows, the Generic registers, axes and
440/// classes, the matmul's array and operand rows.
441struct Shape {
442    banks: usize,
443    rows: usize,
444    slots: usize,
445    /// The bits of a Generic axis counter: its extents, its initiation interval.
446    axis_bits: usize,
447    /// The deepest of the Generic ops' schedules: each slot's issue after its point's start, each slot's hold from its
448    /// landing to its point's body cycle, and the body cycle itself.
449    issue_taps: Vec<usize>,
450    hold_taps: Vec<usize>,
451    body_tap: usize,
452    cls: Classes,
453    array: Option<(usize, usize, u32)>,
454    a_rows: usize,
455    gathers: BTreeSet<usize>,
456    /// The slots a vector op gathers through, each of whose lanes reads through a port of its own ([`gather_lanes`]).
457    lane_gathers: BTreeSet<usize>,
458    /// The widest run a Generic op takes a point: the unit's lanes, one where no op takes more.
459    lanes: usize,
460    /// Each body class's lanes: the widest run any of its ops takes.
461    class_lanes: Vec<usize>,
462    /// The levels of the lane tree: the most any op's lanes climb.
463    tree_levels: usize,
464    /// What the engine's ops whose lanes each keep their own column use past lane 0: the pools their folds fold by,
465    /// whether any contracts, and whether any writes a parallel point (kind 0) or a fold's result (kind 2).
466    lane_pools: [bool; 3],
467    lane_contracts: bool,
468    lane_writes_points: bool,
469    lane_writes_folds: bool,
470    /// Whether a tile walk of the engine fills a page whose rows keep a pitch ([`Ctx::page_pitch`]).
471    pitched_fill: bool,
472}
473
474fn axis_role(g: &GenUnit) -> impl Fn(LoopVar) -> &'static str + '_ {
475    move |v: LoopVar| {
476        if v == g.row.var {
477            "r"
478        } else if v == g.col.var {
479            "c"
480        } else if g.k.is_some_and(|k| k.var == v) {
481            "x"
482        } else {
483            "0"
484        }
485    }
486}
487
488/// An axis as lane `l` of a vector unit's body cycle sees it: its own coordinate along the lane axis, the point's along
489/// any other.
490fn lane_axis_wire(role: &'static str, l: usize) -> String {
491    match role {
492        "r" | "c" | "x" => format!("gb_{role}_l{l}"),
493        _ => "32'd0".into(),
494    }
495}
496
497/// An axis as the body cycle of a point sees it: the point's own coordinate, carried from its start.
498fn axis_wire(role: &'static str) -> String {
499    match role {
500        "r" => "gb_r".into(),
501        "c" => "gb_c".into(),
502        "x" => "gb_x".into(),
503        _ => "32'd0".into(),
504    }
505}
506
507impl Shape {
508    fn of(ctx: &Ctx, micro: &[Micro], regions: &[Option<usize>]) -> Result<Shape, SpatialLowerError> {
509        let (te, tr, tc) = (ctx.te, ctx.tr, ctx.tc);
510        let mut sh = Shape {
511            banks: 1,
512            rows: 1,
513            slots: 0,
514            axis_bits: 1,
515            issue_taps: Vec::new(),
516            hold_taps: Vec::new(),
517            body_tap: 0,
518            cls: Classes::of(micro)?,
519            array: None,
520            a_rows: 1,
521            gathers: BTreeSet::new(),
522            lane_gathers: BTreeSet::new(),
523            lanes: 1,
524            class_lanes: Vec::new(),
525            tree_levels: 0,
526            lane_pools: [false; 3],
527            lane_contracts: false,
528            lane_writes_points: false,
529            lane_writes_folds: false,
530            pitched_fill: micro.iter().any(|m| {
531                matches!(m, Micro::Fixed(Fixed::ReadTiles { into, .. }) if ctx.page_pitch.contains_key(&ctx.template(*into)))
532            }),
533        };
534        sh.class_lanes = vec![1; sh.cls.classes.len()];
535        let need = |cols_t: Option<usize>, elems: usize, sh: &mut Shape| -> Result<(), SpatialLowerError> {
536            let cols_t = cols_t.ok_or(SpatialLowerError::Unsupported("an accumulator op outside an acquired region"))?;
537            let width = cols_t.max(1) * tc;
538            sh.banks = sh.banks.max(width);
539            sh.rows = sh.rows.max(elems.div_ceil(cols_t.max(1) * te) * tr);
540            Ok(())
541        };
542        for (i, m) in micro.iter().enumerate() {
543            let Micro::Fixed(f) = m else { continue };
544            match f {
545                Fixed::AcquireAcc { tiles, .. } | Fixed::Unary { tiles, .. } => need(regions[i], tiles * te, &mut sh)?,
546                Fixed::PackAcc { tiles, .. } => need(regions[i], tiles * te, &mut sh)?,
547                Fixed::Matmul { block, operand, array, reload, .. } => {
548                    need(regions[i], block.osh * block.osw * te, &mut sh)?;
549                    sh.banks = sh.banks.max(array.1);
550                    let _ = reload;
551                    let bits = precision_int_bits(*operand).unwrap_or(64);
552                    let widest = sh.array.map_or(bits, |a| a.2.max(bits));
553                    sh.array = Some((array.0, array.1, widest));
554                    sh.a_rows = sh.a_rows.max(block.osh * tr);
555                }
556                Fixed::Generic { unit, tiles, .. } => {
557                    if tr != 1 || tc != 1 {
558                        return Err(SpatialLowerError::Unsupported("a Generic on a tile of more than one element"));
559                    }
560                    need(regions[i], tiles * te, &mut sh)?;
561                    sh.slots = sh.slots.max(unit.slots());
562                    for s in 0..unit.slots() {
563                        if unit.gathers(s) {
564                            sh.gathers.insert(s);
565                            if unit.lanes > 1 {
566                                sh.lane_gathers.insert(s);
567                            }
568                        }
569                    }
570                    sh.lanes = sh.lanes.max(unit.lanes);
571                    let class = sh.cls.op_class[&i].0;
572                    sh.class_lanes[class] = sh.class_lanes[class].max(unit.lanes);
573                    sh.tree_levels = sh.tree_levels.max(unit.tree_levels() as usize);
574                    if unit.lanes > 1 && !unit.tree() {
575                        sh.lane_contracts |= unit.k.is_some();
576                        match (unit.row.kind, unit.col.kind) {
577                            (IterKind::Reduce { pool }, IterKind::Parallel) => {
578                                sh.lane_pools[pool_index(pool)] = true;
579                                sh.lane_writes_folds = true;
580                            }
581                            _ => sh.lane_writes_points = true,
582                        }
583                    }
584                    let is = &unit.issue;
585                    let most = [unit.row.extent, unit.col.extent, unit.k_extent(), is.ii].into_iter().max().unwrap_or(1);
586                    sh.axis_bits = sh.axis_bits.max(bits_for(most + 1));
587                    sh.body_tap = sh.body_tap.max(is.body);
588                    sh.issue_taps.resize(sh.slots, 0);
589                    sh.hold_taps.resize(sh.slots, 0);
590                    for (s, at) in is.at.iter().enumerate() {
591                        sh.issue_taps[s] = sh.issue_taps[s].max(*at);
592                        sh.hold_taps[s] = sh.hold_taps[s].max(is.body - at - ctx.lat as usize - 1);
593                    }
594                }
595                _ => {}
596            }
597        }
598        Ok(sh)
599    }
600}
601
602/// A Generic slot's offset walk: its base plus its walk along the axes — a delta as each one advances, `d0` along the
603/// contraction, `d1` to the next middle point, `d2` to the next outer one — plus a gather's value times its stride,
604/// read through the page as one row of its width, whose `lim` elements are the page.
605struct SlotWalk {
606    d0: i64,
607    d1: i64,
608    d2: i64,
609    lim: i64,
610}
611
612impl SlotWalk {
613    /// The walk in points: a step along the lane axis moves a run of the lanes.
614    fn of(ctx: &Ctx, unit: &GenUnit, ins: &[BufferId], sl: usize) -> SlotWalk {
615        let m = &unit.maps[sl];
616        let (vr, vc, vk) = (unit.row.var, unit.col.var, unit.k.map(|k| k.var));
617        let lv = unit.lane_var();
618        let stride = |v: Option<LoopVar>| -> i64 {
619            v.map_or(0, |v| m.terms.iter().filter(|(t, _)| *t == v).map(|(_, s)| *s).sum::<i64>() * if v == lv { unit.lanes as i64 } else { 1 })
620        };
621        let (sx, sr, scol) = (stride(vk), stride(Some(vr)), stride(Some(vc)));
622        let (smid, sout) = if unit.rows_inner() { (sr, scol) } else { (scol, sr) };
623        let nmid = if unit.rows_inner() { unit.points_along(vr) } else { unit.points_along(vc) };
624        let kx1 = vk.map_or(1, |v| unit.points_along(v)) as i64 - 1;
625        let width = (ctx.page_elems(ins[sl]) / ctx.te) * ctx.tr;
626        SlotWalk {
627            d0: sx,
628            d1: smid - sx * kx1,
629            d2: sout - smid * (nmid as i64 - 1) - sx * kx1,
630            lim: (ctx.tr * width) as i64,
631        }
632    }
633
634    /// Every offset the slot's walk takes: its map over the iteration space, a gather's term over every index `index`
635    /// holds — and the walk itself, from zero.
636    fn offsets(unit: &GenUnit, sl: usize, index: Option<Interval>) -> (Interval, Interval) {
637        let m = &unit.maps[sl];
638        let extent = |v: LoopVar| {
639            if v == unit.row.var {
640                unit.row.extent
641            } else if v == unit.col.var {
642                unit.col.extent
643            } else if unit.k.is_some_and(|k| k.var == v) {
644                unit.k_extent()
645            } else {
646                1
647            }
648        };
649        // The lanes of a last point past the lane axis's end address past it too.
650        let lv = unit.lane_var();
651        let (mut lo, mut hi) = (0i128, 0i128);
652        for (v, s) in &m.terms {
653            let reach = if *v == lv { unit.points_along(*v) * unit.lanes } else { extent(*v) };
654            let span = *s as i128 * (reach as i128 - 1);
655            lo += span.min(0);
656            hi += span.max(0);
657        }
658        let lin = (lo, hi);
659        let (mut lo, mut hi) = (lo + m.base as i128, hi + m.base as i128);
660        if let (Some(ind), true, Some(ix)) = (m.indirect, unit.gathers(sl), index) {
661            let (a, b) = (ind.stride as i128 * ix.0 as i128, ind.stride as i128 * ix.1 as i128);
662            lo += a.min(b);
663            hi += a.max(b);
664        }
665        let fit = |(lo, hi): (i128, i128)| -> Interval {
666            if lo < i64::MIN as i128 || hi > i64::MAX as i128 {
667                (i64::MIN, i64::MAX)
668            } else {
669                (lo as i64, hi as i64)
670            }
671        };
672        (fit(lin), fit((lo, hi)))
673    }
674}
675
676/// A reduction pool's place among the three, as the fold units and the op table number them.
677fn pool_index(p: ReducePool) -> usize {
678    match p {
679        ReducePool::Sum => 0,
680        ReducePool::Max => 1,
681        ReducePool::ArgMax => 2,
682    }
683}
684
685/// What each register and wire of an engine class holds, as a signed width: the join, over every site the class runs
686/// on, of the proven interval ([`crate::spatial::value_range::program_ranges`]) of every value it carries.
687struct Widths {
688    /// The accumulator banks, and every value written into them or read out of them.
689    acc: u32,
690    /// The Generic unit's operand registers: what each slot reads, or zero outside its page.
691    slot: u32,
692    /// The Generic unit's value: a body's result, a contraction's partial sums, a fold's registers, the write.
693    value: u32,
694    /// Each body class's nodes, in [`TensorBody::post_order`].
695    nodes: Vec<Vec<u32>>,
696    /// Each body class's parameters, by the constants the class's ops give them.
697    params: Vec<Vec<u32>>,
698    /// The unary unit: its input, its second operand (a bias, an elementwise operand), its product or sum, its result,
699    /// and its multiplier.
700    un_in: u32,
701    un_b: u32,
702    un_mul: u32,
703    un_out: u32,
704    un_m: u32,
705    /// The Generic unit's offset walks: each slot's base, deltas, page bound, gather stride, walk and offset.
706    addr: u32,
707}
708
709impl Widths {
710    fn of(ctx: &Ctx, plans: &[&EnginePlan], cls: &Classes) -> Widths {
711        let r = &ctx.ranges;
712        let mut w = Widths {
713            acc: 1,
714            slot: 1,
715            value: 1,
716            nodes: cls.classes.iter().map(|c| vec![1; c.unit.body.post_order().len()]).collect(),
717            params: (0..cls.classes.len()).map(|c| vec![1; cls.params_of(c)]).collect(),
718            un_in: 1,
719            un_b: 1,
720            un_mul: 1,
721            un_out: 1,
722            un_m: 1,
723            addr: 1,
724        };
725        let grow = |slot: &mut u32, iv: Option<Interval>| {
726            if let Some(iv) = iv {
727                *slot = (*slot).max(signed_bits(iv));
728            }
729        };
730        let buf = |b: BufferId| r.buffers[b.idx()];
731        // What a compute stage keeps — the whole of i64 where nothing proves less.
732        let held = |s: StageId| Some(r.stages[s.idx()].held.unwrap_or((i64::MIN, i64::MAX)));
733        let with_zero = |iv: Option<Interval>| iv.map(|(lo, hi)| (lo.min(0), hi.max(0)));
734        for plan in plans {
735            for (i, m) in plan.micro.iter().enumerate() {
736                let Micro::Fixed(f) = m else { continue };
737                match f {
738                    Fixed::Matmul { stage, reload, .. } => {
739                        grow(&mut w.acc, held(*stage));
740                        grow(&mut w.acc, reload.and_then(buf));
741                    }
742                    Fixed::Unary { kind, ins, stage, .. } => {
743                        let st = &r.stages[stage.idx()];
744                        grow(&mut w.un_in, buf(ins[0]));
745                        if let Some(b) = ins.get(1) {
746                            grow(&mut w.un_b, buf(*b));
747                        }
748                        match kind {
749                            UnaryKind::Requant { m, .. } => {
750                                grow(&mut w.un_m, Some((*m, *m)));
751                                grow(&mut w.un_mul, st.nodes.first().copied());
752                            }
753                            UnaryKind::Bias => grow(&mut w.un_mul, st.nodes.first().copied()),
754                            UnaryKind::Elementwise(_) => grow(&mut w.un_mul, st.nodes.last().copied()),
755                            UnaryKind::Relu => {}
756                            UnaryKind::Scalar(_) => {
757                                for x in [&mut w.un_in, &mut w.un_b, &mut w.un_mul, &mut w.un_out] {
758                                    *x = 64;
759                                }
760                            }
761                        }
762                        grow(&mut w.un_out, held(*stage));
763                        grow(&mut w.acc, held(*stage));
764                    }
765                    Fixed::Generic { unit, ins, stage, .. } => {
766                        let st = &r.stages[stage.idx()];
767                        for b in ins {
768                            grow(&mut w.slot, with_zero(buf(*b)));
769                        }
770                        for (sl, m) in unit.maps.iter().enumerate() {
771                            let walk = SlotWalk::of(ctx, unit, ins, sl);
772                            let index = m.indirect.and_then(|ind| with_zero(buf(ins[ind.operand])));
773                            let (lin, off) = SlotWalk::offsets(unit, sl, index);
774                            for iv in [lin, off, (m.base, m.base), (walk.d0, walk.d0), (walk.d1, walk.d1), (walk.d2, walk.d2), (walk.lim, walk.lim)] {
775                                grow(&mut w.addr, Some(iv));
776                            }
777                            if let Some(ind) = m.indirect {
778                                grow(&mut w.addr, Some((ind.stride, ind.stride)));
779                            }
780                        }
781                        let (class, _) = &cls.op_class[&i];
782                        for (j, iv) in st.nodes.iter().enumerate() {
783                            grow(&mut w.nodes[*class][j], Some(*iv));
784                        }
785                        grow(&mut w.value, held(*stage));
786                        grow(&mut w.value, st.nodes.last().copied());
787                        // A lane tree that sums holds the sum of a run of up to its lanes' values: no prefix of the fold,
788                        // so none the fold's proof bounds.
789                        if unit.tree() && unit.tree_sums() {
790                            grow(&mut w.value, st.nodes.last().map(|iv| run_sum(*iv, unit.lanes)));
791                        }
792                        grow(&mut w.acc, held(*stage));
793                    }
794                    _ => {}
795                }
796            }
797        }
798        for (class, values) in cls.op_class.values() {
799            for (j, v) in values.iter().enumerate() {
800                grow(&mut w.params[*class][j], Some((*v, *v)));
801            }
802        }
803        w
804    }
805}
806
807/// Every sum of one to `n` values of `iv`, and zero: `[min(lo, n·lo, 0), max(hi, n·hi, 0)]`, the whole of i64 past it.
808fn run_sum((lo, hi): Interval, n: usize) -> Interval {
809    let fit = |v: i128| v.clamp(i64::MIN as i128, i64::MAX as i128) as i64;
810    let (lo, hi, n) = (lo as i128, hi as i128, n as i128);
811    (fit(lo.min(n * lo).min(0)), fit(hi.max(n * hi).max(0)))
812}
813
814/// A table parameter's width that is the engine's address width, `AW`: the walks' offsets, bounds, strides and phases.
815const AW_WIDE: usize = 0;
816
817/// The engine class's Verilog module, printed from the representative plan.
818pub(super) fn print_engine(
819    ctx: &Ctx,
820    plan: &EnginePlan,
821    class_plans: &[&EnginePlan],
822    name: &str,
823    grid_names: &BTreeMap<usize, String>,
824) -> Result<String, SpatialLowerError> {
825    let p = ctx.p;
826    let (te, tr, tc, lat) = (ctx.te, ctx.tr, ctx.tc, ctx.lat);
827    if !te.is_power_of_two() || !tr.is_power_of_two() || !tc.is_power_of_two() {
828        return Err(SpatialLowerError::Unsupported("a fabric tile whose sides are not powers of two"));
829    }
830    ctx.phase_flags.borrow_mut().clear();
831    let micro = &plan.micro;
832    let uses = buffer_uses(micro);
833    let bufs: BTreeSet<BufferId> = uses.all();
834    let tp = |b: BufferId| ctx.template(b).idx();
835    let gathered = gather_lanes(ctx, micro);
836    let t_rd = tensors_read(micro);
837    let t_wr = tensors_written(micro);
838    let s_local = sems_local(micro);
839    let s_inc = sem_incs(micro);
840    let m_valid = mcast_valids(micro);
841    let m_send = mcast_sends(micro);
842    let lvs = loop_vars(micro);
843    let regions = acc_regions(micro);
844    let sh = Shape::of(ctx, micro, &regions)?;
845    let wd = Widths::of(ctx, class_plans, &sh.cls);
846    let mut e = Emit { s: String::new() };
847    // Each Generic class's pipeline — its lines, its dividers, its root and its depth — the dividers printed ahead of the
848    // engine. Every op of a class was given the class's depth with its plan. A vector unit (`vl` lanes) prints a class's
849    // pipeline once per lane its widest op takes, each against its lane's operands and coordinates.
850    let vl = sh.lanes;
851    let vec = vl > 1;
852    let lane_pipes: Vec<Vec<(Vec<String>, Vec<String>, String, u64)>> = sh
853        .cls
854        .classes
855        .iter()
856        .enumerate()
857        .map(|(c, cl)| {
858            let role = axis_role(&cl.unit);
859            if vec {
860                (0..sh.class_lanes[c])
861                    .map(|l| {
862                        let axis = |v: LoopVar| lane_axis_wire(role(v), l);
863                        let operand = |s: usize| format!("g_op{s}_l{l}");
864                        class_pipeline(&cl.unit.body, c, &axis, &operand, &format!("_l{l}"), &wd.nodes[c], &format!("floor_div_{name}_l{l}"), ctx.multiplier)
865                    })
866                    .collect()
867            } else {
868                let axis = |v: LoopVar| axis_wire(role(v));
869                vec![class_pipeline(&cl.unit.body, c, &axis, &scalar_operand, "", &wd.nodes[c], &format!("floor_div_{name}"), ctx.multiplier)]
870            }
871        })
872        .collect();
873    let pipes: Vec<&(Vec<String>, Vec<String>, String, u64)> = lane_pipes.iter().map(|lanes| &lanes[0]).collect();
874    for (i, (c, _)) in &sh.cls.op_class {
875        if let Micro::Fixed(Fixed::Generic { unit, depth, .. }) = &micro[*i] {
876            debug_assert_eq!(*depth, pipes[*c].3 + unit.tree_levels() + FOLD_STAGES, "op {i} is as deep as its class's pipeline, its lane tree and the folds");
877        }
878    }
879    for lanes in &lane_pipes {
880        for (_, modules, _, _) in lanes {
881            for m in modules {
882                e.s.push_str(m);
883            }
884        }
885    }
886
887    // ── Ports ───────────────────────────────────────────────────────────────────────────────────────────────
888    let mut ports: Vec<String> = vec!["input clk".into(), "input rst".into(), "input start".into(), "output reg done".into()];
889    for b in &bufs {
890        let pe = ctx.page_words(*b);
891        let (sw, ew, cw) = (bits_for(ctx.depth(*b)), bits_for(pe), bits_for(ctx.depth(*b) + 1));
892        let k = tp(*b);
893        if uses.produces.contains(b) {
894            ports.push(format!("output reg f{k}_reserve"));
895            ports.push(format!("output reg [{cw}:0] f{k}_reserve_n"));
896            ports.push(format!("output reg f{k}_push"));
897            ports.push(format!("output reg [{cw}:0] f{k}_push_n"));
898            ports.push(format!("input [{cw}:0] f{k}_free"));
899        }
900        if uses.consumes.contains(b) {
901            ports.push(format!("output reg f{k}_pop"));
902            ports.push(format!("output reg [{cw}:0] f{k}_pop_n"));
903            ports.push(format!("input [{cw}:0] f{k}_count"));
904        }
905        let hi = (ctx.banks as u32 - 1) * ctx.buffer_width(*b);
906        if uses.writes.contains(b) {
907            ports.push(format!("output reg f{k}_wr_en"));
908            ports.push(format!("output reg [{sw}:0] f{k}_wr_slot"));
909            ports.push(format!("output reg [{ew}:0] f{k}_wr_elem"));
910            ports.push(format!("output reg [63:0] f{k}_wr_data"));
911            if ctx.banks > 1 {
912                ports.push(format!("output reg [{}:0] f{k}_wr_n", log2_banks(ctx.banks)));
913                ports.push(format!("output reg [3:0] f{k}_wr_lw"));
914                ports.push(format!("output reg [{}:0] f{k}_wr_hi", hi - 1));
915            }
916            ports.push(format!("input [{sw}:0] f{k}_fill_slot0"));
917        }
918        if uses.reads.contains(b) {
919            ports.push(format!("output reg [{sw}:0] f{k}_rd_slot"));
920            ports.push(format!("output reg [{ew}:0] f{k}_rd_elem"));
921            ports.push(format!("input [63:0] f{k}_rd_data"));
922            for l in 1..gathered.get(&ctx.template(*b)).copied().unwrap_or(1) {
923                ports.push(format!("output reg [{sw}:0] f{k}_l{l}_rd_slot"));
924                ports.push(format!("output reg [{ew}:0] f{k}_l{l}_rd_elem"));
925                ports.push(format!("input [63:0] f{k}_l{l}_rd_data"));
926            }
927            if ctx.banks > 1 {
928                ports.push(format!("input [{}:0] f{k}_rd_hi", hi - 1));
929            }
930            ports.push(format!("input [{sw}:0] f{k}_rd_slot0"));
931            ports.push(format!("input [{sw}:0] f{k}_fill_slot0_r"));
932        }
933    }
934    let t_hi = |t: TensorId| (ctx.banks as u32 - 1) * tensor_width(p.tensor(t), &ctx.ranges);
935    for t in &t_rd {
936        ports.push(format!("output reg [31:0] t{}_rd_addr", t.idx()));
937        ports.push(format!("input [63:0] t{}_rd_data", t.idx()));
938        if ctx.banks > 1 {
939            ports.push(format!("input [{}:0] t{}_rd_hi", t_hi(*t) - 1, t.idx()));
940        }
941    }
942    for t in &t_wr {
943        ports.push(format!("output reg t{}_wr_en", t.idx()));
944        ports.push(format!("output reg [31:0] t{}_wr_addr", t.idx()));
945        ports.push(format!("output reg [63:0] t{}_wr_data", t.idx()));
946        if ctx.banks > 1 {
947            ports.push(format!("output reg [{}:0] t{}_wr_n", log2_banks(ctx.banks), t.idx()));
948            ports.push(format!("output reg [3:0] t{}_wr_lw", t.idx()));
949            ports.push(format!("output reg [{}:0] t{}_wr_hi", t_hi(*t) - 1, t.idx()));
950        }
951    }
952    for s in &s_local {
953        ports.push(format!("input [15:0] s{}_count", s.idx()));
954        ports.push(format!("output reg s{}_set", s.idx()));
955        ports.push(format!("output reg [15:0] s{}_set_val", s.idx()));
956    }
957    for s in &s_inc {
958        ports.push(format!("output reg s{}_inc_remote", s.idx()));
959        ports.push(format!("output reg [15:0] s{}_inc_n", s.idx()));
960    }
961    for (s, g) in &m_valid {
962        ports.push(format!("output reg mv{}_g{}", s.idx(), g.idx()));
963    }
964    for g in &m_send {
965        ports.push(format!("output reg mc{}_send", g.idx()));
966        ports.push(format!("output reg [79:0] mc{}_din", g.idx()));
967    }
968    // A chip program's root engine halts for the host: the continues the host has made, and where it waits.
969    let links: Vec<(usize, usize, usize)> = micro
970        .iter()
971        .enumerate()
972        .filter_map(|(i, m)| match m {
973            Micro::Guarded(Guard::Link { point, ordinal }) => Some((i, *point, *ordinal)),
974            _ => None,
975        })
976        .collect();
977    if !links.is_empty() {
978        ports.push("input [7:0] link_n".into());
979        ports.push("output halted".into());
980        ports.push("output [15:0] halt_at".into());
981    }
982    e.l(format!("module {name} #(parameter ITEM_START = 0, parameter ITEM_COUNT = 1) ("));
983    e.l(format!("  {}", ports.join(",\n  ")));
984    e.l(");");
985    // The walks' width, one bit past the largest value any walk takes (computed once the op table's rows exist).
986    e.l("  localparam AW = __AW__;");
987
988    // ── Sequencer state ─────────────────────────────────────────────────────────────────────────────────────
989    let pcw = bits_for(micro.len() + 1);
990    e.l(format!("  reg [{pcw}:0] pc, pc_n;"));
991    // The phase counts no further than the longest op lasts, and at least the sixteen bits a multicast's element takes.
992    let longest = micro.iter().filter_map(|m| if let Micro::Fixed(f) = m { Some(ctx.duration(f)) } else { None }).max().unwrap_or(1);
993    let phw = bits_for(longest as usize + 2).max(16);
994    e.l(format!("  reg [{}:0] ph, ph_n;", phw - 1));
995    // The op's last cycle, a register: its duration less one loaded as the op is entered and counted down, so nothing
996    // decodes the state or compares the phase in front of the many units it resets.
997    e.l(format!("  reg op_last; reg [{}:0] op_left;", phw - 1));
998    e.l("  reg running, running_n, done_n;");
999    if !links.is_empty() {
1000        // Halted while it waits at a halt the host has not continued it past; the halt's link, while it does.
1001        let waits: Vec<String> = links.iter().map(|(i, _, o)| format!("(pc == {i} && !(link_n > 8'd{o}))")).collect();
1002        e.l(format!("  assign halted = running && ({});", waits.join(" || ")));
1003        let at: String = links.iter().map(|(i, point, _)| format!("(pc == {i}) ? 16'd{point} : ")).collect();
1004        e.l(format!("  assign halt_at = {at}16'd0;"));
1005    }
1006    for v in &lvs {
1007        e.l(format!("  reg [31:0] {0}, {0}_n;", var_name(*v)));
1008    }
1009    let or_zero = |v: LoopVar| if lvs.contains(&v) { var_name(v) } else { "32'd0".into() };
1010    e.l(format!("  wire [31:0] lv5_or_zero = {};", or_zero(LV_ITEM)));
1011    e.l("  wire [31:0] item_id = ITEM_START + lv5_or_zero;");
1012    e.l(format!("  wire [31:0] oy = item_id / {};", ctx.grid_nx.max(1)));
1013    e.l(format!("  wire [31:0] ox = item_id % {};", ctx.grid_nx.max(1)));
1014    let sections_addressed: BTreeSet<usize> = micro
1015        .iter()
1016        .filter_map(|m| match m {
1017            Micro::Fixed(Fixed::ReadTiles { section, .. } | Fixed::WriteTiles { section, .. }) => *section,
1018            _ => None,
1019        })
1020        .collect();
1021    if !sections_addressed.is_empty() {
1022        let items = p.site_items();
1023        for k in &sections_addressed {
1024            let coords: Vec<(usize, usize)> =
1025                items.at(Some(*k), plan.site).iter().map(|it| p.work.items[it.idx()].coord).collect();
1026            for (axis, pick) in [("y", 0usize), ("x", 1usize)] {
1027                e.l(format!("  function [31:0] item_{axis}_s{k}(input [31:0] lv);"));
1028                let mut c = String::from("    case (lv)");
1029                for (n, xy) in coords.iter().enumerate() {
1030                    c.push_str(&format!(" {n}: item_{axis}_s{k} = {};", if pick == 0 { xy.0 } else { xy.1 }));
1031                }
1032                c.push_str(&format!(" default: item_{axis}_s{k} = 0; endcase"));
1033                e.l(c);
1034                e.l("  endfunction");
1035            }
1036        }
1037    }
1038
1039    // ── Parameters ───────────────────────────────────────────────────────────────────────────────────────────
1040    // What moves with the phase or carries a port's data: driven by the decode of the current state.
1041    let g = sh.slots;
1042    let ax = sh.axis_bits;
1043    // The deepest tap of the control word: a class's pipeline, and the lane tree an op of it climbs.
1044    let dmax = sh
1045        .cls
1046        .op_class
1047        .iter()
1048        .filter_map(|(i, (c, _))| match &micro[*i] {
1049            Micro::Fixed(Fixed::Generic { unit, .. }) => Some(pipes[*c].3 + unit.tree_levels()),
1050            _ => None,
1051        })
1052        .chain(pipes.iter().map(|p| p.3))
1053        .max()
1054        .unwrap_or(0);
1055    let mut live: Vec<(String, usize, bool)> = vec![
1056        ("aw_step".into(), 1, false),
1057        ("sw_we".into(), 1, false),
1058        ("az_we".into(), 1, false),
1059        ("sw_d".into(), wd.acc as usize, true),
1060        ("unary_in".into(), wd.un_in as usize, false),
1061        ("unary_bias".into(), wd.un_b as usize, false),
1062        ("mu_ra".into(), sh.array.map_or(1, |a| a.2) as usize, false),
1063        ("mu_rb".into(), sh.array.map_or(1, |a| a.2) as usize, false),
1064        ("mu_rr".into(), wd.acc as usize, false),
1065    ];
1066    for s in 0..sh.slots {
1067        if vec {
1068            for l in 0..vl {
1069                live.push((format!("gu_ld{s}_l{l}"), wd.slot as usize, false));
1070            }
1071        } else {
1072            live.push((format!("gu_ld{s}"), wd.slot as usize, false));
1073        }
1074        live.push((format!("gu_pp{s}"), 1, false));
1075    }
1076    if let (Some((_, _, bits)), true) = (sh.array, ctx.runs()) {
1077        for j in 0..ctx.banks {
1078            live.push((format!("mu_rl{j}"), wd.acc as usize, true));
1079            live.push((format!("mu_al{j}"), bits as usize, true));
1080            live.push((format!("mu_bl{j}"), bits as usize, true));
1081        }
1082    }
1083    // The op table: every unit parameter an op holds fixed, loaded on the clock that enters the op's state from the
1084    // state it enters, so no decode of the state stands in front of the datapath it configures.
1085    let mut table: Vec<(String, usize, bool)> = vec![
1086        // a fixed op's state
1087        ("fx".into(), 1, false),
1088        // tile walk
1089        ("tw_on".into(), 1, false),
1090        ("tw_from".into(), AW_WIDE, false),
1091        ("tw_bw".into(), AW_WIDE, false),
1092        ("tw_d1".into(), AW_WIDE, true),
1093        ("tw_d2".into(), AW_WIDE, true),
1094        ("tw_vec".into(), 1, false),
1095        ("tw_rs".into(), AW_WIDE, false),
1096        ("tw_lw".into(), 4, false),
1097        ("tw_n0".into(), log2_banks(ctx.banks) as usize + 1, false),
1098    ];
1099    if sh.pitched_fill {
1100        // the padding past the end of a page row the walk fills
1101        table.push(("tw_pp".into(), AW_WIDE, false));
1102    }
1103    table.extend([
1104        // accumulator walk, and its run walk
1105        ("aw_cols".into(), AW_WIDE, false),
1106        ("ar_on".into(), 1, false),
1107        ("ar_e".into(), AW_WIDE, false),
1108        ("ar_dly".into(), 1, false),
1109        ("ar_lw".into(), 4, false),
1110        ("ar_n0".into(), log2_banks(ctx.banks) as usize + 1, false),
1111        ("pk_tr".into(), 1, false),
1112        // unary
1113        ("un_on".into(), 1, false),
1114        ("un_pe".into(), AW_WIDE, false),
1115        ("un_pb".into(), AW_WIDE, false),
1116        ("un_kind".into(), 4, false),
1117        ("un_m".into(), wd.un_m as usize, true),
1118        ("un_s".into(), 8, false),
1119        // multicast send
1120        ("mc_on".into(), 1, false),
1121        ("mc_pe".into(), AW_WIDE, false),
1122        // matmul
1123        ("mu_on".into(), 1, false),
1124        ("mu_p0".into(), AW_WIDE, false),
1125        ("mu_p1".into(), AW_WIDE, false),
1126        ("mu_p2".into(), AW_WIDE, false),
1127        ("mu_p3".into(), AW_WIDE, false),
1128        ("mu_rows".into(), AW_WIDE, false),
1129        ("mu_kop".into(), AW_WIDE, false),
1130        ("mu_nop".into(), AW_WIDE, false),
1131        ("mu_osw".into(), AW_WIDE, false),
1132        ("mu_ibw".into(), AW_WIDE, false),
1133        ("mu_bwrap".into(), AW_WIDE, false),
1134        ("mu_bjump".into(), AW_WIDE, false),
1135        ("mu_pn".into(), AW_WIDE, false),
1136        ("mr_on".into(), 1, false),
1137        ("mu_mv".into(), 1, false),
1138        ("mr_lw".into(), 4, false),
1139        ("mr_n0o".into(), log2_banks(ctx.banks) as usize + 1, false),
1140        ("mr_n0i".into(), log2_banks(ctx.banks) as usize + 1, false),
1141        // Generic
1142        ("gu_on".into(), 1, false),
1143        ("gu_ii".into(), ax, false),
1144        ("gu_kx".into(), ax, false),
1145        ("gu_nmid".into(), ax, false),
1146        ("gu_nout".into(), ax, false),
1147        ("gu_rinner".into(), 1, false),
1148        ("gu_cls".into(), 8, false),
1149        ("gu_kind".into(), 2, false),
1150        ("gu_pool".into(), 2, false),
1151        ("gu_rpool".into(), 2, false),
1152        ("gu_rinner_cx".into(), ax, false),
1153        ("gu_rx".into(), ax, false),
1154        ("gu_dep".into(), bits_for(dmax as usize + 1), false),
1155        ("gu_iss".into(), AW_WIDE, false),
1156        ("gu_body".into(), bits_for(sh.body_tap + 1), false),
1157    ]);
1158    if vec {
1159        // The op's lanes as a shift and as a count, the axis they run along (0 the row, 1 the column, 2 the contraction),
1160        // its last point along that axis and the lanes that point holds;
1161        // the level of the lane tree its value leaves at and the pool it folds by; the tap of the control word its lanes'
1162        // validity is read at, its class's depth; and the shift of the columns its lanes write, zero where one lane
1163        // writes.
1164        for (n, w) in [
1165            ("gu_lsh", 4),
1166            ("gu_lax", 2),
1167            ("gu_lanes", bits_for(vl + 1)),
1168            ("gu_llast", ax),
1169            ("gu_ltail", bits_for(vl + 1)),
1170            ("gu_tl", bits_for(sh.tree_levels + 1)),
1171            ("gu_tpool", 2),
1172            ("gu_cdep", bits_for(dmax as usize + 1)),
1173            ("gu_wsh", 4),
1174        ] {
1175            table.push((n.into(), w, false));
1176        }
1177    }
1178    for s in 0..g {
1179        let aw = wd.addr as usize;
1180        let (at, hold) = (bits_for(sh.issue_taps[s] + 1), bits_for(sh.hold_taps[s] + 1));
1181        for (n, w, sg) in [("base", aw, true), ("d0", aw, true), ("d1", aw, true), ("d2", aw, true), ("lim", aw, true), ("gon", 1, false), ("gst", aw, true), ("gop", 8, false), ("at", at, false), ("dl", hold, false)] {
1182            table.push((format!("gu_{n}{s}"), w, sg));
1183        }
1184        if vec {
1185            // Whether the slot reads one element a point, every lane's.
1186            table.push((format!("gu_bc{s}"), 1, false));
1187        }
1188    }
1189    for c in 0..sh.cls.classes.len() {
1190        for j in 0..sh.cls.params_of(c) {
1191            table.push((format!("gp{c}_{j}"), wd.params[c][j] as usize, true));
1192        }
1193    }
1194    for (n, w, sg) in live.iter().chain(&table) {
1195        let s = if *sg { "signed " } else { "" };
1196        let range = if *w == AW_WIDE { "[AW-1:0]".to_string() } else { format!("[{}:0]", w - 1) };
1197        e.l(format!("  reg {s}{range} {n};"));
1198    }
1199
1200    // ── Units: registers ─────────────────────────────────────────────────────────────────────────────────────
1201    e.l("  // tile walk: element offset of a tile read or write");
1202    e.l("  reg [AW-1:0] tw_lo, tw_w, tw_off;");
1203    e.l("  // accumulator walk: the element's row and column in the accumulator block");
1204    e.l("  reg [AW-1:0] aw_cc, aw_r, aw_j, aw_i;");
1205    e.l(format!("  wire [AW-1:0] aw_row = (aw_i << {}) + aw_r;", log2(tr)));
1206    e.l(format!("  wire [AW-1:0] aw_col = (aw_j << {}) + aw_cc;", log2(tc)));
1207    e.l("  // unary and multicast walks");
1208    e.l("  reg [AW-1:0] un_e, un_e2, un_bk, un_bt, mc_e, mc_pg;");
1209    e.l(format!("  reg [{}:0] unary_out;", wd.un_out - 1));
1210    e.l("  wire mu_reload;");
1211
1212    // The accumulator banks.
1213    let (banks, rows) = (sh.banks, sh.rows);
1214    e.l(format!("  // accumulator: {banks} banks of {rows} rows"));
1215    // A bank is a register file, written by the unit an op runs on and read whole by the matmul: never a LUT memory.
1216    for c in 0..banks {
1217        e.l(format!("  (* ram_style = \"registers\" *) reg signed [{}:0] acc_b{c} [0:{}];", wd.acc - 1, rows - 1));
1218    }
1219    // The single read port, at the accumulator walk's element.
1220    e.l(format!("  reg signed [{}:0] sr_d;", wd.acc - 1));
1221    {
1222        let mut r = String::from("  always @* begin sr_d = 0; case (aw_col)");
1223        for c in 0..banks {
1224            r.push_str(&format!(" {c}: sr_d = acc_b{c}[aw_row];"));
1225        }
1226        r.push_str(" default: ; endcase end");
1227        e.l(r);
1228    }
1229    // The run walk: a run of a row's column banks a cycle — its row, its first column, its place in the page and its
1230    // length — and the same `lat` cycles on, where an accumulating pack writes what it read.
1231    if ctx.runs() {
1232        let (l, lg) = (ctx.banks, log2_banks(ctx.banks));
1233        // A run's length and the banks it zeroes are registers, set a cycle ahead from where the walk goes next; at the
1234        // walk's start they are the op's own (`ar_n0`), so nothing between the walk and the banks is arithmetic.
1235        let banks = sh.banks;
1236        e.l("  reg [AW-1:0] ar_i, ar_j, ar_p, ar_r, ar_rr_r, ar_rp_r; reg ar_first;");
1237        e.l(format!("  reg [{lg}:0] ar_n_r; reg [{}:0] ar_z_r;", banks - 1));
1238        e.l("  wire [AW-1:0] ar_l = 32'd1 << ar_lw;");
1239        e.l(format!("  wire [{lg}:0] ar_n = ar_first ? ar_n0 : ar_n_r;"));
1240        let z0: Vec<String> = (0..banks).rev().map(|c| format!("(32'd{c} < ar_n0)")).collect();
1241        e.l(format!("  wire [{}:0] ar_z = ar_first ? {{{}}} : ar_z_r;", banks - 1, z0.join(", ")));
1242        // What is left of the row (`aw_cols - ar_j`) and of the page (`ar_e - ar_p`) are registers, the op's own at its
1243        // first cycle, so the walk advances on a compare of the run's width with a remainder, never on a sum. A run
1244        // starts at a multiple of its width, so its index in the row (`ar_r`) is its column over the width.
1245        e.l("  wire [AW-1:0] ar_rr = ar_first ? aw_cols : ar_rr_r, ar_rp = ar_first ? ar_e : ar_rp_r;");
1246        e.l("  wire ar_adv = (ar_l < ar_rr);");
1247        e.l("  wire [AW-1:0] ar_jn = ar_adv ? ar_j + ar_l : 32'd0, ar_pn = ar_p + ar_n;");
1248        e.l("  wire [AW-1:0] ar_rln = ar_adv ? ar_rr - ar_l : aw_cols, ar_ln = ar_rp - ar_n, ar_rn = ar_adv ? ar_r + 1 : 32'd0;");
1249        e.l("  wire [AW-1:0] ar_mn = (ar_rln < ar_ln) ? ar_rln : ar_ln;");
1250        e.l(format!("  wire [{lg}:0] ar_nn = (ar_mn < ar_l) ? ar_mn[{lg}:0] : ar_l[{lg}:0];"));
1251        // Bank `c` is in the next run when its word is the run's and its lane is below the run's length; a lane is
1252        // always below the run's width, so it is below the length exactly when it is below both next remainders. Each
1253        // side of the advance is decided from registers, and the advance only chooses between them.
1254        let zn: Vec<String> = (0..banks)
1255            .rev()
1256            .map(|c| {
1257                let (word, lane) = (format!("(32'd{c} >> ar_lw)"), format!("(32'd{c} & (ar_l - 1))"));
1258                format!(
1259                    "(ar_adv ? ({word} == ar_r + 1 && {lane} < ar_rr - ar_l && {lane} < ar_ln) : ({word} == 0 && {lane} < aw_cols && {lane} < ar_ln))"
1260                )
1261            })
1262            .collect();
1263        e.l(format!("  wire [{}:0] ar_zn = {{{}}};", banks - 1, zn.join(", ")));
1264        e.l("  always @(posedge clk)");
1265        e.l("    if (rst || !(running && ar_on) || op_last) begin ar_i <= 0; ar_j <= 0; ar_p <= 0; ar_r <= 0; ar_first <= 1'b1; end");
1266        e.l("    else begin ar_first <= 1'b0; ar_n_r <= ar_nn; ar_z_r <= ar_zn; ar_p <= ar_pn; ar_j <= ar_jn; ar_r <= ar_rn; ar_rr_r <= ar_rln; ar_rp_r <= ar_ln; if (!ar_adv) ar_i <= ar_i + 1; end");
1267        // Under simulation, in every cycle the walk runs: no run passes its page, and the banks a run zeroes are its own,
1268        // its word's lanes below its length. Past the page nothing reads what a run writes or zeroes, so no output
1269        // shows either broken; these checks do.
1270        let own: Vec<String> = (0..banks)
1271            .rev()
1272            .map(|c| format!("((32'd{c} >> ar_lw) == (ar_j >> ar_lw) && ((32'd{c} & (ar_l - 1)) < ar_n))"))
1273            .collect();
1274        e.l("  // synthesis translate_off");
1275        e.l("  always @(posedge clk) if (!rst && running && ar_on && ar_p + ar_n > ar_e) begin $display(\"FATAL: a run of %0d at %0d passes its page of %0d\", ar_n, ar_p, ar_e); $finish; end");
1276        e.l(format!(
1277            "  always @(posedge clk) if (!rst && running && ar_on && ar_z != {{{}}}) begin $display(\"FATAL: the banks a run zeroes are not its own: %b at column %0d, length %0d\", ar_z, ar_j, ar_n); $finish; end",
1278            own.join(", ")
1279        ));
1280        e.l("  // synthesis translate_on");
1281        if lat == 0 {
1282            e.l(format!("  wire [AW-1:0] ar_wi = ar_i, ar_wj = ar_j, ar_wp = ar_p; wire [{lg}:0] ar_wn = ar_n;"));
1283        } else {
1284            for d in 1..=lat {
1285                let src = |x: &str| if d == 1 { x.to_string() } else { format!("{x}{}", d - 1) };
1286                e.l(format!(
1287                    "  reg [AW-1:0] ar_i{d}, ar_j{d}, ar_p{d}; reg [{lg}:0] ar_n{d}; always @(posedge clk) begin ar_i{d} <= {}; ar_j{d} <= {}; ar_p{d} <= {}; ar_n{d} <= {}; end",
1288                    src("ar_i"),
1289                    src("ar_j"),
1290                    src("ar_p"),
1291                    src("ar_n")
1292                ));
1293            }
1294            e.l(format!(
1295                "  wire [AW-1:0] ar_wi = ar_dly ? ar_i{lat} : ar_i, ar_wj = ar_dly ? ar_j{lat} : ar_j, ar_wp = ar_dly ? ar_p{lat} : ar_p; wire [{lg}:0] ar_wn = ar_dly ? ar_n{lat} : ar_n;"
1296            ));
1297        }
1298        // Lane `j` of the run: column `ar_wj + j`, as an I32 plan keeps it. A run that reaches lane `j` is wider than `j`,
1299        // so it starts at a multiple of `j`'s next power of two: the lane chooses among those columns only.
1300        let acc = wd.acc;
1301        for j in 0..l {
1302            let step = (j + 1).next_power_of_two();
1303            let mut r = format!("  reg signed [{}:0] pk_a{j}; always @* case (ar_wj >> {})", acc - 1, step.trailing_zeros());
1304            for m in 0..banks.div_ceil(step) {
1305                if m * step + j < banks {
1306                    r.push_str(&format!(" {m}: pk_a{j} = acc_b{}[ar_wi];", m * step + j));
1307                }
1308            }
1309            r.push_str(&format!(" default: pk_a{j} = 0; endcase"));
1310            e.l(r);
1311            let t = if acc > 32 {
1312                format!("pk_tr ? {{{{{}{{pk_a{j}[31]}}}}, pk_a{j}[31:0]}} : pk_a{j}", acc - 32)
1313            } else {
1314                format!("pk_a{j}")
1315            };
1316            e.l(format!("  wire signed [{}:0] pk_t{j} = {t};", acc - 1));
1317        }
1318        // Each page a pack writes: the run's lanes at the page's width, an accumulating pack's added to what it read.
1319        let packed: BTreeSet<BufferId> = micro
1320            .iter()
1321            .filter_map(|m| match m {
1322                Micro::Fixed(Fixed::PackAcc { to, .. }) => Some(ctx.template(*to)),
1323                _ => None,
1324            })
1325            .collect();
1326        let read: BTreeSet<BufferId> = uses.reads.iter().map(|b| ctx.template(*b)).collect();
1327        for tpl in &packed {
1328            let (k, w) = (tpl.idx(), ctx.buffer_width(*tpl));
1329            for j in 0..l {
1330                let page = if !read.contains(tpl) {
1331                    "0".to_string()
1332                } else if j == 0 {
1333                    format!("(ar_dly ? $signed(f{k}_rd_data) : 0)")
1334                } else {
1335                    format!("(ar_dly ? $signed(f{k}_rd_hi[{}:{}]) : 0)", j as u32 * w - 1, (j as u32 - 1) * w)
1336                };
1337                e.l(format!("  wire signed [{}:0] pk{k}_v{j} = $signed(pk_t{j}) + {page};", w - 1));
1338            }
1339        }
1340    }
1341
1342    // The PE array of a matmul engine.
1343    let pe = sh.array;
1344    if let Some((k, n, bits)) = pe {
1345        e.l("  reg pe_w_load, pe_w_load_r;");
1346        e.l(format!("  reg [{}:0] pe_w_row;", bits_for(k) - 1));
1347        e.l(format!("  reg [{}:0] pe_w_data;", n as u32 * bits - 1));
1348        for r in 0..k {
1349            e.l(format!("  reg signed [63:0] pe_a{r};"));
1350        }
1351        for c in 0..n {
1352            e.l(format!("  wire signed [63:0] pe_o{c};"));
1353        }
1354        let mut conn = vec![
1355            ".clk(clk)".to_string(),
1356            ".rst(rst)".to_string(),
1357            ".w_load(pe_w_load)".to_string(),
1358            ".w_row(pe_w_row)".to_string(),
1359            ".w_data(pe_w_data)".to_string(),
1360        ];
1361        conn.extend((0..k).map(|r| format!(".a{r}(pe_a{r})")));
1362        conn.extend((0..n).map(|c| format!(".o{c}(pe_o{c})")));
1363        e.l(format!("  pe_{name} u_pe({});", conn.join(", ")));
1364        e.l("  always @* pe_w_load = pe_w_load_r;");
1365        // Operand rows: the A rows the array streams and the B rows it loads as weights, each element at its width.
1366        let ar = sh.a_rows;
1367        for s in 0..ar {
1368            for x in 0..k {
1369                e.l(format!("  reg signed [{}:0] a_r{s}_{x};", bits - 1));
1370            }
1371        }
1372        for r in 0..k {
1373            e.l(format!("  reg [{}:0] b_r{r};", n as u32 * bits - 1));
1374        }
1375        e.l("  reg [AW-1:0] ma_xx, ma_r, ma_kk, ma_i, mb_cc, mb_x, mb_j, mb_kk, mbi_t, mbi_off;");
1376    }
1377
1378    // The Generic unit: the walk that starts a point every `gu_ii` cycles; each slot's read on its schedule, its value
1379    // latched as it lands and held to its point's body cycle; each class's pipeline; and what the fold and the write
1380    // need of a point, carried down the pipeline beside its value.
1381    if g > 0 {
1382        let axb = ax - 1;
1383        let aw = wd.addr;
1384        // A value carried `taps` cycles in a register a cycle, read at the tap an op's parameter names.
1385        let delay = |e: &mut Emit, name: &str, width: usize, taps: usize, sel: &str, first: &str| {
1386            for d in 1..=taps {
1387                let prev = if d == 1 { first.to_string() } else { format!("{name}_{}", d - 1) };
1388                e.l(format!("  reg [{}:0] {name}_{d}; always @(posedge clk) {name}_{d} <= {prev};", width - 1));
1389            }
1390            let mut t = format!("  reg [{}:0] {name}; always @* case ({sel}) 0: {name} = {first};", width - 1);
1391            for d in 1..=taps {
1392                t.push_str(&format!(" {d}: {name} = {name}_{d};"));
1393            }
1394            t.push_str(&format!(" default: {name} = {first}; endcase"));
1395            e.l(t);
1396        };
1397        e.l(format!("  reg signed [{}:0] g_kacc, g_fold, g_ofold;", wd.value - 1));
1398        e.l(format!("  reg [{axb}:0] g_fidx, g_oidx;"));
1399        e.l(format!("  reg [{axb}:0] gu_sub, gu_x, gu_mid, gu_out;"));
1400        e.l(format!("  wire [{axb}:0] gu_r = gu_rinner ? gu_mid : gu_out;"));
1401        e.l(format!("  wire [{axb}:0] gu_c = gu_rinner ? gu_out : gu_mid;"));
1402        // A point starts: within the op's issue phases, at the head of each interval.
1403        e.l("  wire gs_v = gu_on && (ph < gu_iss) && (gu_sub == 0);");
1404        // The point's coordinates, carried to its body cycle — and in a vector unit, above them, which of its lanes are
1405        // the op's: every lane below the op's lanes, and of the last point along the lane axis those below its tail. The
1406        // point's own counters decide it at its start, so its body cycle compares nothing.
1407        let lane_start = if vec {
1408            e.l(format!("  wire [{axb}:0] gu_li = (gu_lax == 2'd0) ? gu_r : (gu_lax == 2'd1) ? gu_c : gu_x;"));
1409            e.l("  wire gu_lat_last = (gu_li == gu_llast);");
1410            let bits: Vec<String> = (0..vl).rev().map(|l| format!("(gu_lat_last ? ({l} < gu_ltail) : ({l} < gu_lanes))")).collect();
1411            // The run's first coordinate along the lane axis, a multiple of the op's lanes.
1412            e.l(format!("  wire [{}:0] gs_lbase = {{4'd0, gu_li}} << gu_lsh;", ax + 3));
1413            format!("gs_lbase, {}, ", bits.join(", "))
1414        } else {
1415            String::new()
1416        };
1417        let gbw = 3 * ax + 1 + if vec { vl + ax + 4 } else { 0 };
1418        e.l(format!("  wire [{}:0] gb0 = {{{lane_start}gu_c, gu_r, gu_x, gs_v}};", gbw - 1));
1419        delay(&mut e, "gbo", gbw, sh.body_tap, "gu_body", "gb0");
1420        e.l("  wire gb_v = gu_on && gbo[0] && (ph >= gu_body);");
1421        e.l(format!("  wire [{axb}:0] gb_x = gbo[{}:1], gb_r = gbo[{}:{}], gb_c = gbo[{}:{}];", ax, 2 * ax, ax + 1, 3 * ax, 2 * ax + 1));
1422        // A vector unit's lanes: each lane's coordinates — its point's, and along the lane axis the run's first
1423        // coordinate with the lane in its low bits, which a run's start, a multiple of the op's lanes, leaves clear for
1424        // every lane of the op — and whether it is one of the op's, as its point carries it.
1425        let cwd = ax + 4;
1426        if vec {
1427            e.l(format!("  wire [{}:0] gb_lbase = gbo[{}:{}];", cwd - 1, 3 * ax + vl + cwd, 3 * ax + 1 + vl));
1428            for l in 0..vl {
1429                for (role, code, src) in [("r", 0, "gb_r"), ("c", 1, "gb_c"), ("x", 2, "gb_x")] {
1430                    e.l(format!("  wire [{0}:0] gb_{role}_l{l} = (gu_lax == 2'd{code}) ? (gb_lbase | {cwd}'d{l}) : {{4'd0, {src}}};", cwd - 1));
1431                }
1432                e.l(format!("  wire gb_lv{l} = gbo[{}];", 3 * ax + 1 + l));
1433            }
1434        }
1435        for s in 0..g {
1436            e.l(format!("  reg signed [{}:0] gu_lin{s};", aw - 1));
1437            if vec {
1438                for l in 0..vl {
1439                    e.l(format!("  reg signed [{}:0] gv{s}_l{l};", wd.slot - 1));
1440                }
1441            } else {
1442                e.l(format!("  reg signed [{}:0] gv{s};", wd.slot - 1));
1443            }
1444        }
1445        for s in 0..g {
1446            // The slot's read: its point's start, the select its point takes where the slot shares a read, and its walk
1447            // offset, `gu_at` cycles on.
1448            e.l(format!("  wire [{}:0] gi{s}_0 = {{gs_v, gu_pp{s}, gu_lin{s}}};", aw + 1));
1449            delay(&mut e, &format!("gi{s}"), aw as usize + 2, sh.issue_taps[s], &format!("gu_at{s}"), &format!("gi{s}_0"));
1450            e.l(format!("  wire gi_v{s} = gu_on && gi{s}[{}] && (ph >= gu_at{s}), gi_pp{s} = gi{s}[{aw}];", aw + 1));
1451            // A gather adds the value its index slot latched, times its stride — in an op where it gathers: an operand
1452            // register no op has written yet holds no value, and no stride of zero makes one. The index and the product
1453            // are nodes of the offset's width, which is all of either the offset keeps. The product is a register, so
1454            // the slot reads a cycle after its index latched (`Issue::of`) and no multiply stands in front of its
1455            // address.
1456            let gather = if sh.gathers.contains(&s) {
1457                let mut idx = String::new();
1458                for j in 0..g {
1459                    let gv = if vec { format!("gv{j}_l0") } else { format!("gv{j}") };
1460                    idx.push_str(&format!("(gu_gop{s} == 8'd{j}) ? $signed({gv}) : "));
1461                }
1462                idx.push('0');
1463                e.l(format!("  wire signed [{0}:0] gu_gx{s} = {idx}; reg signed [{0}:0] gu_gm{s};", aw - 1));
1464                e.l(format!("  always @(posedge clk) gu_gm{s} <= gu_gst{s} * gu_gx{s};"));
1465                format!(" + (gu_gon{s} ? gu_gm{s} : 0)")
1466            } else {
1467                String::new()
1468            };
1469            e.l(format!("  wire signed [{}:0] gu_off{s} = gu_base{s} + $signed(gi{s}[{}:0]){gather};", aw - 1, aw - 1));
1470            e.l(format!("  wire gu_in{s} = (gu_off{s} >= 0) && (gu_off{s} < gu_lim{s});"));
1471            let el = if aw > 32 { format!("gu_off{s}[31:0]") } else { format!("gu_off{s}") };
1472            e.l(format!("  wire [31:0] gu_el{s} = {el};"));
1473            // Its value lands `lat` cycles after the read, zero where the read fell outside the page.
1474            if !vec {
1475                if lat == 0 {
1476                    e.l(format!("  wire gl_v{s} = gi_v{s}, gl_in{s} = gu_in{s};"));
1477                } else {
1478                    for d in 1..=lat {
1479                        let prev = if d == 1 { format!("gi_v{s}, gu_in{s}") } else { format!("gl_v{s}_{0}, gl_in{s}_{0}", d - 1) };
1480                        e.l(format!("  reg gl_v{s}_{d}, gl_in{s}_{d}; always @(posedge clk) {{gl_v{s}_{d}, gl_in{s}_{d}}} <= {{{prev}}};"));
1481                    }
1482                    e.l(format!("  wire gl_v{s} = gl_v{s}_{lat}, gl_in{s} = gl_in{s}_{lat};"));
1483                }
1484                e.l(format!("  always @(posedge clk) if (gl_v{s}) gv{s} <= gl_in{s} ? $signed(gu_ld{s}) : 0;"));
1485                // Held to the point's body cycle.
1486                delay(&mut e, &format!("g_op{s}"), wd.slot as usize, sh.hold_taps[s], &format!("gu_dl{s}"), &format!("gv{s}"));
1487                continue;
1488            }
1489            // A run's lane `l` reads element `l` past the run's start: inside the page or zero, as one element is. A slot
1490            // that reads one element a point gives every lane that element. A slot a vector op gathers through reads, in
1491            // lane `l`, the element its index slot's lane `l` names: its own offset, through a port of its own.
1492            let lane_gather = sh.lane_gathers.contains(&s);
1493            for l in 1..vl {
1494                if lane_gather {
1495                    let mut idx = String::new();
1496                    for j in 0..g {
1497                        idx.push_str(&format!("(gu_gop{s} == 8'd{j}) ? $signed(gv{j}_l{l}) : "));
1498                    }
1499                    idx.push('0');
1500                    e.l(format!("  wire signed [{0}:0] gu_gx{s}_l{l} = {idx}; reg signed [{0}:0] gu_gm{s}_l{l};", aw - 1));
1501                    e.l(format!("  always @(posedge clk) gu_gm{s}_l{l} <= gu_gst{s} * gu_gx{s}_l{l};"));
1502                    e.l(format!(
1503                        "  wire signed [{}:0] gu_off{s}_l{l} = gu_base{s} + $signed(gi{s}[{}:0]) + (gu_gon{s} ? gu_gm{s}_l{l} : 0);",
1504                        aw - 1,
1505                        aw - 1
1506                    ));
1507                    let el = if aw > 32 { format!("gu_off{s}_l{l}[31:0]") } else { format!("gu_off{s}_l{l}") };
1508                    e.l(format!("  wire [31:0] gu_el{s}_l{l} = {el};"));
1509                    e.l(format!(
1510                        "  wire gu_in{s}_l{l} = gu_gon{s} ? ((gu_off{s}_l{l} >= 0) && (gu_off{s}_l{l} < gu_lim{s})) : gu_bc{s} ? gu_in{s} : ((gu_off{s} + {l} >= 0) && (gu_off{s} + {l} < gu_lim{s}));"
1511                    ));
1512                } else {
1513                    e.l(format!("  wire gu_in{s}_l{l} = gu_bc{s} ? gu_in{s} : ((gu_off{s} + {l} >= 0) && (gu_off{s} + {l} < gu_lim{s}));"));
1514                }
1515            }
1516            let lane_in = |l: usize| if l == 0 { format!("gu_in{s}") } else { format!("gu_in{s}_l{l}") };
1517            let ins: Vec<String> = (0..vl).rev().map(lane_in).collect();
1518            e.l(format!("  wire [{}:0] gu_ins{s} = {{{}}};", vl - 1, ins.join(", ")));
1519            if lat == 0 {
1520                e.l(format!("  wire gl_v{s} = gi_v{s}; wire [{}:0] gl_in{s} = gu_ins{s};", vl - 1));
1521            } else {
1522                for d in 1..=lat {
1523                    let prev = if d == 1 { format!("gi_v{s}, gu_ins{s}") } else { format!("gl_v{s}_{0}, gl_in{s}_{0}", d - 1) };
1524                    e.l(format!("  reg gl_v{s}_{d}; reg [{}:0] gl_in{s}_{d}; always @(posedge clk) {{gl_v{s}_{d}, gl_in{s}_{d}}} <= {{{prev}}};", vl - 1));
1525                }
1526                e.l(format!("  wire gl_v{s} = gl_v{s}_{lat}; wire [{}:0] gl_in{s} = gl_in{s}_{lat};", vl - 1));
1527            }
1528            for l in 0..vl {
1529                e.l(format!("  always @(posedge clk) if (gl_v{s}) gv{s}_l{l} <= gl_in{s}[{l}] ? $signed(gu_ld{s}_l{l}) : 0;"));
1530                delay(&mut e, &format!("g_op{s}_l{l}"), wd.slot as usize, sh.hold_taps[s], &format!("gu_dl{s}"), &format!("gv{s}_l{l}"));
1531            }
1532        }
1533        for lanes in &lane_pipes {
1534            for (lines, _, _, _) in lanes {
1535                for l in lines {
1536                    e.l(l.clone());
1537                }
1538            }
1539        }
1540        e.l("  wire gu_pd = (gu_kx == 1) || (gb_x == gu_kx - 1);");
1541        e.l(format!("  wire [{axb}:0] gu_ia = gu_kind[0] ? gb_c : gb_r;"));
1542        // The control word of the point: its column to write, its row, the folded axis's index; whether it writes, folds
1543        // the outer axis, starts each fold, finishes and starts its contraction; whether it is a point at all — and in a
1544        // vector unit, above them, which of its lanes are.
1545        let cs = 7 + 3 * ax;
1546        let cw = if vec { cs + vl } else { cs };
1547        let lane_bits = if vec { format!("{}, ", (0..vl).rev().map(|l| format!("gb_lv{l}")).collect::<Vec<_>>().join(", ")) } else { String::new() };
1548        e.l(format!(
1549            "  wire [{}:0] gc0 = {{{lane_bits}(gu_kind == 2'd0 || gu_kind == 2'd2) ? gb_c : {ax}'d0, gb_r, gu_ia, gu_pd && ((gu_kind == 2'd0) || (gu_kind == 2'd1 && gb_c == gu_rinner_cx) || (gu_kind == 2'd2 && gb_r == gu_rx - 1) || (gu_kind == 2'd3 && gb_c == gu_rinner_cx && gb_r == gu_rx - 1)), gu_kind == 2'd3 && gu_pd && gb_c == gu_rinner_cx, gb_r == 0, gu_ia == 0, gu_pd, gu_kx == 1 || gb_x == 0, gb_v}};",
1550            cw - 1
1551        ));
1552        for d in 1..=dmax {
1553            e.l(format!("  reg [{}:0] gc{d}; always @(posedge clk) gc{d} <= gc{};", cw - 1, d - 1));
1554        }
1555        // The point whose value leaves the op's pipeline now: the control word as many cycles back as its class and its
1556        // lane tree are deep, and never one from before the op began.
1557        let taps: BTreeSet<u64> = sh
1558            .cls
1559            .op_class
1560            .iter()
1561            .filter_map(|(i, (c, _))| match &micro[*i] {
1562                Micro::Fixed(Fixed::Generic { unit, .. }) => Some(pipes[*c].3 + unit.tree_levels()),
1563                _ => None,
1564            })
1565            .collect();
1566        let mut tap = format!("  reg [{}:0] gco; always @* case (gu_dep)", cw - 1);
1567        for d in &taps {
1568            tap.push_str(&format!(" {d}: gco = gc{d};"));
1569        }
1570        tap.push_str(" default: gco = gc0; endcase");
1571        e.l(tap);
1572        if vec {
1573            // The control word of the point whose lanes leave their class's pipeline now, ahead of the lane tree: which
1574            // lanes are, and the folded axis's index their coordinates count from.
1575            let class_taps: BTreeSet<u64> = pipes.iter().map(|p| p.3).collect();
1576            let mut tap = format!("  reg [{}:0] gcd; always @* case (gu_cdep)", cw - 1);
1577            for d in &class_taps {
1578                tap.push_str(&format!(" {d}: gcd = gc{d};"));
1579            }
1580            tap.push_str(" default: gcd = gc0; endcase");
1581            e.l(tap);
1582            e.l(format!("  wire [{}:0] gq_lv = gco[{}:{cs}], gd_lv = gcd[{}:{cs}];", vl - 1, cw - 1, cw - 1));
1583            e.l(format!("  wire [{axb}:0] gd_ia = gcd[{}:7];", 6 + ax));
1584        }
1585        e.l("  wire gq_v = gu_on && gco[0] && (ph >= gu_dep);");
1586        e.l("  wire gq_kfirst = gco[1], gq_pd = gco[2], gq_ia0 = gco[3], gq_r0 = gco[4], gq_ofold = gco[5], gq_we = gco[6];");
1587        e.l(format!(
1588            "  wire [{axb}:0] gq_ia = gco[{}:7], gq_r = gco[{}:{}], gq_wcol = gco[{}:{}];",
1589            6 + ax,
1590            6 + 2 * ax,
1591            7 + ax,
1592            6 + 3 * ax,
1593            7 + 2 * ax
1594        ));
1595        // Under simulation: every point that leaves the pipeline is a point of the op it leaves in.
1596        e.l("  // synthesis translate_off");
1597        e.l("  always @(posedge clk) if (gq_v && (gq_r >= gu_rx || gq_wcol > gu_rinner_cx)) begin $display(\"FATAL: a point of row %0d, column %0d leaves the pipeline of an op of %0d rows and %0d columns\", gq_r, gq_wcol, gu_rx, gu_rinner_cx + 1); $finish; end");
1598        e.l("  // synthesis translate_on");
1599        let vw = wd.value - 1;
1600        if vec {
1601            // Each lane's value as it leaves its class's pipeline.
1602            for l in 0..vl {
1603                let mut v = format!("  wire signed [{vw}:0] gu_v_l{l} = ");
1604                for (c, lanes) in lane_pipes.iter().enumerate() {
1605                    if let Some((_, _, root, _)) = lanes.get(l) {
1606                        v.push_str(&format!("(gu_cls == 8'd{c}) ? $signed({root}) : "));
1607                    }
1608                }
1609                v.push_str("0;");
1610                e.l(v);
1611            }
1612            // ★ THE LANE TREE: a run's lanes folded pairwise, a registered level a pair, each node the fold of the lanes
1613            // below it — their sum, or the larger and on a tie the lower lane, so the first maximum — over the lanes that
1614            // are the op's and inside its extent, a node of none none. An op of `2^t` lanes takes level `t`'s first node.
1615            if sh.tree_levels > 0 {
1616                for j in 0..vl {
1617                    e.l(format!("  wire signed [{vw}:0] tv0_{j} = gu_v_l{j}; wire tval0_{j} = gd_lv[{j}]; wire [{axb}:0] tix0_{j} = (gd_ia << gu_lsh) | {ax}'d{j};"));
1618                }
1619                for lv in 1..=sh.tree_levels {
1620                    for j in 0..(vl >> lv) {
1621                        let (a, b) = (2 * j, 2 * j + 1);
1622                        let p = lv - 1;
1623                        e.l(format!(
1624                            "  wire tpick{lv}_{j} = !tval{p}_{a} || (tval{p}_{b} && (gu_tpool != 2'd0) && ($signed(tv{p}_{b}) > $signed(tv{p}_{a})));"
1625                        ));
1626                        e.l(format!("  reg signed [{vw}:0] tv{lv}_{j}; reg tval{lv}_{j}; reg [{axb}:0] tix{lv}_{j};"));
1627                        e.l(format!(
1628                            "  always @(posedge clk) begin tval{lv}_{j} <= tval{p}_{a} || tval{p}_{b}; tix{lv}_{j} <= tpick{lv}_{j} ? tix{p}_{b} : tix{p}_{a}; tv{lv}_{j} <= (gu_tpool == 2'd0 && tval{p}_{a} && tval{p}_{b}) ? $signed(tv{p}_{a}) + $signed(tv{p}_{b}) : tpick{lv}_{j} ? tv{p}_{b} : tv{p}_{a}; end"
1629                        ));
1630                    }
1631                }
1632                let mut v = format!("  reg signed [{vw}:0] gu_v; reg [{axb}:0] gu_tix; always @* case (gu_tl)");
1633                v.push_str(" 0: begin gu_v = gu_v_l0; gu_tix = gq_ia; end");
1634                for lv in 1..=sh.tree_levels {
1635                    v.push_str(&format!(" {lv}: begin gu_v = tv{lv}_0; gu_tix = tix{lv}_0; end"));
1636                }
1637                v.push_str(" default: begin gu_v = gu_v_l0; gu_tix = gq_ia; end endcase");
1638                e.l(v);
1639            } else {
1640                e.l(format!("  wire signed [{vw}:0] gu_v = gu_v_l0; wire [{axb}:0] gu_tix = gq_ia;"));
1641            }
1642        } else {
1643            let mut v = format!("  wire signed [{}:0] gu_v = ", wd.value - 1);
1644            for (c, (_, _, root, _)) in pipes.iter().enumerate() {
1645                v.push_str(&format!("(gu_cls == 8'd{c}) ? $signed({root}) : "));
1646            }
1647            v.push_str("0;");
1648            e.l(v);
1649        }
1650        // Stage A, the body's cycle: the contraction.
1651        e.l(format!("  wire signed [{vw}:0] gu_pv = gq_kfirst ? gu_v : ($signed(g_kacc) + gu_v);"));
1652        // Stage B: the fold of the innermost reduced axis, over the contraction a cycle on. A point's control rides
1653        // beside its value, and an op's last stages land within its own cycles, so a valid bit never crosses ops.
1654        e.l(format!("  reg fb_v, fb_pd, fb_ia0, fb_r0, fb_ofold, fb_we; reg [{axb}:0] fb_ia, fb_r, fb_wcol; reg signed [{vw}:0] fb_pv;"));
1655        let fb_ia = if vec { "gu_tix" } else { "gq_ia" };
1656        e.l(format!("  always @(posedge clk) begin fb_v <= gq_v && !op_last; fb_pd <= gq_pd; fb_ia0 <= gq_ia0; fb_r0 <= gq_r0; fb_ofold <= gq_ofold; fb_we <= gq_we; fb_ia <= {fb_ia}; fb_r <= gq_r; fb_wcol <= gq_wcol; fb_pv <= gu_pv; end"));
1657        for (pool, tag) in [(ReducePool::Sum, 0), (ReducePool::Max, 1), (ReducePool::ArgMax, 2)] {
1658            let (fv, fi) = fold_step(pool, "fb_ia0", "g_fold", "g_fidx", "fb_pv", "fb_ia");
1659            e.l(format!("  wire signed [{vw}:0] gu_fv{tag} = {fv}; wire [{axb}:0] gu_fi{tag} = {fi};"));
1660        }
1661        e.l(format!("  wire signed [{vw}:0] gu_fv = (gu_pool == 2'd2) ? gu_fv2 : (gu_pool == 2'd1) ? gu_fv1 : gu_fv0;"));
1662        e.l(format!("  wire [{axb}:0] gu_fi = (gu_pool == 2'd2) ? gu_fi2 : (gu_pool == 2'd1) ? gu_fi1 : gu_fi0;"));
1663        e.l(format!("  wire signed [{vw}:0] gu_ir = (gu_pool == 2'd2) ? $signed({{1'b0, gu_fi}}) : gu_fv;"));
1664        // Stage C: the outer fold of a full reduction, and the write.
1665        e.l(format!("  reg fc_v, fc_r0, fc_ofold, fc_we; reg [{axb}:0] fc_r, fc_wcol; reg signed [{vw}:0] fc_pv, fc_ir;"));
1666        e.l("  always @(posedge clk) begin fc_v <= fb_v && !op_last; fc_r0 <= fb_r0; fc_ofold <= fb_ofold; fc_we <= fb_we; fc_r <= fb_r; fc_wcol <= fb_wcol; fc_pv <= fb_pv; fc_ir <= gu_ir; end");
1667        for (pool, tag) in [(ReducePool::Sum, 0), (ReducePool::Max, 1), (ReducePool::ArgMax, 2)] {
1668            let (ov, oi) = fold_step(pool, "fc_r0", "g_ofold", "g_oidx", "fc_ir", "fc_r");
1669            e.l(format!("  wire signed [{vw}:0] gu_ov{tag} = {ov}; wire [{axb}:0] gu_oi{tag} = {oi};"));
1670        }
1671        e.l(format!("  wire signed [{vw}:0] gu_ov = (gu_rpool == 2'd2) ? gu_ov2 : (gu_rpool == 2'd1) ? gu_ov1 : gu_ov0;"));
1672        e.l(format!("  wire [{axb}:0] gu_oi = (gu_rpool == 2'd2) ? gu_oi2 : (gu_rpool == 2'd1) ? gu_oi1 : gu_oi0;"));
1673        e.l(format!("  wire signed [{vw}:0] gu_or = (gu_rpool == 2'd2) ? $signed({{1'b0, gu_oi}}) : gu_ov;"));
1674        if vec {
1675            // Each further lane of an op whose lanes each keep their own column: its contraction, its fold of the axis
1676            // walked inside, and its write, beside lane 0's, on lane 0's control.
1677            // Only what the engine's ops of that kind use: a contraction where one contracts, the folds of their pools.
1678            e.l(format!("  reg [{}:0] fb_lv, fc_lv; always @(posedge clk) begin fb_lv <= gq_lv; fc_lv <= fb_lv; end", vl - 1));
1679            let pools: Vec<(ReducePool, u8)> =
1680                [(ReducePool::Sum, 0u8), (ReducePool::Max, 1), (ReducePool::ArgMax, 2)].into_iter().filter(|(p, _)| sh.lane_pools[pool_index(*p)]).collect();
1681            for l in 1..vl {
1682                e.l(format!("  reg signed [{vw}:0] fb_pv_l{l}, fc_pv_l{l};"));
1683                if sh.lane_contracts {
1684                    e.l(format!("  reg signed [{vw}:0] g_kacc_l{l};"));
1685                    e.l(format!("  wire signed [{vw}:0] gu_pv_l{l} = gq_kfirst ? gu_v_l{l} : ($signed(g_kacc_l{l}) + gu_v_l{l});"));
1686                } else {
1687                    e.l(format!("  wire signed [{vw}:0] gu_pv_l{l} = gu_v_l{l};"));
1688                }
1689                e.l(format!("  always @(posedge clk) begin fb_pv_l{l} <= gu_pv_l{l}; fc_pv_l{l} <= fb_pv_l{l}; end"));
1690                if pools.is_empty() {
1691                    continue;
1692                }
1693                e.l(format!("  reg signed [{vw}:0] g_fold_l{l}, fc_ir_l{l}; reg [{axb}:0] g_fidx_l{l};"));
1694                for (pool, tag) in &pools {
1695                    let (fv, fi) = fold_step(*pool, "fb_ia0", &format!("g_fold_l{l}"), &format!("g_fidx_l{l}"), &format!("fb_pv_l{l}"), "fb_ia");
1696                    e.l(format!("  wire signed [{vw}:0] gu_fv{tag}_l{l} = {fv}; wire [{axb}:0] gu_fi{tag}_l{l} = {fi};"));
1697                }
1698                let pick = |what: &str| -> String {
1699                    let (last, rest) = pools.split_last().expect("a pool");
1700                    let mut x = String::new();
1701                    for (_, tag) in rest {
1702                        x.push_str(&format!("(gu_pool == 2'd{tag}) ? {what}{tag}_l{l} : "));
1703                    }
1704                    format!("{x}{what}{}_l{l}", last.1)
1705                };
1706                e.l(format!("  wire signed [{vw}:0] gu_fv_l{l} = {};", pick("gu_fv")));
1707                e.l(format!("  wire [{axb}:0] gu_fi_l{l} = {};", pick("gu_fi")));
1708                let ir = if sh.lane_pools[pool_index(ReducePool::ArgMax)] {
1709                    format!("(gu_pool == 2'd2) ? $signed({{1'b0, gu_fi_l{l}}}) : gu_fv_l{l}")
1710                } else {
1711                    format!("gu_fv_l{l}")
1712                };
1713                e.l(format!("  wire signed [{vw}:0] gu_ir_l{l} = {ir};"));
1714                e.l(format!("  always @(posedge clk) fc_ir_l{l} <= gu_ir_l{l};"));
1715            }
1716        }
1717    }
1718    // Scalar grid.
1719    let needs_grid = micro.iter().any(|m| matches!(m, Micro::Fixed(Fixed::Unary { kind: UnaryKind::Scalar(_), .. })));
1720    if needs_grid {
1721        let gname = grid_names.get(&plan.class).cloned().unwrap_or_else(|| format!("g{}", plan.class));
1722        e.l("  wire [63:0] grid_result;");
1723        e.l(format!("  grid_{gname} u_grid(.in0(unary_in), .result(grid_result));"));
1724    } else {
1725        e.l("  wire grid_result = 1'b0;");
1726    }
1727
1728    // ── Comb: per-state port drives and live values ──────────────────────────────────────────────────────────
1729    let mut comb = Emit { s: String::new() };
1730    comb.l("  always @* begin");
1731    for b in &bufs {
1732        let k = tp(*b);
1733        if uses.produces.contains(b) {
1734            comb.l(format!("    f{k}_reserve = 1'b0; f{k}_reserve_n = 0; f{k}_push = 1'b0; f{k}_push_n = 0;"));
1735        }
1736        if uses.consumes.contains(b) {
1737            comb.l(format!("    f{k}_pop = 1'b0; f{k}_pop_n = 0;"));
1738        }
1739        if uses.writes.contains(b) {
1740            comb.l(format!("    f{k}_wr_en = 1'b0; f{k}_wr_slot = 0; f{k}_wr_elem = 0; f{k}_wr_data = 64'd0;"));
1741            if ctx.banks > 1 {
1742                comb.l(format!("    f{k}_wr_n = 1; f{k}_wr_lw = 0; f{k}_wr_hi = 0;"));
1743            }
1744        }
1745        if uses.reads.contains(b) {
1746            comb.l(format!("    f{k}_rd_slot = 0; f{k}_rd_elem = 0;"));
1747            for l in 1..gathered.get(&ctx.template(*b)).copied().unwrap_or(1) {
1748                comb.l(format!("    f{k}_l{l}_rd_slot = 0; f{k}_l{l}_rd_elem = 0;"));
1749            }
1750        }
1751    }
1752    for t in &t_rd {
1753        comb.l(format!("    t{}_rd_addr = 32'd0;", t.idx()));
1754    }
1755    for t in &t_wr {
1756        comb.l(format!("    t{0}_wr_en = 1'b0; t{0}_wr_addr = 32'd0; t{0}_wr_data = 64'd0;", t.idx()));
1757        if ctx.banks > 1 {
1758            comb.l(format!("    t{0}_wr_n = 1; t{0}_wr_lw = 0; t{0}_wr_hi = 0;", t.idx()));
1759        }
1760    }
1761    for s in &s_local {
1762        comb.l(format!("    s{0}_set = 1'b0; s{0}_set_val = 16'd0;", s.idx()));
1763    }
1764    for s in &s_inc {
1765        comb.l(format!("    s{0}_inc_remote = 1'b0; s{0}_inc_n = 16'd0;", s.idx()));
1766    }
1767    for (s, gr) in &m_valid {
1768        comb.l(format!("    mv{}_g{} = 1'b0;", s.idx(), gr.idx()));
1769    }
1770    for gr in &m_send {
1771        comb.l(format!("    mc{0}_send = 1'b0; mc{0}_din = 80'd0;", gr.idx()));
1772    }
1773    {
1774        let mut d = String::from("   ");
1775        for (n, _, _) in &live {
1776            d.push_str(&format!(" {n} = 0;"));
1777        }
1778        comb.l(d);
1779    }
1780    comb.l("    if (running) case (pc)");
1781
1782    // ── The sequencer: the next state, then its register ────────────────────────────────────────────────────────
1783    let mut seq = Emit { s: String::new() };
1784    seq.l("  always @* begin");
1785    {
1786        let mut hold = String::from("    pc_n = pc; ph_n = ph; running_n = running; done_n = done;");
1787        for v in &lvs {
1788            hold.push_str(&format!(" {0}_n = {0};", var_name(*v)));
1789        }
1790        seq.l(hold);
1791    }
1792    seq.l("    if (!running && start && !done) begin running_n = 1'b1; pc_n = 0; ph_n = 0; end");
1793    seq.l("    else if (running) case (pc)");
1794    // Each running state's successor, and each fixed op's row of the op table and load of the cycle counter, which the
1795    // two print as the rows of the successors the state can take.
1796    let mut nexts: Vec<(usize, Next)> = Vec::new();
1797    let mut rows: BTreeMap<usize, String> = BTreeMap::new();
1798    let mut counts: BTreeMap<usize, String> = BTreeMap::new();
1799
1800    let mut bound: BTreeSet<LoopVar> = BTreeSet::new();
1801    for (i, m) in micro.iter().enumerate() {
1802        let next = i + 1;
1803        match m {
1804            Micro::LoopStart { var, count, end } => {
1805                bound.insert(*var);
1806                let c = if *var == LV_ITEM { "ITEM_COUNT".to_string() } else { count.to_string() };
1807                let to = Next::when(format!("({c} == 0)"), Next::To(end + 1), Next::To(next));
1808                seq.l(format!("      {i}: begin {}_n = 0; pc_n = {}; end", var_name(*var), to.expr()));
1809                nexts.push((i, to));
1810            }
1811            Micro::LoopEnd { var, count, start } => {
1812                bound.remove(var);
1813                // A loop's end is reached with its counter below its count, so it runs again while the counter is not the
1814                // last: an equality with a constant, no carry.
1815                let last = if *var == LV_ITEM { "(ITEM_COUNT - 1)".to_string() } else { count.saturating_sub(1).to_string() };
1816                let v = var_name(*var);
1817                let to = Next::when(format!("({v} != {last})"), Next::To(start + 1), Next::To(next));
1818                seq.l(format!("      {i}: begin {v}_n = {v} + 1; pc_n = {}; end", to.expr()));
1819                nexts.push((i, to));
1820            }
1821            // A folded effect is printed with the op it is folded into; it has no state.
1822            Micro::Then(_) => {}
1823            Micro::Guarded(gd) => {
1824                let cond = guard_cond(ctx, gd);
1825                if let Guard::Reserve { buf, pages } = gd {
1826                    let k = ctx.template(*buf).idx();
1827                    comb.l(format!("      {i}: begin f{k}_reserve = {cond}; f{k}_reserve_n = {pages}; end"));
1828                }
1829                let to = Next::when(cond, Next::To(next), Next::To(i));
1830                seq.l(format!("      {i}: pc_n = {};", to.expr()));
1831                nexts.push((i, to));
1832            }
1833            Micro::Fixed(f) => {
1834                let d = ctx.duration(f);
1835                // Through the loop bound its last cycle takes itself, then past the guards of the branch it checks early.
1836                let exit = super::exit_of(micro, i, plan.site);
1837                let check = |e: &super::Early| -> String {
1838                    use super::Early;
1839                    match *e {
1840                        Early::Count(_, n) | Early::Free(_, n) | Early::AtLeast(_, n) if n <= 0 => "1'b1".into(),
1841                        Early::Count(b, n) => format!("(f{}_count >= {n})", ctx.template(b).idx()),
1842                        Early::Free(b, n) => format!("(f{}_free >= {n})", ctx.template(b).idx()),
1843                        Early::AtLeast(sem, n) => format!("(s{}_count >= {n})", sem.idx()),
1844                        Early::Equals(sem, n) => format!("(s{}_count == {n})", sem.idx()),
1845                    }
1846                };
1847                let branch = |(at, checks): &(usize, Vec<(usize, super::Early)>)| -> (Next, Option<String>) {
1848                    match checks.last() {
1849                        Some((last, _)) => {
1850                            let all = checks.iter().map(|(_, e)| check(e)).collect::<Vec<_>>().join(" && ");
1851                            (Next::when(format!("({all})"), Next::To(last + 1), Next::To(*at)), Some(all))
1852                        }
1853                        None => (Next::To(*at), None),
1854                    }
1855                };
1856                let count = |var: LoopVar, count: usize| if var == LV_ITEM { "ITEM_COUNT".to_string() } else { count.to_string() };
1857                let final_count = |var: LoopVar, count: usize| if var == LV_ITEM { "(ITEM_COUNT - 1)".to_string() } else { count.saturating_sub(1).to_string() };
1858                let (to, steps, taken): (Next, String, Vec<String>) = match exit.bound {
1859                    None => (branch(&exit.branches[0]).0, String::new(), vec!["1'b1".into()]),
1860                    Some(super::Bound::End { var, count: c, .. }) => {
1861                        let v = var_name(var);
1862                        let runs = format!("({v} != {})", final_count(var, c));
1863                        (
1864                            Next::when(runs.clone(), branch(&exit.branches[0]).0, branch(&exit.branches[1]).0),
1865                            format!(" {v}_n = {v} + 1;"),
1866                            vec![runs.clone(), format!("!{runs}")],
1867                        )
1868                    }
1869                    Some(super::Bound::Start { var, count: c, .. }) => {
1870                        let empty = format!("({} == 0)", count(var, c));
1871                        (
1872                            Next::when(empty.clone(), branch(&exit.branches[1]).0, branch(&exit.branches[0]).0),
1873                            format!(" {}_n = 0;", var_name(var)),
1874                            vec![format!("!{empty}"), empty.clone()],
1875                        )
1876                    }
1877                };
1878                seq.l(format!("      {i}: if (op_last) begin ph_n = 0;{steps} pc_n = {}; end else ph_n = ph + 1;", to.expr()));
1879                counts.insert(i, format!(" op_left <= {}; op_last <= {};", d.max(1) - 1, if d <= 1 { "1'b1" } else { "1'b0" }));
1880                let mut c = format!("      {i}: begin");
1881                // A branch whose guards all hold takes their reservations in the op's last cycle.
1882                for (b, taken) in exit.branches.iter().zip(&taken) {
1883                    let Some(all) = branch(b).1 else { continue };
1884                    let mut takes = String::new();
1885                    for (g, _) in &b.1 {
1886                        if let Micro::Guarded(Guard::Reserve { buf, pages }) = &micro[*g] {
1887                            let k = ctx.template(*buf).idx();
1888                            takes.push_str(&format!(" f{k}_reserve = 1'b1; f{k}_reserve_n = {pages};"));
1889                        }
1890                    }
1891                    if !takes.is_empty() {
1892                        c.push_str(&format!(" if (op_last && {taken} && {all}) begin{takes} end"));
1893                    }
1894                }
1895                let after = super::after_effects(micro, i);
1896                let mut entries: Vec<(String, String)> = vec![("fx".into(), "1'b1".into())];
1897                print_op(ctx, &sh, &wd, &regions, i, f, &bound, &mut entries, &mut c)?;
1898                // The effects folded into the op commit in its last cycle.
1899                for (j, m) in micro.iter().enumerate().take(after).skip(i + 1) {
1900                    if let Micro::Then(e) = m {
1901                        let mut drives = String::new();
1902                        let mut none = Vec::new();
1903                        print_op(ctx, &sh, &wd, &regions, j, e, &bound, &mut none, &mut drives)?;
1904                        debug_assert!(none.is_empty(), "an effect sets no unit parameter");
1905                        if !drives.is_empty() {
1906                            c.push_str(&format!(" if (op_last) begin{drives} end"));
1907                        }
1908                    }
1909                }
1910                c.push_str(" end");
1911                comb.l(c);
1912                rows.insert(i, entries.iter().map(|(n, v)| format!(" {n} <= {v};")).collect());
1913                nexts.push((i, Next::when("(op_last)".into(), to, Next::To(i))));
1914            }
1915        }
1916    }
1917    let fin = micro.len();
1918    seq.l(format!("      {fin}: begin running_n = 1'b0; done_n = 1'b1; end"));
1919    seq.l("      default: pc_n = 0;");
1920    seq.l("    endcase");
1921    seq.l("  end");
1922    {
1923        let mut reset = String::from("  always @(posedge clk) if (rst) begin pc <= 0; ph <= 0; running <= 1'b0; done <= 1'b0;");
1924        let mut r = String::from(" end else begin pc <= pc_n; ph <= ph_n; running <= running_n; done <= done_n;");
1925        for v in &lvs {
1926            reset.push_str(&format!(" {} <= 0;", var_name(*v)));
1927            r.push_str(&format!(" {0} <= {0}_n;", var_name(*v)));
1928        }
1929        r.push_str(" end");
1930        seq.l(format!("{reset}{r}"));
1931    }
1932    comb.l("      default: ;");
1933    comb.l("    endcase");
1934    comb.l("  end");
1935
1936    // ── The op table, loaded on the clock that enters a state ───────────────────────────────────────────────────
1937    // Each running state loads the row of the successor it takes, chosen by the conditions its next state is, so the
1938    // decode in front of the table is of the registered state and the conditions select among a few constant rows. The
1939    // final state loads none, as the run ends there; a state the sequencer never holds loads the first's, as it restarts.
1940    let row = |s: usize| rows.get(&s).cloned().unwrap_or_default();
1941    let mut tab = Emit { s: String::new() };
1942    tab.l("  always @(posedge clk) begin");
1943    {
1944        let mut d = String::from("   ");
1945        for (n, _, _) in &table {
1946            d.push_str(&format!(" {n} <= 0;"));
1947        }
1948        tab.l(d);
1949    }
1950    tab.l(format!("    if (rst) ; else if (!running) begin if (start && !done) begin{} end end", row(0)));
1951    tab.l("    else case (pc)");
1952    for (i, nx) in &nexts {
1953        tab.l(format!("      {i}: begin{} end", nx.load(&row)));
1954    }
1955    tab.l(format!("      {fin}: ;"));
1956    tab.l(format!("      default: begin{} end", row(0)));
1957    tab.l("    endcase");
1958    tab.l("  end");
1959    // A state's first cycle — after a fixed op's last, in any other state, or the run's first — loads the cycle counter
1960    // for the state entered, as the table does. An engine that is not running holds no count.
1961    let zero = " op_left <= 0; op_last <= 1'b0;".to_string();
1962    let count = |s: usize| counts.get(&s).cloned().unwrap_or_else(|| zero.clone());
1963    let mut last = Emit { s: String::new() };
1964    last.l("  wire fresh = !running || !fx || op_last;");
1965    last.l("  always @(posedge clk)");
1966    last.l("    if (rst) begin op_left <= 0; op_last <= 1'b0; end");
1967    last.l(format!("    else if (!running) begin if (start && !done) begin{} end else begin{zero} end end", count(0)));
1968    last.l("    else if (fresh) case (pc)");
1969    for (i, nx) in &nexts {
1970        last.l(format!("      {i}: begin{} end", nx.load(&count)));
1971    }
1972    last.l(format!("      {fin}: begin{zero} end"));
1973    last.l(format!("      default: begin{} end", count(0)));
1974    last.l("    endcase");
1975    last.l("    else begin op_left <= op_left - 1; op_last <= (op_left == 1); end");
1976
1977    let units = units(ctx, &sh, &wd, te, tr, tc, lat);
1978    // The flags of the phase constants the blocks above compare with: `ph_ge{k}` holds `ph >= k` in every cycle. The
1979    // phase's next value is only ever 0 (a start, a fixed op's last cycle), `ph + 1` (a fixed op counting) or `ph`, so
1980    // the flags follow it as a shift chosen by registers: clear, each from the one below (`ph + 1 >= k` is `ph >= k - 1`),
1981    // or hold. They run dense from 1 so every flag's predecessor is a flag, and no decode of the next state or comparator
1982    // stands in front of them.
1983    let top = ctx.phase_flags.borrow().iter().copied().max().unwrap_or(0);
1984    let ks: Vec<u64> = (1..=top).collect();
1985    if !ks.is_empty() {
1986        let flags: Vec<String> = ks.iter().map(|k| format!("ph_ge{k}")).collect();
1987        e.l(format!("  reg {};", flags.join(", ")));
1988    }
1989    e.s.push_str(&comb.s);
1990    e.s.push_str(&tab.s);
1991    e.s.push_str(&last.s);
1992    e.s.push_str(&seq.s);
1993    if !ks.is_empty() {
1994        let clear: String = ks.iter().map(|k| format!(" ph_ge{k} <= 1'b0;")).collect();
1995        let shift: String = ks.iter().map(|k| if *k == 1 { " ph_ge1 <= 1'b1;".to_string() } else { format!(" ph_ge{k} <= ph_ge{};", k - 1) }).collect();
1996        e.l(format!(
1997            "  always @(posedge clk) if (rst || (running && fx && op_last) || (!running && start && !done)) begin{clear} end else if (running && fx) begin{shift} end"
1998        ));
1999    }
2000    // Under simulation, in every cycle out of reset: each flag is the comparison it stands for, and an engine that is not
2001    // running holds no row of its table. Neither shows in any output (a flag is read only while the engine runs, and a
2002    // parameter only where a state drives its unit), so only these checks see them broken.
2003    e.l("  // synthesis translate_off");
2004    for k in &ks {
2005        e.l(format!("  always @(posedge clk) if (!rst && ph_ge{k} != (ph >= {k})) begin $display(\"FATAL: ph_ge{k} is %0d at phase %0d\", ph_ge{k}, ph); $finish; end"));
2006    }
2007    let held: Vec<&str> = table.iter().map(|(n, _, _)| n.as_str()).collect();
2008    if !held.is_empty() {
2009        e.l(format!(
2010            "  always @(posedge clk) if (!rst && !running && (|{{{}}})) begin $display(\"FATAL: an idle engine holds a row of its op table\"); $finish; end",
2011            held.join(", ")
2012        ));
2013    }
2014    e.l("  // synthesis translate_on");
2015    e.s.push_str(&units);
2016    e.l("endmodule");
2017    // The address width: the largest tensor's elements, every literal the rows load into a walk parameter, and the
2018    // longest op's phase, with a bit of headroom so a sum of two of them never wraps and a difference below zero reads
2019    // above every one.
2020    let at_aw: BTreeSet<&str> = table.iter().filter(|(_, w, _)| *w == AW_WIDE).map(|(n, _, _)| n.as_str()).collect();
2021    let mut bound = p.tensors.iter().map(|t| (t.elems.rows * t.elems.cols) as u64).max().unwrap_or(1).max(longest + 2);
2022    for row in rows.values() {
2023        for stmt in row.split(';') {
2024            if let Some((n, v)) = stmt.split_once("<=") {
2025                if let (true, Ok(v)) = (at_aw.contains(n.trim()), v.trim().parse::<i64>()) {
2026                    bound = bound.max(v.unsigned_abs());
2027                }
2028            }
2029        }
2030    }
2031    let aw = 64 - bound.leading_zeros() + 1;
2032    Ok(e.s.replacen("__AW__", &aw.to_string(), 1))
2033}
2034
2035/// Where a running state goes on the next clock: a state, or the choice a condition on registered values makes between
2036/// two. The sequencer prints it as `pc_n`; the op table and the cycle counter print it as the rows of the states it can
2037/// reach, chosen by the same conditions, so each loads the state entered without decoding `pc_n`.
2038enum Next {
2039    To(usize),
2040    If(String, Box<Next>, Box<Next>),
2041}
2042
2043impl Next {
2044    fn when(cond: String, then: Next, other: Next) -> Next {
2045        Next::If(cond, Box::new(then), Box::new(other))
2046    }
2047
2048    /// The state taken, as the expression `pc_n` is set to.
2049    fn expr(&self) -> String {
2050        match self {
2051            Next::To(s) => s.to_string(),
2052            Next::If(c, a, b) => format!("({c} ? {} : {})", a.expr(), b.expr()),
2053        }
2054    }
2055
2056    /// Statements loading, for the state taken, what `row` prints for that state.
2057    fn load(&self, row: &dyn Fn(usize) -> String) -> String {
2058        match self {
2059            Next::To(s) => row(*s),
2060            Next::If(c, a, b) => format!(" if {c} begin{} end else begin{} end", a.load(row), b.load(row)),
2061        }
2062    }
2063}
2064
2065/// A guard's condition on its FIFO's or semaphore's registered state.
2066fn guard_cond(ctx: &Ctx, g: &Guard) -> String {
2067    match g {
2068        Guard::Reserve { buf, pages } => format!("(f{}_free >= {pages})", ctx.template(*buf).idx()),
2069        Guard::Wait { buf, pages } => format!("(f{}_count >= {pages})", ctx.template(*buf).idx()),
2070        Guard::SemWait { sem, until } => match until {
2071            SemCond::Equals(n) => format!("(s{}_count == {n})", sem.idx()),
2072            SemCond::AtLeast(n) => format!("(s{}_count >= {n})", sem.idx()),
2073        },
2074        Guard::Link { ordinal, .. } => format!("(link_n > 8'd{ordinal})"),
2075    }
2076}
2077
2078/// One fixed op's state: the parameters of the unit it runs on, into `table` as `(name, value)`, and the ports and live
2079/// values it drives as the phase moves, into `c`.
2080#[allow(clippy::too_many_arguments)]
2081fn print_op(
2082    ctx: &Ctx,
2083    sh: &Shape,
2084    wd: &Widths,
2085    regions: &[Option<usize>],
2086    i: usize,
2087    f: &Fixed,
2088    bound: &BTreeSet<LoopVar>,
2089    table: &mut Vec<(String, String)>,
2090    c: &mut String,
2091) -> Result<(), SpatialLowerError> {
2092    let (te, tc, lat) = (ctx.te, ctx.tc, ctx.lat);
2093    let cols = regions[i].unwrap_or(1);
2094    let mut set = |n: &str, v: String| table.push((n.to_string(), v));
2095    match f {
2096        Fixed::Push { buf, pages } => {
2097            let k = ctx.template(*buf).idx();
2098            c.push_str(&format!(" f{k}_push = 1'b1; f{k}_push_n = {pages};"));
2099        }
2100        Fixed::Pop { buf, pages } => {
2101            let k = ctx.template(*buf).idx();
2102            c.push_str(&format!(" f{k}_pop = 1'b1; f{k}_pop_n = {pages};"));
2103        }
2104        Fixed::Barrier | Fixed::ReleaseAcc => {}
2105        Fixed::SemSet { sem, value } => {
2106            let v = match value {
2107                SemValue::Invalid => 0,
2108                SemValue::Valid => 1,
2109                SemValue::Count(n) => *n,
2110            };
2111            c.push_str(&format!(" s{0}_set = 1'b1; s{0}_set_val = {v};", sem.idx()));
2112        }
2113        Fixed::SemInc { sem, by, .. } => {
2114            c.push_str(&format!(" s{0}_inc_remote = 1'b1; s{0}_inc_n = {by};", sem.idx()));
2115        }
2116        Fixed::McastSetValid { sem, group } => {
2117            c.push_str(&format!(" mv{}_g{} = 1'b1;", sem.idx(), group.idx()));
2118        }
2119        Fixed::McastSend { buf, pages, group } => {
2120            let k = ctx.template(*buf).idx();
2121            let pe = ctx.page_elems(*buf);
2122            let total = pages * pe;
2123            let depth = ctx.depth(*buf);
2124            let gr = group.idx();
2125            set("mc_on", "1'b1".into());
2126            set("mc_pe", pe.to_string());
2127            // The page this element is in, after the front: its slot wraps once at most.
2128            let ph_lt_total = ctx.ph_lt(total as u64);
2129            c.push_str(&format!(
2130                " if ({ph_lt_total}) begin f{k}_rd_slot = (f{k}_fill_slot0_r + mc_pg >= {depth}) ? f{k}_fill_slot0_r + mc_pg - {depth} : f{k}_fill_slot0_r + mc_pg; f{k}_rd_elem = mc_e; end"
2131            ));
2132            let elem = if lat == 0 { "ph[15:0]".to_string() } else { format!("ph[15:0] - 16'd{lat}") };
2133            let live = format!("{} && {}", ctx.ph_ge(lat), ctx.ph_lt(total as u64 + lat));
2134            c.push_str(&format!(" if ({live}) begin mc{gr}_send = 1'b1; mc{gr}_din = {{{elem}, f{k}_rd_data}}; end"));
2135        }
2136        Fixed::ReadTiles { tensor, at, block_w, stride_w, stride_h, into, section, .. } => {
2137            let k = ctx.template(*into).idx();
2138            let addr = addr_v(at, bound, item_coord_names(*section).as_tuple(), ctx);
2139            let gather = index_read(ctx, at);
2140            // Element `e`'s address goes out at phase `skew + e` — a gather's waits for its index — and its data lands in
2141            // the page `lat` phases later.
2142            let skew = ctx.gather_skew(at);
2143            let (d1, d2) = walk_deltas(te, *block_w, *stride_w, *stride_h);
2144            let t = tensor.idx();
2145            let cp = ph_less(skew + lat);
2146            let land = from_phase(ctx, 
2147                skew + lat,
2148                &format!("f{k}_wr_en = 1'b1; f{k}_wr_slot = f{k}_fill_slot0; f{k}_wr_elem = {cp}; f{k}_wr_data = t{t}_rd_data;"),
2149            );
2150            set("tw_on", "1'b1".into());
2151            set("tw_from", skew.to_string());
2152            set("tw_bw", block_w.to_string());
2153            if let Some((_, pitch)) = ctx.page_pitch.get(&ctx.template(*into)) {
2154                // The page's rows lie `pitch` apart: the walk steps over each row's padding as it ends the row.
2155                set("tw_pp", (pitch - block_w).to_string());
2156            }
2157            if ctx.walks_runs(f) {
2158                // A run of the row a cycle, from `skew` on; it lands in the page `lat` later, where the walk had it.
2159                let (wt, wf) = (tensor_width(ctx.p.tensor(*tensor), &ctx.ranges), ctx.buffer_width(*into));
2160                let hi = resize_lanes(&format!("t{t}_rd_hi"), wt, wf, ctx.banks - 1);
2161                let land = from_phase(ctx, 
2162                    skew + lat,
2163                    &format!("f{k}_wr_en = 1'b1; f{k}_wr_slot = f{k}_fill_slot0; f{k}_wr_elem = tw_pd; f{k}_wr_n = tw_nd; f{k}_wr_lw = tw_lw; f{k}_wr_data = t{t}_rd_data; f{k}_wr_hi = {hi};"),
2164                );
2165                set("tw_vec", "1'b1".into());
2166                set("tw_lw", ctx.run(f).trailing_zeros().to_string());
2167                set("tw_n0", (*block_w).min(ctx.run(f)).to_string());
2168                set("tw_rs", stride_h.to_string());
2169                c.push_str(&format!("{gather} t{t}_rd_addr = ({addr}) + tw_vo; {land}"));
2170            } else {
2171                set("tw_d1", d1.to_string());
2172                set("tw_d2", d2.to_string());
2173                c.push_str(&format!("{gather} t{t}_rd_addr = ({addr}) * {te} + tw_off; {land}"));
2174            }
2175        }
2176        Fixed::WriteTiles { from, tensor, at, block_w, stride_w, stride_h, section, .. } => {
2177            let k = ctx.template(*from).idx();
2178            if at.indirect.is_some_and(|ind| ctx.template(ind.index) == ctx.template(*from)) {
2179                return Err(SpatialLowerError::Unsupported(
2180                    "a tile scatter whose index sits in the buffer it drains — one read port cannot read both in a cycle",
2181                ));
2182            }
2183            let addr = addr_v(at, bound, item_coord_names(*section).as_tuple(), ctx);
2184            let gather = index_read(ctx, at);
2185            // Element `e` is read at phase `e` and stored `lat` phases later, when its data — and a scatter's index,
2186            // read at every phase — have landed.
2187            let (d1, d2) = walk_deltas(te, *block_w, *stride_w, *stride_h);
2188            let t = tensor.idx();
2189            set("tw_on", "1'b1".into());
2190            set("tw_bw", block_w.to_string());
2191            if ctx.run(f) > 1 {
2192                // A run of the page a cycle from phase 0, stored `lat` later where the walk had it in the tensor.
2193                let (wt, wf) = (tensor_width(ctx.p.tensor(*tensor), &ctx.ranges), ctx.buffer_width(*from));
2194                let hi = resize_lanes(&format!("f{k}_rd_hi"), wf, wt, ctx.banks - 1);
2195                let store = from_phase(ctx, 
2196                    lat,
2197                    &format!("t{t}_wr_en = 1'b1; t{t}_wr_addr = ({addr}) + tw_vod; t{t}_wr_n = tw_nd; t{t}_wr_lw = tw_lw; t{t}_wr_data = f{k}_rd_data; t{t}_wr_hi = {hi};"),
2198                );
2199                set("tw_from", "0".into());
2200                set("tw_vec", "1'b1".into());
2201                set("tw_lw", ctx.run(f).trailing_zeros().to_string());
2202                set("tw_n0", (*block_w).min(ctx.run(f)).to_string());
2203                set("tw_rs", stride_h.to_string());
2204                c.push_str(&format!("{gather} f{k}_rd_slot = f{k}_rd_slot0; f{k}_rd_elem = tw_p; {store}"));
2205            } else {
2206                let store = from_phase(ctx, lat, &format!("t{t}_wr_en = 1'b1; t{t}_wr_addr = ({addr}) * {te} + tw_off; t{t}_wr_data = f{k}_rd_data;"));
2207                set("tw_from", lat.to_string());
2208                set("tw_d1", d1.to_string());
2209                set("tw_d2", d2.to_string());
2210                c.push_str(&format!("{gather} f{k}_rd_slot = f{k}_rd_slot0; f{k}_rd_elem = ph; {store}"));
2211            }
2212        }
2213        Fixed::AcquireAcc { tiles, .. } => {
2214            set("aw_cols", cols.to_string());
2215            if ctx.acc_run(f) > 1 {
2216                // A run of the row's banks zeroed a cycle.
2217                set("ar_on", "1'b1".into());
2218                set("ar_lw", ctx.acc_run(f).trailing_zeros().to_string());
2219                set("ar_e", (tiles * te).to_string());
2220                set("ar_n0", (tiles * te).min(cols).min(ctx.acc_run(f)).to_string());
2221                c.push_str(" az_we = 1'b1;");
2222            } else {
2223                c.push_str(" aw_step = 1'b1; sw_we = 1'b1; sw_d = 0;");
2224            }
2225        }
2226        Fixed::PackAcc { to, tiles, trunc32, accumulate, .. } => {
2227            let k = ctx.template(*to).idx();
2228            // Accumulating into the page reads each element before it writes it back, `lat` phases later.
2229            let skew = if *accumulate { lat } else { 0 };
2230            set("aw_cols", cols.to_string());
2231            if ctx.acc_run(f) > 1 {
2232                // A run of the row's banks a cycle, into the page where the walk had it.
2233                set("ar_on", "1'b1".into());
2234                set("ar_lw", ctx.acc_run(f).trailing_zeros().to_string());
2235                set("ar_e", (tiles * te).to_string());
2236                set("ar_n0", (tiles * te).min(cols).min(ctx.acc_run(f)).to_string());
2237                set("ar_dly", (*accumulate as u8).to_string());
2238                set("pk_tr", (*trunc32 as u8).to_string());
2239                if *accumulate {
2240                    c.push_str(&format!(" f{k}_rd_slot = f{k}_fill_slot0; f{k}_rd_elem = ar_p;"));
2241                }
2242                let lanes: Vec<String> = (1..ctx.banks).rev().map(|l| format!("pk{k}_v{l}")).collect();
2243                let store = from_phase(ctx, 
2244                    skew,
2245                    &format!(
2246                        "f{k}_wr_en = 1'b1; f{k}_wr_slot = f{k}_fill_slot0; f{k}_wr_elem = ar_wp; f{k}_wr_n = ar_wn; f{k}_wr_lw = ar_lw; f{k}_wr_data = $signed(pk{k}_v0); f{k}_wr_hi = {{{}}};",
2247                        lanes.join(", ")
2248                    ),
2249                );
2250                c.push_str(&format!(" {store}"));
2251            } else {
2252                let cp = ph_less(skew);
2253                // An accumulator past 32 bits keeps the low 32 of an I32 plan; one within them holds its value whole.
2254                let v = if *trunc32 && wd.acc > 32 { "{{32{sr_d[31]}}, sr_d[31:0]}".to_string() } else { "sr_d".to_string() };
2255                let base = if *accumulate { format!("$signed(f{k}_rd_data) + ") } else { String::new() };
2256                if *accumulate {
2257                    c.push_str(&format!(" f{k}_rd_slot = f{k}_fill_slot0; f{k}_rd_elem = ph;"));
2258                }
2259                let store = from_phase(ctx, 
2260                    skew,
2261                    &format!("aw_step = 1'b1; f{k}_wr_en = 1'b1; f{k}_wr_slot = f{k}_fill_slot0; f{k}_wr_elem = {cp}; f{k}_wr_data = {base}$signed({v});"),
2262                );
2263                c.push_str(&format!(" {store}"));
2264            }
2265        }
2266        Fixed::Unary { kind, ins, .. } => {
2267            let k = ctx.template(ins[0]).idx();
2268            let pe = ctx.page_elems(ins[0]);
2269            set("un_on", "1'b1".into());
2270            set("un_pe", pe.to_string());
2271            c.push_str(&format!(" f{k}_rd_slot = f{k}_rd_slot0; f{k}_rd_elem = un_e; unary_in = f{k}_rd_data;"));
2272            let kind_code = match kind {
2273                UnaryKind::Requant { m, s } => {
2274                    set("un_m", lit64(*m));
2275                    set("un_s", s.to_string());
2276                    0
2277                }
2278                UnaryKind::Bias => {
2279                    let kb = ctx.template(ins[1]).idx();
2280                    set("un_pb", ctx.page_elems(ins[1]).to_string());
2281                    c.push_str(&format!(
2282                        " f{kb}_rd_slot = f{kb}_rd_slot0; f{kb}_rd_elem = un_bt + (un_bk & {}); unary_bias = f{kb}_rd_data;",
2283                        tc - 1
2284                    ));
2285                    1
2286                }
2287                UnaryKind::Relu => 2,
2288                UnaryKind::Elementwise(op) => {
2289                    let kb = ctx.template(ins[1]).idx();
2290                    set("un_pb", ctx.page_elems(ins[1]).to_string());
2291                    c.push_str(&format!(" f{kb}_rd_slot = f{kb}_rd_slot0; f{kb}_rd_elem = un_e2; unary_bias = f{kb}_rd_data;"));
2292                    match op {
2293                        ElementwiseOp::Add => 4,
2294                        ElementwiseOp::Mul => 5,
2295                    }
2296                }
2297                UnaryKind::Scalar(_) => 6,
2298            };
2299            let store = from_phase(ctx, lat, "aw_step = 1'b1; sw_we = 1'b1;");
2300            set("un_kind", kind_code.to_string());
2301            set("aw_cols", cols.to_string());
2302            c.push_str(&format!(" sw_d = unary_out; {store}"));
2303        }
2304        Fixed::Generic { unit, ins, depth, .. } => {
2305            let (class, values) = &sh.cls.op_class[&i];
2306            let is = &unit.issue;
2307            // The walk counts points: along the lane axis, runs of the lanes.
2308            let (rx, cx, kx) = (unit.points_along(unit.row.var), unit.points_along(unit.col.var), unit.k.map_or(1, |k| unit.points_along(k.var)));
2309            let (nmid, nout) = if unit.rows_inner() { (rx, cx) } else { (cx, rx) };
2310            use IterKind::{Parallel, Reduce};
2311            let (kind, pool, rpool) = match (unit.row.kind, unit.col.kind) {
2312                (Parallel, Parallel) => (0, ReducePool::Sum, ReducePool::Sum),
2313                (Parallel, Reduce { pool }) => (1, pool, ReducePool::Sum),
2314                (Reduce { pool }, Parallel) => (2, pool, ReducePool::Sum),
2315                (Reduce { pool: rp }, Reduce { pool }) => (3, pool, rp),
2316            };
2317            let code = |p: ReducePool| match p {
2318                ReducePool::Sum => 0,
2319                ReducePool::Max => 1,
2320                ReducePool::ArgMax => 2,
2321            };
2322            for (n, v) in [
2323                ("gu_on", "1'b1".to_string()),
2324                ("gu_ii", is.ii.to_string()),
2325                ("gu_kx", kx.to_string()),
2326                ("gu_nmid", nmid.to_string()),
2327                ("gu_nout", nout.to_string()),
2328                ("gu_rinner", (unit.rows_inner() as u8).to_string()),
2329                ("gu_cls", class.to_string()),
2330                ("gu_kind", kind.to_string()),
2331                ("gu_pool", code(pool).to_string()),
2332                ("gu_rpool", code(rpool).to_string()),
2333                ("gu_rinner_cx", (cx - 1).to_string()),
2334                ("gu_rx", rx.to_string()),
2335                ("gu_dep", (depth - FOLD_STAGES).to_string()),
2336                ("gu_iss", (unit.points() * is.ii as u64).to_string()),
2337                ("gu_body", is.body.to_string()),
2338            ] {
2339                set(n, v);
2340            }
2341            for (j, v) in values.iter().enumerate() {
2342                set(&format!("gp{class}_{j}"), lit64(*v));
2343            }
2344            let vec = sh.lanes > 1;
2345            let lsh = unit.lanes.trailing_zeros();
2346            if vec {
2347                let lv = unit.lane_var();
2348                let lax = if lv == unit.row.var { 0 } else if lv == unit.col.var { 1 } else { 2 };
2349                set("gu_lsh", lsh.to_string());
2350                set("gu_lax", lax.to_string());
2351                let last = unit.points_along(lv) - 1;
2352                set("gu_lanes", unit.lanes.to_string());
2353                set("gu_llast", last.to_string());
2354                set("gu_ltail", (unit.lane_extent() - last * unit.lanes).to_string());
2355                set("gu_tl", unit.tree_levels().to_string());
2356                set("gu_tpool", code(unit.tree_pool()).to_string());
2357                set("gu_cdep", (depth - FOLD_STAGES - unit.tree_levels()).to_string());
2358                if unit.lanes > 1 && !unit.tree() {
2359                    set("gu_wsh", lsh.to_string());
2360                }
2361            }
2362            for (sl, m) in unit.maps.iter().enumerate() {
2363                let w = SlotWalk::of(ctx, unit, ins, sl);
2364                set(&format!("gu_base{sl}"), lit64(m.base));
2365                set(&format!("gu_d0{sl}"), lit64(w.d0));
2366                set(&format!("gu_d1{sl}"), lit64(w.d1));
2367                set(&format!("gu_d2{sl}"), lit64(w.d2));
2368                set(&format!("gu_lim{sl}"), lit64(w.lim));
2369                if let Some(ind) = m.indirect.filter(|_| unit.gathers(sl)) {
2370                    set(&format!("gu_gon{sl}"), "1'b1".into());
2371                    set(&format!("gu_gst{sl}"), lit64(ind.stride));
2372                    set(&format!("gu_gop{sl}"), ind.operand.to_string());
2373                }
2374                set(&format!("gu_at{sl}"), is.at[sl].to_string());
2375                set(&format!("gu_dl{sl}"), (is.body - is.at[sl] - lat as usize - 1).to_string());
2376                if vec && unit.broadcast(sl) {
2377                    set(&format!("gu_bc{sl}"), "1'b1".into());
2378                }
2379                // The slot reads its buffer's page through the port of its FIFO on its schedule, and takes the port's data
2380                // as it lands; of two that share a read, the one its point's select takes.
2381                let k = ctx.template(ins[sl]).idx();
2382                let role = axis_role(unit);
2383                // A shared read's select decides on its point's first coordinate: along the lane axis, the run's.
2384                let start_axis = |v: LoopVar| -> String {
2385                    let wire = match role(v) {
2386                        "r" => "gu_r",
2387                        "c" => "gu_c",
2388                        "x" => "gu_x",
2389                        _ => return "32'd0".into(),
2390                    };
2391                    if lsh > 0 && v == unit.lane_var() {
2392                        format!("({wire} << {lsh})")
2393                    } else {
2394                        wire.into()
2395                    }
2396                };
2397                let addr = match is.shares.iter().find(|sh| sh.then == sl || sh.otherwise == sl) {
2398                    Some(share) if share.then == sl => {
2399                        let PredRhs::Const(bound) = share.pred.rhs() else { unreachable!("a shared read's select has a constant bound") };
2400                        let cmp = match share.pred {
2401                            Pred::AxisLt(..) => "<",
2402                            Pred::AxisGe(..) => ">=",
2403                        };
2404                        c.push_str(&format!(" gu_pp{sl} = ({{32'd0, {}}} {cmp} 32'd{bound});", start_axis(share.pred.var())));
2405                        Some(format!("gi_pp{sl} ? gu_el{sl} : gu_el{}", share.otherwise))
2406                    }
2407                    Some(_) => None,
2408                    None => Some(format!("gu_el{sl}")),
2409                };
2410                if let Some(addr) = addr {
2411                    c.push_str(&format!(" if (gi_v{sl}) begin f{k}_rd_slot = f{k}_rd_slot0; f{k}_rd_elem = {addr}; end"));
2412                }
2413                // A gather of a vector op: lane `l` reads the element its own index names through its own port.
2414                let lane_gather = unit.lanes > 1 && unit.gathers(sl);
2415                if lane_gather {
2416                    for l in 1..unit.lanes {
2417                        c.push_str(&format!(" if (gi_v{sl}) begin f{k}_l{l}_rd_slot = f{k}_rd_slot0; f{k}_l{l}_rd_elem = gu_el{sl}_l{l}; end"));
2418                    }
2419                }
2420                if vec {
2421                    // Lane `l` takes element `l` of the run the port returns, or every lane the one element it reads, or a
2422                    // gather's lane the element its own port returns.
2423                    let w = ctx.buffer_width(ins[sl]);
2424                    for l in 0..sh.lanes {
2425                        let src = if lane_gather && l > 0 && l < unit.lanes {
2426                            format!("f{k}_l{l}_rd_data")
2427                        } else if l == 0 || unit.broadcast(sl) || l >= unit.lanes {
2428                            format!("f{k}_rd_data")
2429                        } else {
2430                            format!("$signed(f{k}_rd_hi[{}:{}])", l as u32 * w - 1, (l as u32 - 1) * w)
2431                        };
2432                        c.push_str(&format!(" gu_ld{sl}_l{l} = {src};"));
2433                    }
2434                } else {
2435                    c.push_str(&format!(" gu_ld{sl} = f{k}_rd_data;"));
2436                }
2437            }
2438        }
2439        Fixed::Matmul { block, per_site_n, sub_rows, ins, reload, .. } => {
2440            let (osh, osw, ibw) = (block.osh, block.osw, block.ibw);
2441            let (k, n) = (ibw * tc, osw * tc);
2442            let b_row = ctx.page_row(ins[1], *per_site_n);
2443            let run = ctx.matmul_run(block, b_row, *per_site_n, *sub_rows);
2444            let (rl, la, lb) = ctx.matmul_loads(block, reload.is_some(), run);
2445            let (rl, la, lb) = (rl as usize, la as usize, lb as usize);
2446            let (p0, p1, p2, p3) = (rl, rl + la, rl + la + lb, rl + la + lb + k);
2447            let rows = osh * ctx.tr;
2448            let (ka, kb) = (ctx.template(ins[0]).idx(), ctx.template(ins[1]).idx());
2449            // The sub-block this op multiplies: a loop's counter where a loop over sub-blocks encloses the op, else the
2450            // first — a counter an earlier loop left behind is not this op's.
2451            let sub = |v: LoopVar| if bound.contains(&v) { var_name(v) } else { "32'd0".to_string() };
2452            let (shv, swv) = (sub(LV_SUB_H), sub(LV_SUB_W));
2453            for (name, v) in [
2454                ("mu_on", "1'b1".to_string()),
2455                ("mu_mv", if ctx.matvec(block) { "1'b1" } else { "1'b0" }.to_string()),
2456                ("mu_p0", p0.to_string()),
2457                ("mu_p1", p1.to_string()),
2458                ("mu_p2", p2.to_string()),
2459                ("mu_p3", p3.to_string()),
2460                ("mu_rows", rows.to_string()),
2461                ("mu_kop", k.to_string()),
2462                ("mu_nop", n.to_string()),
2463                ("mu_osw", osw.to_string()),
2464                ("mu_ibw", ibw.to_string()),
2465                ("mu_bwrap", (osw * te).to_string()),
2466                ("mu_bjump", ((b_row as i64 - osw as i64) * te as i64 + 1).to_string()),
2467                ("aw_cols", cols.to_string()),
2468            ] {
2469                set(name, v);
2470            }
2471            if run > 1 {
2472                // Each load a row's runs a cycle: the reloaded partial, the A rows, the B rows of the site's columns.
2473                set("mr_on", "1'b1".into());
2474                set("mr_lw", run.trailing_zeros().to_string());
2475                set("mr_n0o", (osw * te).min(run).to_string());
2476                set("mr_n0i", (ibw * te).min(run).to_string());
2477                set("mu_pn", b_row.to_string());
2478                let lanes = |k: usize, b: BufferId, into: &str| -> String {
2479                    let w = ctx.buffer_width(b);
2480                    (0..ctx.banks)
2481                        .map(|j| {
2482                            let src = if j == 0 { format!("f{k}_rd_data") } else { format!("f{k}_rd_hi[{}:{}]", j as u32 * w - 1, (j as u32 - 1) * w) };
2483                            format!(" {into}{j} = $signed({src});")
2484                        })
2485                        .collect()
2486                };
2487                if let Some(r) = reload {
2488                    let kr = ctx.template(*r).idx();
2489                    c.push_str(&format!(" if ({}) begin f{kr}_rd_slot = f{kr}_rd_slot0; f{kr}_rd_elem = mr_pos; end{}", ctx.ph_lt(p0 as u64), lanes(kr, *r, "mu_rl")));
2490                }
2491                c.push_str(&format!(
2492                    " if ({} && {}) begin f{ka}_rd_slot = f{ka}_rd_slot0; f{ka}_rd_elem = {shv} * {} + mr_pos; end{}",
2493                    ctx.ph_ge(p0 as u64),
2494                    ctx.ph_lt(p1 as u64),
2495                    osh * ibw,
2496                    lanes(ka, ins[0], "mu_al")
2497                ));
2498                c.push_str(&format!(
2499                    " if ({} && {}) begin f{kb}_rd_slot = f{kb}_rd_slot0; f{kb}_rd_elem = {swv} * {osw} + mr_pos; end{}",
2500                    ctx.ph_ge(p1 as u64),
2501                    ctx.ph_lt(p2 as u64),
2502                    lanes(kb, ins[1], "mu_bl")
2503                ));
2504                return Ok(());
2505            }
2506            if let Some(r) = reload {
2507                let kr = ctx.template(*r).idx();
2508                c.push_str(&format!(" if ({}) begin f{kr}_rd_slot = f{kr}_rd_slot0; f{kr}_rd_elem = ph; end mu_rr = f{kr}_rd_data;", ctx.ph_lt(p0 as u64)));
2509            }
2510            // A page tiles (sh·osh + i, kk), whole and in order; B page tiles (kk, sw·osw + j) of the site's columns.
2511            c.push_str(&format!(
2512                " if ({} && {}) begin f{ka}_rd_slot = f{ka}_rd_slot0; f{ka}_rd_elem = {shv} * {} + (ph - {p0}); end mu_ra = f{ka}_rd_data;",
2513                ctx.ph_ge(p0 as u64),
2514                ctx.ph_lt(p1 as u64),
2515                osh * ibw * te
2516            ));
2517            c.push_str(&format!(
2518                " if ({} && {}) begin f{kb}_rd_slot = f{kb}_rd_slot0; f{kb}_rd_elem = {swv} * {} + mbi_off; end mu_rb = f{kb}_rd_data;",
2519                ctx.ph_ge(p1 as u64),
2520                ctx.ph_lt(p2 as u64),
2521                osw * te
2522            ));
2523        }
2524    }
2525    Ok(())
2526}
2527
2528/// A tile walk's offset deltas: within a tile row of `te` elements one, then `d1` to the next tile of the row, `d2` to
2529/// the first tile of the next block row.
2530fn walk_deltas(te: usize, block_w: usize, stride_w: usize, stride_h: usize) -> (i64, i64) {
2531    let (te, bw, sw, shh) = (te as i64, block_w as i64, stride_w as i64, stride_h as i64);
2532    (sw * te - (te - 1), shh * te - (bw - 1) * sw * te - (te - 1))
2533}
2534
2535/// The units' registered logic.
2536fn units(ctx: &Ctx, sh: &Shape, wd: &Widths, te: usize, tr: usize, tc: usize, lat: u64) -> String {
2537    let mut e = Emit { s: String::new() };
2538    let lat32 = format!("32'd{lat}");
2539    // Tile walk.
2540    e.l("  always @(posedge clk)");
2541    e.l("    if (rst || !(running && tw_on) || op_last) begin tw_lo <= 0; tw_w <= 0; tw_off <= 0; end");
2542    e.l(format!(
2543        "    else if (ph >= tw_from) begin if (tw_lo != {}) begin tw_lo <= tw_lo + 1; tw_off <= tw_off + 1; end else begin tw_lo <= 0; if (tw_w != tw_bw - 1) begin tw_w <= tw_w + 1; tw_off <= tw_off + tw_d1; end else begin tw_w <= 0; tw_off <= tw_off + tw_d2; end end end",
2544        te - 1
2545    ));
2546    // Run walk: a run of a contiguous row a cycle, `2^tw_lw` elements wide — its offset in the tensor, its place in the
2547    // page, its length — and the same `lat` cycles on, where its data lands.
2548    if ctx.banks > 1 {
2549        let lg = log2_banks(ctx.banks);
2550        // A run's offset in the tensor and its length are registers, set a cycle ahead from where the walk goes next; at
2551        // the walk's start they are its first run's (offset 0, length `tw_n0`), so no addition stands between the walk
2552        // and the memory it addresses.
2553        e.l("  reg [AW-1:0] tw_c, tw_rb, tw_p, tw_vo_r; reg tw_first;");
2554        e.l(format!("  reg [{lg}:0] tw_n_r;"));
2555        e.l("  wire [AW-1:0] tw_l = 32'd1 << tw_lw;");
2556        e.l(format!("  wire [{lg}:0] tw_n = tw_first ? tw_n0 : tw_n_r;"));
2557        e.l("  wire [AW-1:0] tw_vo = tw_first ? 32'd0 : tw_vo_r;");
2558        e.l("  wire tw_adv = (tw_c + tw_l < tw_bw);");
2559        e.l("  wire [AW-1:0] tw_cn = tw_adv ? tw_c + tw_l : 32'd0, tw_rbn = tw_adv ? tw_rb : tw_rb + tw_rs, tw_leftn = tw_bw - tw_cn;");
2560        e.l(format!("  wire [{lg}:0] tw_nn = (tw_leftn < tw_l) ? tw_leftn[{lg}:0] : tw_l[{lg}:0];"));
2561        e.l("  always @(posedge clk)");
2562        e.l("    if (rst || !(running && tw_on && tw_vec) || op_last) begin tw_c <= 0; tw_rb <= 0; tw_p <= 0; tw_first <= 1'b1; end");
2563        let step = if sh.pitched_fill { "tw_p + tw_n + (tw_adv ? 32'd0 : tw_pp)" } else { "tw_p + tw_n" };
2564        e.l(format!("    else if (ph >= tw_from) begin tw_first <= 1'b0; tw_p <= {step}; tw_c <= tw_cn; tw_rb <= tw_rbn; tw_vo_r <= tw_rbn + tw_cn; tw_n_r <= tw_nn; end"));
2565        if lat == 0 {
2566            e.l(format!("  wire [AW-1:0] tw_vod = tw_vo, tw_pd = tw_p; wire [{lg}:0] tw_nd = tw_n;"));
2567        } else {
2568            for d in 1..=lat {
2569                let (vo, p, n) = if d == 1 { ("tw_vo".to_string(), "tw_p".to_string(), "tw_n".to_string()) } else { (format!("tw_vo{}", d - 1), format!("tw_p{}", d - 1), format!("tw_n{}", d - 1)) };
2570                e.l(format!(
2571                    "  reg [AW-1:0] tw_vo{d}, tw_p{d}; reg [{lg}:0] tw_n{d}; always @(posedge clk) begin tw_vo{d} <= {vo}; tw_p{d} <= {p}; tw_n{d} <= {n}; end"
2572                ));
2573            }
2574            e.l(format!("  wire [AW-1:0] tw_vod = tw_vo{lat}, tw_pd = tw_p{lat}; wire [{lg}:0] tw_nd = tw_n{lat};"));
2575        }
2576    }
2577    // Accumulator walk.
2578    e.l("  always @(posedge clk)");
2579    e.l("    if (rst || !running || op_last) begin aw_cc <= 0; aw_r <= 0; aw_j <= 0; aw_i <= 0; end");
2580    e.l(format!(
2581        "    else if (aw_step || mu_reload) begin if (aw_cc != {}) aw_cc <= aw_cc + 1; else begin aw_cc <= 0; if (aw_r != {}) aw_r <= aw_r + 1; else begin aw_r <= 0; if (aw_j != aw_cols - 1) aw_j <= aw_j + 1; else begin aw_j <= 0; aw_i <= aw_i + 1; end end end end",
2582        tc - 1,
2583        tr - 1
2584    ));
2585    // Unary walks: the input element wraps at its page, the bias walks one tile's elements per tile row.
2586    e.l("  always @(posedge clk)");
2587    e.l("    if (rst || !(running && un_on) || op_last) begin un_e <= 0; un_e2 <= 0; un_bk <= 0; un_bt <= 0; end");
2588    e.l(format!(
2589        "    else begin un_e <= (un_e == un_pe - 1) ? 0 : un_e + 1; un_e2 <= (un_e2 == un_pb - 1) ? 0 : un_e2 + 1; if (un_bk == {}) begin un_bk <= 0; un_bt <= (un_bt + {te} >= un_pb) ? un_bt + {te} - un_pb : un_bt + {te}; end else un_bk <= un_bk + 1; end",
2590        te - 1
2591    ));
2592    // Unary datapath: one multiplier for a requant or a product, taken at its node's width.
2593    let um = wd.un_mul.max(wd.un_out);
2594    e.l(format!(
2595        "  wire signed [{0}:0] un_y = (un_kind == 4'd5) ? $signed(unary_bias) : un_m; wire signed [{1}:0] un_mul = {2} * un_y;",
2596        wd.un_b.max(wd.un_m).min(um) - 1,
2597        um - 1,
2598        fit("unary_in", wd.un_in, um)
2599    ));
2600    e.l("  always @* case (un_kind)");
2601    e.l("    4'd0: unary_out = un_mul >>> un_s;");
2602    e.l("    4'd1: unary_out = $signed(unary_in) + $signed(unary_bias);");
2603    e.l("    4'd2: unary_out = ($signed(unary_in) < 0) ? 0 : unary_in;");
2604    e.l("    4'd4: unary_out = $signed(unary_in) + $signed(unary_bias);");
2605    e.l("    4'd5: unary_out = un_mul;");
2606    e.l("    4'd6: unary_out = grid_result;");
2607    e.l("    default: unary_out = 0;");
2608    e.l("  endcase");
2609    // Multicast walk.
2610    e.l("  always @(posedge clk)");
2611    e.l("    if (rst || !(running && mc_on) || op_last) begin mc_e <= 0; mc_pg <= 0; end");
2612    e.l("    else if (mc_e == mc_pe - 1) begin mc_e <= 0; mc_pg <= mc_pg + 1; end else mc_e <= mc_e + 1;");
2613
2614    // Matmul unit, on the timeline its reads land on.
2615    if let Some((k, n, bits)) = sh.array {
2616        let l = sh.array.map(|(a, b, _)| pipelined_latency(a, b)).unwrap_or(0);
2617        e.l(format!("  wire mu_live = mu_on && {};", ctx.ph_ge(lat)));
2618        e.l(format!("  wire [AW-1:0] mu_q = ph - {lat32};"));
2619        // B issue walk: the page offset, one past each element, `bjump` past each row of the site's tiles.
2620        e.l("  always @(posedge clk)");
2621        e.l("    if (rst || !(running && mu_on) || op_last) begin mbi_t <= 0; mbi_off <= 0; end");
2622        e.l("    else if (ph >= mu_p1 && ph < mu_p2) begin if (mbi_t == mu_bwrap - 1) begin mbi_t <= 0; mbi_off <= mbi_off + mu_bjump; end else begin mbi_t <= mbi_t + 1; mbi_off <= mbi_off + 1; end end");
2623        // A and B consume walks: each element's row and lane in the operand rows.
2624        e.l("  always @(posedge clk)");
2625        e.l("    if (rst || !(running && mu_on) || op_last) begin ma_xx <= 0; ma_r <= 0; ma_kk <= 0; ma_i <= 0; mb_cc <= 0; mb_x <= 0; mb_j <= 0; mb_kk <= 0; end");
2626        e.l("    else if (mu_live) begin");
2627        e.l(format!(
2628            "      if (mu_q >= mu_p0 && mu_q < mu_p1) begin if (ma_xx != {}) ma_xx <= ma_xx + 1; else begin ma_xx <= 0; if (ma_r != {}) ma_r <= ma_r + 1; else begin ma_r <= 0; if (ma_kk != mu_ibw - 1) ma_kk <= ma_kk + 1; else begin ma_kk <= 0; ma_i <= ma_i + 1; end end end end",
2629            tc - 1,
2630            tr - 1
2631        ));
2632        e.l(format!(
2633            "      if (mu_q >= mu_p1 && mu_q < mu_p2) begin if (mb_cc != {}) mb_cc <= mb_cc + 1; else begin mb_cc <= 0; if (mb_x != {}) mb_x <= mb_x + 1; else begin mb_x <= 0; if (mb_j != mu_osw - 1) mb_j <= mb_j + 1; else begin mb_j <= 0; mb_kk <= mb_kk + 1; end end end end",
2634            tc - 1,
2635            tr - 1
2636        ));
2637        e.l("    end");
2638        e.l(format!("  wire [AW-1:0] ma_s = (ma_i << {}) + ma_r;", log2(tr)));
2639        e.l(format!("  wire [AW-1:0] ma_x = (ma_kk << {}) + ma_xx;", log2(tc)));
2640        e.l(format!("  wire [AW-1:0] mb_r = (mb_kk << {}) + mb_x;", log2(tc)));
2641        e.l(format!("  wire [AW-1:0] mb_c = (mb_j << {}) + mb_cc;", log2(tc)));
2642        // Operand rows, an element a cycle. A load by runs lands its own way (below): each operand register has one writer
2643        // block, which takes whichever landing the op loads by.
2644        let mut a_elem = String::new();
2645        for s in 0..sh.a_rows {
2646            for x in 0..k {
2647                a_elem.push_str(&format!("      if (ma_s == {s} && ma_x == {x}) a_r{s}_{x} <= mu_ra[{}:0];\n", bits - 1));
2648            }
2649        }
2650        let mut b_elem = String::new();
2651        for r in 0..k {
2652            for c in 0..n {
2653                b_elem.push_str(&format!(
2654                    "      if (mb_r == {r} && mb_c == {c}) b_r{r}[{}:{}] <= mu_rb[{}:0];\n",
2655                    (c as u32 + 1) * bits - 1,
2656                    c as u32 * bits,
2657                    bits - 1
2658                ));
2659            }
2660        }
2661        if !ctx.runs() {
2662            e.l("  always @(posedge clk) if (mu_live && mu_q >= mu_p0 && mu_q < mu_p1) begin");
2663            e.s.push_str(&a_elem);
2664            e.l("  end");
2665            e.l("  always @(posedge clk) if (mu_live && mu_q >= mu_p1 && mu_q < mu_p2) begin");
2666            e.s.push_str(&b_elem);
2667            e.l("  end");
2668        }
2669        // The columns past this op's are loaded with zero weights.
2670        let mut mask = format!("  wire [{}:0] wmask = {{", n as u32 * bits - 1);
2671        let parts: Vec<String> =
2672            (0..n).rev().map(|c| format!("(({c} < mu_nop) ? {{{bits}{{1'b1}}}} : {{{bits}{{1'b0}}}})")).collect();
2673        mask.push_str(&parts.join(", "));
2674        mask.push_str("};");
2675        e.l(mask);
2676        // Weight load, row stream, and the output rows the array returns `l` cycles on.
2677        e.l("  wire [AW-1:0] mu_w = mu_q - mu_p2;");
2678        e.l("  wire [AW-1:0] mu_s = mu_q - mu_p3;");
2679        e.l(format!("  wire [AW-1:0] mu_o = mu_q - mu_p3 - {};", l + 1));
2680        e.l("  always @(posedge clk) begin");
2681        e.l("    pe_w_load_r <= 1'b0;");
2682        e.l("    if (mu_live && !mu_mv && mu_q >= mu_p2 && mu_q < mu_p3) begin");
2683        e.l("      pe_w_load_r <= 1'b1; pe_w_row <= mu_w;");
2684        let mut wr = String::from("      case (mu_w)");
2685        for r in 0..k {
2686            wr.push_str(&format!(" {r}: pe_w_data <= b_r{r} & wmask;"));
2687        }
2688        wr.push_str(" default: pe_w_data <= 0; endcase");
2689        e.l(wr);
2690        e.l("    end");
2691        e.l("    if (mu_live && mu_q >= mu_p3) begin");
2692        for x in 0..k {
2693            let mut a = format!("      pe_a{x} <= (mu_s < mu_rows && {x} < mu_kop) ? (");
2694            for s in 0..sh.a_rows {
2695                a.push_str(&format!("(mu_s == {s}) ? $signed(a_r{s}_{x}) : "));
2696            }
2697            a.push_str("64'sd0) : 64'sd0;");
2698            e.l(a);
2699        }
2700        e.l("    end");
2701        e.l("  end");
2702        // The array's output rows: row `s` lands `l + 1` cycles after it streamed, every column into its bank.
2703        e.l(format!("  wire mw_we = mu_live && (mu_mv ? (mu_q == mu_p2 + 1) : (mu_q >= mu_p3 + {0} && mu_q < mu_p3 + {0} + mu_rows));", l + 1));
2704        e.l("  wire [AW-1:0] mw_row = mu_mv ? 32'd0 : mu_o, mw_n = mu_nop;");
2705        // The reload phase writes the accumulator through its walk.
2706        e.l("  assign mu_reload = mu_live && !mr_on && mu_q < mu_p0;");
2707        if ctx.runs() {
2708            // Each load phase a row's runs a cycle: its row, its block of columns, its place in the page, its length; and
2709            // the same `lat` cycles on, where its data lands in the operand rows or the accumulator.
2710            let lg = log2_banks(ctx.banks);
2711            // The load phase, a run's place in its page and its length are registers, set a cycle ahead from where the
2712            // walk goes next; at each phase's start they are its first run's (place 0, length `mr_n0o` over a row of
2713            // `osw`, `mr_n0i` over a row of `ibw`), so no addition stands between the walk and the page it reads.
2714            e.l("  reg [AW-1:0] mr_row, mr_cb, mr_base, mr_pos_r, mr_rem_r; reg mr_first; reg [1:0] mr_ph_r;");
2715            e.l(format!("  reg [{lg}:0] mr_n_r;"));
2716            e.l("  wire [AW-1:0] mr_l = 32'd1 << mr_lw;");
2717            // An op's first cycle takes its phase from its own table; every later one from the register set the cycle
2718            // before, within the op.
2719            e.l("  always @(posedge clk) mr_ph_r <= (ph + 1 < mu_p0) ? 2'd0 : (ph + 1 < mu_p1) ? 2'd1 : 2'd2;");
2720            e.l(format!("  wire [1:0] mr_ph = ({}) ? ((mu_p0 != 0) ? 2'd0 : (mu_p1 != 0) ? 2'd1 : 2'd2) : mr_ph_r;", ctx.ph_lt(1)));
2721            e.l("  wire [AW-1:0] mr_w = (mr_ph == 2'd1) ? mu_ibw : mu_osw;");
2722            e.l("  wire [AW-1:0] mr_rs = (mr_ph == 2'd0) ? mu_osw : (mr_ph == 2'd1) ? mu_ibw : mu_pn;");
2723            e.l("  wire [AW-1:0] mr_pos = mr_first ? 32'd0 : mr_pos_r;");
2724            e.l(format!("  wire [{lg}:0] mr_n = mr_first ? ((mr_ph == 2'd1) ? mr_n0i : mr_n0o) : mr_n_r;"));
2725            // What is left of the row from this run's start is a register, the phase's whole row at its first cycle, so
2726            // the walk advances on a compare of the run's width with it: another run starts at `(cb + 1)·l` exactly when
2727            // that is inside the row.
2728            e.l("  wire [AW-1:0] mr_rem = mr_first ? mr_w : mr_rem_r;");
2729            e.l("  wire mr_adv = (mr_l < mr_rem);");
2730            e.l("  wire [AW-1:0] mr_cbn = mr_adv ? mr_cb + 1 : 32'd0, mr_basen = mr_adv ? mr_base : mr_base + mr_rs;");
2731            e.l("  wire [AW-1:0] mr_leftn = mr_adv ? mr_rem - mr_l : mr_w;");
2732            e.l(format!("  wire [{lg}:0] mr_nn = (mr_leftn < mr_l) ? mr_leftn[{lg}:0] : mr_l[{lg}:0];"));
2733            e.l("  always @(posedge clk)");
2734            e.l("    if (rst || !(running && mu_on && mr_on) || op_last || ph + 1 == mu_p0 || ph + 1 == mu_p1) begin mr_row <= 0; mr_cb <= 0; mr_base <= 0; mr_first <= 1'b1; end");
2735            e.l("    else begin mr_first <= 1'b0; mr_cb <= mr_cbn; mr_base <= mr_basen; if (!mr_adv) mr_row <= mr_row + 1; mr_pos_r <= mr_basen + (mr_cbn << mr_lw); mr_n_r <= mr_nn; mr_rem_r <= mr_leftn; end");
2736            if lat == 0 {
2737                e.l(format!("  wire [AW-1:0] mr_rowd = mr_row, mr_cbd = mr_cb; wire [{lg}:0] mr_nd = mr_n;"));
2738            } else {
2739                for d in 1..=lat {
2740                    let src = |x: &str| if d == 1 { x.to_string() } else { format!("{x}{}", d - 1) };
2741                    e.l(format!(
2742                        "  reg [AW-1:0] mr_row{d}, mr_cb{d}; reg [{lg}:0] mr_n{d}; always @(posedge clk) begin mr_row{d} <= {}; mr_cb{d} <= {}; mr_n{d} <= {}; end",
2743                        src("mr_row"),
2744                        src("mr_cb"),
2745                        src("mr_n")
2746                    ));
2747                }
2748                e.l(format!("  wire [AW-1:0] mr_rowd = mr_row{lat}, mr_cbd = mr_cb{lat}; wire [{lg}:0] mr_nd = mr_n{lat};"));
2749            }
2750            // Lane `j` of a landing run of `2^mr_lw` fills the operand at column `mr_cbd·2^mr_lw + j` of its row: column `x`
2751            // takes lane `x mod 2^mr_lw` of run `x >> mr_lw`.
2752            let landing = |x: usize, dst: &str, lane: &str| -> String {
2753                let mut r = String::from("case (mr_lw)");
2754                for w in 1..=lg {
2755                    let j = x & ((1 << w) - 1);
2756                    r.push_str(&format!(" {w}: if (mr_cbd == {} && {j} < mr_nd) {dst} <= {lane}{j}[{}:0];", x >> w, bits - 1));
2757                }
2758                r.push_str(" default: ; endcase");
2759                r
2760            };
2761            e.l("  always @(posedge clk) if (mu_live && mu_q >= mu_p0 && mu_q < mu_p1) begin");
2762            e.l("    if (mr_on) begin");
2763            for s in 0..sh.a_rows {
2764                for x in 0..k {
2765                    e.l(format!("      if (mr_rowd == {s}) {}", landing(x, &format!("a_r{s}_{x}"), "mu_al")));
2766                }
2767            }
2768            e.l("    end else begin");
2769            e.s.push_str(&a_elem);
2770            e.l("    end");
2771            e.l("  end");
2772            e.l("  always @(posedge clk) if (mu_live && mu_q >= mu_p1 && mu_q < mu_p2) begin");
2773            e.l("    if (mr_on) begin");
2774            for r in 0..k {
2775                for c in 0..n {
2776                    e.l(format!("      if (mr_rowd == {r}) {}", landing(c, &format!("b_r{r}[{}:{}]", (c as u32 + 1) * bits - 1, c as u32 * bits), "mu_bl")));
2777                }
2778            }
2779            e.l("    end else begin");
2780            e.s.push_str(&b_elem);
2781            e.l("    end");
2782            e.l("  end");
2783            e.l("  wire mr_reload = mu_live && mr_on && mu_q < mu_p0;");
2784        }
2785        // The matrix-vector product of a one-row matmul ([`Ctx::matvec`]): column `c` multiplies each B element of its
2786        // column by the A element of the B element's row as it lands, and sums the products a cycle later. A run lands
2787        // the column's element in lane `c mod 2^mr_lw` of run `c >> mr_lw`; an element lands at its own column.
2788        let land_b = "mu_live && mu_q >= mu_p1 && mu_q < mu_p2";
2789        let kidx = if ctx.runs() { "(mr_on ? mr_rowd : mb_r)" } else { "mb_r" };
2790        let mut ak = format!("  reg signed [{}:0] mv_a; always @* case ({kidx})", bits - 1);
2791        for x in 0..k {
2792            ak.push_str(&format!(" {x}: mv_a = $signed(a_r0_{x});"));
2793        }
2794        ak.push_str(" default: mv_a = 0; endcase");
2795        e.l(ak);
2796        let lgb = log2_banks(ctx.banks) as usize;
2797        for c in 0..n {
2798            let element = format!("(!mr_on && mb_c == {c}) ? {{1'b1, mu_rb[{}:0]}} : ", bits - 1);
2799            let runs: String = if ctx.runs() {
2800                (1..=lgb)
2801                    .map(|w| {
2802                        let j = c & ((1 << w) - 1);
2803                        format!("(mr_on && mr_lw == {w} && mr_cbd == {} && {j} < mr_nd) ? {{1'b1, mu_bl{j}[{}:0]}} : ", c >> w, bits - 1)
2804                    })
2805                    .collect()
2806            } else {
2807                String::new()
2808            };
2809            e.l(format!("  wire [{bits}:0] mv_b{c} = {runs}{element}{}'d0;", bits + 1));
2810            // The product is as wide as its operands' product — a multiplication takes the width of what it is assigned
2811            // to, and the accumulator's would make it that wide a multiplier.
2812            e.l(format!(
2813                "  wire signed [{2}:0] mv_m{c} = mv_a * $signed(mv_b{c}[{1}:0]); reg mv_q{c}; reg signed [{0}:0] mv_p{c}, mv{c}; always @(posedge clk) begin mv_q{c} <= mu_mv && {land_b} && mv_b{c}[{bits}]; mv_p{c} <= mv_m{c}; end",
2814                wd.acc - 1,
2815                bits - 1,
2816                2 * bits - 1
2817            ));
2818            e.l(format!("  always @(posedge clk) if (rst || !(running && mu_on) || op_last) mv{c} <= 0; else if (mv_q{c}) mv{c} <= mv{c} + mv_p{c};"));
2819        }
2820    } else {
2821        e.l("  assign mu_reload = 1'b0;");
2822    }
2823
2824    // Generic unit: the axes and slot walks through the issue phases, a point every interval, and — where a point's
2825    // value leaves its class's pipeline — the contraction, the fold and its write.
2826    if sh.slots > 0 {
2827        let g = sh.slots;
2828        e.l("  always @(posedge clk)");
2829        e.l("    if (rst || !(running && gu_on) || op_last) begin gu_sub <= 0; gu_x <= 0; gu_mid <= 0; gu_out <= 0;");
2830        for s in 0..g {
2831            e.l(format!("      gu_lin{s} <= 0;"));
2832        }
2833        e.l("    end else if (ph < gu_iss) begin");
2834        e.l("      if (gu_sub != gu_ii - 1) gu_sub <= gu_sub + 1;");
2835        e.l("      else begin gu_sub <= 0;");
2836        e.l("        if (gu_x != gu_kx - 1) begin gu_x <= gu_x + 1;");
2837        for s in 0..g {
2838            e.l(format!("          gu_lin{s} <= gu_lin{s} + gu_d0{s};"));
2839        }
2840        e.l("        end else begin gu_x <= 0;");
2841        e.l("          if (gu_mid != gu_nmid - 1) begin gu_mid <= gu_mid + 1;");
2842        for s in 0..g {
2843            e.l(format!("            gu_lin{s} <= gu_lin{s} + gu_d1{s};"));
2844        }
2845        e.l("          end else begin gu_mid <= 0;");
2846        e.l("            if (gu_out != gu_nout - 1) begin gu_out <= gu_out + 1;");
2847        for s in 0..g {
2848            e.l(format!("              gu_lin{s} <= gu_lin{s} + gu_d2{s};"));
2849        }
2850        e.l("            end else begin gu_out <= 0;");
2851        for s in 0..g {
2852            e.l(format!("              gu_lin{s} <= 0;"));
2853        }
2854        e.l("            end");
2855        e.l("          end");
2856        e.l("        end");
2857        e.l("      end");
2858        e.l("    end");
2859        e.l("  always @(posedge clk) if (gq_v && gu_kx != 1) g_kacc <= gu_pv;");
2860        e.l("  always @(posedge clk) if (fb_v && gu_kind != 2'd0 && fb_pd) begin g_fold <= gu_fv; g_fidx <= gu_fi; end");
2861        e.l("  always @(posedge clk) if (fc_v && fc_ofold) begin g_ofold <= gu_ov; g_oidx <= gu_oi; end");
2862        let vl = sh.lanes;
2863        for l in 1..vl {
2864            if sh.lane_contracts {
2865                e.l(format!("  always @(posedge clk) if (gq_v && gu_kx != 1) g_kacc_l{l} <= gu_pv_l{l};"));
2866            }
2867            if sh.lane_pools.contains(&true) {
2868                e.l(format!("  always @(posedge clk) if (fb_v && gu_kind != 2'd0 && fb_pd) begin g_fold_l{l} <= gu_fv_l{l}; g_fidx_l{l} <= gu_fi_l{l}; end"));
2869            }
2870        }
2871        // The fold's write, at stage C: a parallel point at its column, a reduced column at column 0, a reduced row at
2872        // its column, a full reduction at column 0 once its last row is folded. An op whose lanes each keep their column
2873        // writes a run of columns from its point's (`gu_wsh` its shift), each lane that is the op's its own.
2874        if vl > 1 {
2875            e.l("  wire gu_we = fc_v && fc_we && (gu_wsh == 4'd0);");
2876            e.l("  wire gv_we = fc_v && fc_we && (gu_wsh != 4'd0);");
2877        } else {
2878            e.l("  wire gu_we = fc_v && fc_we;");
2879        }
2880        e.l("  wire [31:0] gu_wcol = fc_wcol;");
2881        e.l(format!("  wire signed [{}:0] gu_wd = (gu_kind == 2'd0) ? fc_pv : (gu_kind == 2'd3) ? gu_or : fc_ir;", wd.value - 1));
2882        if vl > 1 {
2883            // ★ A RUN ROTATES INTO FIXED SLOTS. A run of a parallel op's lanes starts at column `fc_wcol << gu_wsh`, a
2884            // multiple of its lanes: its lane `j` writes column `first + j`. Slot `s` of the vector unit's output holds
2885            // the lane bound for the columns `≡ s mod vl` — lane `(s − first) mod vl` — so bank `c` takes slot `c mod vl`
2886            // alone, and only where the run's word of `vl` columns is the bank's and the slot's lane is one of the op's.
2887            let lg = vl.trailing_zeros();
2888            let mvw = wd.value - 1;
2889            let pick = |pv: &str, ir: &str| -> String {
2890                match (sh.lane_writes_points, sh.lane_writes_folds) {
2891                    (true, true) => format!("((gu_kind == 2'd0) ? {pv} : {ir})"),
2892                    (false, true) => ir.to_string(),
2893                    _ => pv.to_string(),
2894                }
2895            };
2896            for j in 0..vl {
2897                let (pv, ir) = if j == 0 { ("fc_pv".to_string(), "fc_ir".to_string()) } else { (format!("fc_pv_l{j}"), format!("fc_ir_l{j}")) };
2898                e.l(format!("  wire signed [{mvw}:0] gw_lane{j} = {};", pick(&pv, &ir)));
2899            }
2900            let ax = sh.axis_bits;
2901            e.l(format!("  wire [{}:0] gw_first = {{4'd0, fc_wcol}} << gu_wsh;", ax + 3));
2902            e.l(format!("  wire [{}:0] gw_rot = gw_first[{}:0];", lg - 1, lg - 1));
2903            for sl in 0..vl {
2904                e.l(format!("  wire [{}:0] gw_li{sl} = {lg}'d{sl} - gw_rot;", lg - 1));
2905                let mut m = format!("  reg signed [{mvw}:0] gw_slot{sl}; always @* case (gw_li{sl})");
2906                for j in 0..vl {
2907                    m.push_str(&format!(" {j}: gw_slot{sl} = gw_lane{j};"));
2908                }
2909                m.push_str(&format!(" default: gw_slot{sl} = gw_lane0; endcase"));
2910                e.l(m);
2911                e.l(format!("  wire gw_sv{sl} = fc_lv[gw_li{sl}];"));
2912            }
2913            for c in 0..sh.banks {
2914                e.l(format!(
2915                    "  wire gw_e{c} = gv_we && (gw_first[{}:{lg}] == {}) && gw_sv{}; wire signed [{mvw}:0] gw_d{c} = gw_slot{};",
2916                    ax + 3,
2917                    c >> lg,
2918                    c % vl,
2919                    c % vl
2920                ));
2921            }
2922        }
2923    } else {
2924        e.l("  wire gu_we = 1'b0; wire [31:0] gu_wcol = 32'd0; wire signed gu_wd = 1'b0;");
2925    }
2926
2927    // Accumulator banks: the matmul's whole row, else the single write — the walk's element, a reload, a fold.
2928    e.l("  wire acc_we = sw_we || mu_reload || gu_we;");
2929    e.l("  wire [31:0] acc_row = gu_we ? 32'd0 : aw_row;");
2930    e.l("  wire [31:0] acc_col = gu_we ? gu_wcol : aw_col;");
2931    e.l(format!("  wire signed [{}:0] acc_d = gu_we ? gu_wd : mu_reload ? $signed(mu_rr) : sw_d;", wd.acc - 1));
2932    e.l("  always @(posedge clk) begin");
2933    let pe_cols = sh.array.map_or(0, |a| a.1);
2934    // An acquire's run zeroes the row's banks it covers; a matmul's reload run writes them, lane by its residue.
2935    let reload_run = sh.array.is_some() && ctx.runs();
2936    let zero = |c: usize| {
2937        if ctx.runs() {
2938            // A reload run of `2^mr_lw` writes column `c` from lane `c mod 2^mr_lw` of run `c >> mr_lw`.
2939            let reload = if reload_run {
2940                let lands: Vec<String> = (1..=log2_banks(ctx.banks) as usize)
2941                    .map(|w| format!("(mr_lw == {w} && mr_cbd == {} && {} < mr_nd) ? 1'b1 : ", c >> w, c & ((1 << w) - 1)))
2942                    .collect();
2943                let lane: Vec<String> = (1..=log2_banks(ctx.banks) as usize)
2944                    .map(|w| format!("(mr_lw == {w}) ? mu_rl{} : ", c & ((1 << w) - 1)))
2945                    .collect();
2946                format!(
2947                    "if (mr_reload && ({}1'b0)) acc_b{c}[mr_rowd] <= {}mu_rl0; else ",
2948                    lands.concat(),
2949                    lane.concat()
2950                )
2951            } else {
2952                String::new()
2953            };
2954            format!("{reload}if (az_we && ar_z[{c}]) acc_b{c}[ar_i] <= 0; else ")
2955        } else {
2956            String::new()
2957        }
2958    };
2959    // A vector unit's lanes write their run of columns, each bank from its own lane.
2960    let lane_write = |c: usize| if sh.slots > 0 && sh.lanes > 1 { format!(" else if (gw_e{c}) acc_b{c}[0] <= gw_d{c};") } else { String::new() };
2961    for c in 0..sh.banks {
2962        if c < pe_cols {
2963            e.l(format!(
2964                "    if (mw_we && {c} < mw_n) acc_b{c}[mw_row] <= acc_b{c}[mw_row] + (mu_mv ? mv{c} : pe_o{c}); else {}if (acc_we && acc_col == {c}) acc_b{c}[acc_row] <= acc_d;{}",
2965                zero(c),
2966                lane_write(c)
2967            ));
2968        } else {
2969            e.l(format!("    {}if (acc_we && acc_col == {c}) acc_b{c}[acc_row] <= acc_d;{}", zero(c), lane_write(c)));
2970        }
2971    }
2972    e.l("  end");
2973    let _ = (ctx, tr);
2974    e.s
2975}