Skip to main content

logicaffeine_compile/spatial/lower/
fabric.rs

1//! Our fabric: the spatial IR as synthesizable Verilog.
2//!
3//! Every IR object is one RTL object, and every choreography op is a step of the engine that owns it: a site is a
4//! module holding its tensor memories, its page FIFOs, its semaphores and its engines; an engine is a module printed
5//! once per engine-program class and instantiated per site with the site's work as parameters; a multicast group is one
6//! bus at the top; a tile matmul is the proven loadable weight-stationary PE array shaped to the block, and a matmul of
7//! one row — a matrix-vector product — multiplies each operand element as it lands instead (`Ctx::matvec`). The
8//! discrete-event twin (`fabric_cycle_model`) walks the same micro-programs with the same durations and stall rules, so
9//! its count IS the RTL's count.
10//!
11//! A FIFO is its control — counts, reservations, the slot it reads — and its pages live in one of its site's pools: the
12//! buffers of one phase take pools 0, 1, 2, …, so a site has as many page memories as its busiest phase has buffers
13//! (`site_pools`).
14//!
15//! A site's memories interleave their elements over [`SpatialProgram::mem_banks`] banks, and a page starts on a word. A
16//! run of `w` elements from a multiple of `w` — a tile row, a page's span, an accumulator row's columns — moves in one
17//! cycle, each bank at the one row every bank of the port shares. A walk moves the widest run every row it starts is
18//! aligned to in each memory it touches (`Ctx::run`), and each port carries only the run widths its ops move
19//! (`port_widths`), so no port rotates a lane past its own word.
20//!
21//! The Generic unit is a vector unit as wide as those runs: an op takes a run of one axis's coordinates a point, a lane a
22//! coordinate, where its slots read that axis as runs or as one element, and a slot that gathers reads, in each lane, the
23//! element its own index names through a read port of its own (`lanes_of`, `gather_lanes`). A reduced lane axis folds its
24//! lanes through a tree into the op's own fold; a parallel one keeps a fold per lane and writes a run of columns. A site
25//! of one bank prints the scalar unit, byte for byte.
26//!
27//! An op that carries data takes states of its own; an effect that only signals it — a push, a pop, a semaphore's
28//! increment — commits in that op's last cycle, and the op's last cycle also takes the loop bound after it and checks the
29//! guards it reaches, so the sequencer spends a cycle only on work or on a wait that is real. That last cycle is a
30//! register the op's duration counts down to, so nothing decodes the state in front of the units it resets.
31
32use std::collections::{BTreeMap, BTreeSet};
33
34pub mod budget;
35mod engine;
36
37pub use engine::{floor_divider_latency, floor_divider_verilog};
38
39use super::{require_verified, Census, SpatialArtifact, SpatialBackend, SpatialLowerError};
40use crate::codegen_sva::fabric_grid::{emit_grid_verilog, route_loop_body};
41use crate::codegen_sva::fabric_target::Precision;
42use crate::codegen_sva::handshake_rtl::{emit_mcast_bus_verilog, emit_semaphore_verilog};
43use crate::codegen_sva::systolic::{emit_systolic_pipelined_loadable_verilog_starting, pipelined_latency, WeightStart};
44use crate::spatial::eval::{Inputs, Value};
45use crate::spatial::ir::*;
46use crate::spatial::passes::{LV_ITEM, LV_ITEM_X, LV_ITEM_Y};
47use crate::spatial::target::{precision_int_bits, SpatialTarget};
48use crate::spatial::value_range::{program_ranges, signed_bits, ProgramRanges};
49use crate::tensor_recognize::ElementwiseOp;
50
51#[derive(Clone, Debug, PartialEq)]
52pub struct VerilogArtifact {
53    pub top: String,
54    pub rtl: String,
55    pub testbench: String,
56    pub hex: Vec<(String, String)>,
57    pub census: Census,
58    pub cycle_model: u64,
59    pub cycle_floor: u64,
60}
61
62pub struct FabricBackend {
63    pub target: SpatialTarget,
64}
65
66impl FabricBackend {
67    pub fn new(target: SpatialTarget) -> Self {
68        FabricBackend { target }
69    }
70}
71
72// ── The micro-program: one entry per FSM state, shared by the printer and the twin ────────────────────────
73
74/// A loop variable's role in an address: bound by an enclosing loop, or an item coordinate.
75#[derive(Clone, Debug, PartialEq, Eq)]
76enum Micro {
77    /// A one-cycle op whose precondition must hold (a bounded-buffer or semaphore wait).
78    Guarded(Guard),
79    /// An op of a fixed number of cycles, no precondition.
80    Fixed(Fixed),
81    /// An effect that takes no cycle of its own: it commits in the last cycle of the op before it — a push, a pop, a
82    /// semaphore's increment or set, a valid flag — or is nothing at all on the fabric (a barrier, whose reads and
83    /// writes the op before it has already landed; an accumulator's release). See [`fold_effects`].
84    Then(Fixed),
85    LoopStart {
86        var: LoopVar,
87        count: usize,
88        end: usize,
89    },
90    LoopEnd {
91        var: LoopVar,
92        count: usize,
93        start: usize,
94    },
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
98enum Guard {
99    Reserve { buf: BufferId, pages: usize },
100    Wait { buf: BufferId, pages: usize },
101    SemWait { sem: SemId, until: SemCond },
102    /// The halt at link `point` of a chip's program, the `ordinal`-th it makes: open once the host has continued the
103    /// fabric past it, which it does by its `ordinal + 1`-th continue of the step.
104    Link { point: usize, ordinal: usize },
105}
106
107#[derive(Clone, Debug, PartialEq, Eq)]
108enum Fixed {
109    Push {
110        buf: BufferId,
111        pages: usize,
112    },
113    Pop {
114        buf: BufferId,
115        pages: usize,
116    },
117    Barrier,
118    ReadTiles {
119        tensor: TensorId,
120        at: TileAddr,
121        block_h: usize,
122        block_w: usize,
123        stride_w: usize,
124        stride_h: usize,
125        into: BufferId,
126        /// The section the op belongs to: its item coordinates are that section's items.
127        section: Option<usize>,
128    },
129    WriteTiles {
130        from: BufferId,
131        tensor: TensorId,
132        at: TileAddr,
133        block_h: usize,
134        block_w: usize,
135        stride_w: usize,
136        stride_h: usize,
137        section: Option<usize>,
138    },
139    AcquireAcc {
140        tiles: usize,
141        /// The tiles a row of the region holds (set with the plan): a run never crosses a row's end.
142        cols: usize,
143    },
144    ReleaseAcc,
145    Matmul {
146        block: BlockShape,
147        per_site_n: usize,
148        /// The sub-blocks down the site's rows: where there is more than one, A's page holds each one's rows in turn.
149        sub_rows: usize,
150        operand: Precision,
151        ins: [BufferId; 2],
152        reload: Option<BufferId>,
153        tiles: usize,
154        /// The stage this op runs: what its values are proven to hold.
155        stage: StageId,
156        /// The engine's PE array `(rows, cols)` — the largest of its matmuls, which every one of them runs on: this
157        /// one loads its own rows and reads its own columns, and its latency is the physical array's.
158        array: (usize, usize),
159    },
160    Unary {
161        kind: UnaryKind,
162        ins: Vec<BufferId>,
163        tiles: usize,
164        stage: StageId,
165    },
166    /// The general form ([`ComputeOp::Generic`]) on the integer datapath: a point of the iteration space every
167    /// [`Issue::ii`] cycles, its slots read on their schedule and its body cycle entering the body's pipeline — and
168    /// `depth` cycles more for the last point's value to leave it.
169    Generic {
170        unit: Box<GenUnit>,
171        ins: Vec<BufferId>,
172        tiles: usize,
173        stage: StageId,
174        /// The cycles from a point's body cycle to its value: the pipeline of its body class on its engine, set with the
175        /// plan.
176        depth: u64,
177    },
178    PackAcc {
179        to: BufferId,
180        tiles: usize,
181        trunc32: bool,
182        accumulate: bool,
183        /// The tiles a row of the region holds (set with the plan).
184        cols: usize,
185    },
186    SemSet {
187        sem: SemId,
188        value: SemValue,
189    },
190    SemInc {
191        sem: SemId,
192        at: Site,
193        by: usize,
194    },
195    McastSetValid {
196        sem: SemId,
197        group: GroupId,
198    },
199    McastSend {
200        buf: BufferId,
201        pages: usize,
202        group: GroupId,
203    },
204}
205
206/// What a general Generic engine walks: its axes, one read per operand slot, the body, when each point's reads go out
207/// ([`Issue`]), and the lanes a point takes along its lane axis ([`lanes_of`]).
208#[derive(Clone, Debug, PartialEq, Eq)]
209struct GenUnit {
210    row: IterSpec,
211    col: IterSpec,
212    k: Option<IterSpec>,
213    maps: Vec<ElemAccess>,
214    body: TensorBody,
215    issue: Issue,
216    /// The consecutive coordinates of its lane axis a point takes: one, or a power of two no wider than the banks.
217    lanes: usize,
218    lane: LaneAxis,
219}
220
221/// The axis a Generic op's lanes run along: its contraction, the axis it walks inside the other (`Mid`), or the one it
222/// walks outside it (`Out`).
223#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
224enum LaneAxis {
225    K,
226    Mid,
227    Out,
228}
229
230/// A Generic op's modulo schedule: a point starts every `ii` cycles; slot `s` reads `at[s]` cycles after its point's
231/// start, through the read port of its buffer's FIFO; the point's body cycle is `body` cycles after its start, once every
232/// slot's value is latched.
233///
234/// Slots on one port read at distinct cycles modulo `ii`, so `ii` is the most slots any port serves: one point a cycle
235/// where every slot has a port of its own. A gather's slot reads once its index slot's value is latched, and before the
236/// next point's index value replaces it.
237#[derive(Clone, Debug, PartialEq, Eq)]
238struct Issue {
239    ii: usize,
240    at: Vec<usize>,
241    body: usize,
242    /// The pairs of slots that share a read: a point reads the one its select takes.
243    shares: Vec<Share>,
244}
245
246/// Two slots of one port that a select the point's own axes decide keeps apart: `then` is read only where `pred` holds
247/// and `otherwise` only where it does not, so a point reads one of them, in one cycle, and the value it takes is the
248/// only one of the two its body uses.
249#[derive(Clone, Debug, PartialEq, Eq)]
250struct Share {
251    then: usize,
252    otherwise: usize,
253    pred: Pred,
254}
255
256/// Each select of `body` whose bound is a constant — decided by the point's own axes before a slot is read — with the
257/// slots read only on its `then` side and those read only on its `otherwise` side.
258fn exclusive_sides(body: &TensorBody) -> Vec<(Pred, BTreeSet<usize>, BTreeSet<usize>)> {
259    fn uses(b: &TensorBody, out: &mut BTreeMap<usize, usize>) {
260        match b {
261            TensorBody::Operand(i) => *out.entry(*i).or_default() += 1,
262            TensorBody::Const(_) => {}
263            TensorBody::Add(x, y)
264            | TensorBody::Sub(x, y)
265            | TensorBody::Mul(x, y)
266            | TensorBody::Min(x, y)
267            | TensorBody::Max(x, y)
268            | TensorBody::FloorDiv(x, y) => {
269                uses(x, out);
270                uses(y, out);
271            }
272            TensorBody::Unary { arg, .. } | TensorBody::Shr { arg, .. } => uses(arg, out),
273            TensorBody::Select { cond, then, otherwise } => {
274                if let Some(i) = cond.operand() {
275                    *out.entry(i).or_default() += 1;
276                }
277                uses(then, out);
278                uses(otherwise, out);
279            }
280        }
281    }
282    fn walk(b: &TensorBody, total: &BTreeMap<usize, usize>, out: &mut Vec<(Pred, BTreeSet<usize>, BTreeSet<usize>)>) {
283        match b {
284            TensorBody::Operand(_) | TensorBody::Const(_) => {}
285            TensorBody::Add(x, y)
286            | TensorBody::Sub(x, y)
287            | TensorBody::Mul(x, y)
288            | TensorBody::Min(x, y)
289            | TensorBody::Max(x, y)
290            | TensorBody::FloorDiv(x, y) => {
291                walk(x, total, out);
292                walk(y, total, out);
293            }
294            TensorBody::Unary { arg, .. } | TensorBody::Shr { arg, .. } => walk(arg, total, out),
295            TensorBody::Select { cond, then, otherwise } => {
296                if cond.operand().is_none() {
297                    let only = |side: &TensorBody| -> BTreeSet<usize> {
298                        let mut u = BTreeMap::new();
299                        uses(side, &mut u);
300                        u.into_iter().filter(|(i, n)| total.get(i) == Some(n)).map(|(i, _)| i).collect()
301                    };
302                    out.push((*cond, only(then), only(otherwise)));
303                }
304                walk(then, total, out);
305                walk(otherwise, total, out);
306            }
307        }
308    }
309    let mut total = BTreeMap::new();
310    uses(body, &mut total);
311    let mut out = Vec::new();
312    walk(body, &total, &mut out);
313    out
314}
315
316impl Issue {
317    /// The schedule of slots read through `ports`, in `order` — every gather's index slot before the slot it indexes —
318    /// with `index_of` naming a gather's index slot, on memories that answer `lat` cycles after an address. Two slots of
319    /// one port on opposite sides of a select of `sides` share a read; neither a gather nor an index it adds is shared.
320    fn of(
321        ports: &[usize],
322        order: &[usize],
323        index_of: &dyn Fn(usize) -> Option<usize>,
324        lat: usize,
325        sides: &[(Pred, BTreeSet<usize>, BTreeSet<usize>)],
326    ) -> Issue {
327        let n = ports.len();
328        let indexes: BTreeSet<usize> = (0..n).filter_map(index_of).collect();
329        let free = |s: usize| index_of(s).is_none() && !indexes.contains(&s);
330        let mut partner: Vec<Option<usize>> = vec![None; n];
331        let mut shares = Vec::new();
332        for (pred, then, otherwise) in sides {
333            for &t in then {
334                for &o in otherwise {
335                    if t < n && o < n && ports[t] == ports[o] && free(t) && free(o) && partner[t].is_none() && partner[o].is_none() {
336                        partner[t] = Some(o);
337                        partner[o] = Some(t);
338                        shares.push(Share { then: t, otherwise: o, pred: *pred });
339                    }
340                }
341            }
342        }
343        let mut per_port: BTreeMap<usize, usize> = BTreeMap::new();
344        for s in 0..n {
345            if partner[s].is_none_or(|p| p > s) {
346                *per_port.entry(ports[s]).or_default() += 1;
347            }
348        }
349        let ii = per_port.values().copied().max().unwrap_or(1).max(1);
350        let mut at = vec![0usize; n];
351        let mut done = vec![false; n];
352        let mut used: BTreeMap<usize, BTreeSet<usize>> = BTreeMap::new();
353        for &s in order {
354            if let Some(p) = partner[s].filter(|p| done[*p]) {
355                at[s] = at[p];
356                done[s] = true;
357                continue;
358            }
359            // A gather reads once its index has latched, `lat + 1` after the index's read, and its product has been
360            // registered, one more.
361            let earliest = index_of(s).map_or(0, |ix| at[ix] + lat + 2);
362            let taken = used.entry(ports[s]).or_default();
363            // `ii` is the most reads any port takes, so each of a port's reads finds a residue of its own within `ii`
364            // cycles of its earliest.
365            let o = (earliest..earliest + ii)
366                .find(|o| !taken.contains(&(o % ii)))
367                .expect("a port takes no more reads than the issue interval has cycles");
368            taken.insert(o % ii);
369            at[s] = o;
370            done[s] = true;
371        }
372        let body = at.iter().map(|a| a + lat + 1).max().unwrap_or(1);
373        Issue { ii, at, body, shares }
374    }
375
376    /// Cycles of an op of `points` points: the last point's start, its body cycle, then its pipeline.
377    fn cycles(&self, points: u64, depth: u64) -> u64 {
378        points.saturating_sub(1) * self.ii as u64 + self.body as u64 + depth + 1
379    }
380}
381
382impl GenUnit {
383    fn k_extent(&self) -> usize {
384        self.k.map_or(1, |k| k.extent)
385    }
386    /// The points the op issues: each axis's extent, its lane axis's in runs of its lanes.
387    fn points(&self) -> u64 {
388        (self.points_along(self.row.var) * self.points_along(self.col.var) * self.k.map_or(1, |k| self.points_along(k.var))) as u64
389    }
390    /// The points the op issues along axis `v`: its extent, in runs of the lanes where it is the lane axis.
391    fn points_along(&self, v: LoopVar) -> usize {
392        let extent = if v == self.row.var {
393            self.row.extent
394        } else if v == self.col.var {
395            self.col.extent
396        } else {
397            self.k_extent()
398        };
399        if v == self.lane_var() {
400            extent.div_ceil(self.lanes)
401        } else {
402            extent
403        }
404    }
405    /// The loop variable of the lane axis.
406    fn lane_var(&self) -> LoopVar {
407        lane_var(self.lane, &self.row, &self.col, self.k.as_ref())
408    }
409    /// The lane axis's extent, in coordinates.
410    fn lane_extent(&self) -> usize {
411        match self.lane {
412            LaneAxis::K => self.k_extent(),
413            LaneAxis::Mid if self.rows_inner() => self.row.extent,
414            LaneAxis::Mid => self.col.extent,
415            LaneAxis::Out if self.rows_inner() => self.col.extent,
416            LaneAxis::Out => self.row.extent,
417        }
418    }
419    /// Whether the op's lanes fold into one value a point — a contraction, or the axis its fold walks inside — through
420    /// the lane tree, rather than each lane keeping its own.
421    fn tree(&self) -> bool {
422        self.lanes > 1 && (self.lane == LaneAxis::K || (self.lane == LaneAxis::Mid && self.lane_kind_reduces()))
423    }
424    /// Whether the lane tree sums — a contraction, or a sum's fold — rather than taking a maximum.
425    fn tree_sums(&self) -> bool {
426        self.tree_pool() == ReducePool::Sum
427    }
428    /// The pool the lane tree folds by: the contraction's sum, or the pool of the axis its lanes run along.
429    fn tree_pool(&self) -> ReducePool {
430        let v = self.lane_var();
431        let kind = if self.lane == LaneAxis::K { None } else if v == self.row.var { Some(self.row.kind) } else { Some(self.col.kind) };
432        match kind {
433            Some(IterKind::Reduce { pool }) => pool,
434            _ => ReducePool::Sum,
435        }
436    }
437    fn lane_kind_reduces(&self) -> bool {
438        let v = self.lane_var();
439        let kind = if v == self.row.var { self.row.kind } else if v == self.col.var { self.col.kind } else { IterKind::Reduce { pool: ReducePool::Sum } };
440        matches!(kind, IterKind::Reduce { .. })
441    }
442    /// The levels of the lane tree a point's value climbs: `log2` of its lanes where they fold, none where each lane keeps
443    /// its own.
444    fn tree_levels(&self) -> u64 {
445        if self.tree() {
446            self.lanes.trailing_zeros() as u64
447        } else {
448            0
449        }
450    }
451    /// Whether slot `s` reads one element a point, the same in every lane: its map does not move along the lane axis, and
452    /// it does not gather — a gather's lanes each read the element their own index names.
453    fn broadcast(&self, s: usize) -> bool {
454        if self.gathers(s) {
455            return false;
456        }
457        let v = self.lane_var();
458        self.maps[s].terms.iter().filter(|(t, _)| *t == v).map(|(_, st)| *st).sum::<i64>() == 0
459    }
460    fn slots(&self) -> usize {
461        self.maps.len()
462    }
463    /// Whether the row axis is walked inside the column axis: when only the row folds, so its fold sees its values
464    /// one after another. Otherwise the row is outermost.
465    fn rows_inner(&self) -> bool {
466        matches!(self.row.kind, IterKind::Reduce { .. }) && matches!(self.col.kind, IterKind::Parallel)
467    }
468    /// Whether slot `s`'s address adds the value another slot read — a gather. A slot indexing itself at stride 0 is a
469    /// range guard on a plain read.
470    fn gathers(&self, s: usize) -> bool {
471        gathers(&self.maps, s)
472    }
473}
474
475/// Whether slot `s` of `maps` gathers: its address adds the value another slot read.
476fn gathers(maps: &[ElemAccess], s: usize) -> bool {
477    index_slot(maps, s).is_some()
478}
479
480/// The slot whose value slot `s` of `maps` gathers by. A slot indexing itself at stride 0 is a range guard on a plain
481/// read: the evaluator checks the element it reads against the extent and moves nowhere, so it reads like any slot.
482fn index_slot(maps: &[ElemAccess], s: usize) -> Option<usize> {
483    maps[s].indirect.filter(|ind| !(ind.operand == s && ind.stride == 0)).map(|ind| ind.operand)
484}
485
486/// The order a Generic's operand slots are read in: every gather's index slot before the slot it indexes. An index
487/// slot that is itself gathered, or that indexes itself, has no such order.
488fn read_order(maps: &[ElemAccess]) -> Result<Vec<usize>, SpatialLowerError> {
489    let index_of = |s: usize| index_slot(maps, s);
490    for s in 0..maps.len() {
491        if let Some(ix) = index_of(s) {
492            if ix == s || ix >= maps.len() || index_of(ix).is_some() {
493                return Err(SpatialLowerError::Unsupported(
494                    "a gather whose index slot is itself gathered, or names itself or no slot",
495                ));
496            }
497        }
498    }
499    let mut order: Vec<usize> = (0..maps.len()).filter(|s| index_of(*s).is_none()).collect();
500    order.extend((0..maps.len()).filter(|s| index_of(*s).is_some()));
501    Ok(order)
502}
503
504/// The loop variable of lane axis `lane` over the space `row × col (× k)`: the contraction, or the axis walked inside the
505/// other — the row where only the row folds, else the column — or the one walked outside it.
506fn lane_var(lane: LaneAxis, row: &IterSpec, col: &IterSpec, k: Option<&IterSpec>) -> LoopVar {
507    let rows_inner = matches!(row.kind, IterKind::Reduce { .. }) && matches!(col.kind, IterKind::Parallel);
508    match lane {
509        LaneAxis::K => k.map_or(col.var, |k| k.var),
510        LaneAxis::Mid if rows_inner => row.var,
511        LaneAxis::Mid => col.var,
512        LaneAxis::Out if rows_inner => col.var,
513        LaneAxis::Out => row.var,
514    }
515}
516
517/// ★ A GENERIC POINT TAKES A RUN. A banked site moves a run of consecutive elements a cycle, so its Generic unit takes
518/// `L` consecutive coordinates of one axis a point — `L` a power of two no wider than the target's vector lanes
519/// (`widest`, itself no wider than the banks), each a lane of the body — where every slot's read of a point is one such
520/// run or one element:
521///
522/// - each slot's map moves along the lane axis by one (a run, a lane an element) or not at all (one element, the same in
523///   every lane); a slot that gathers moves along no lane axis, and each of its lanes reads the element its own lane of
524///   the index slot names, through a read port of the table's FIFO of its own ([`gather_lanes`]);
525/// - a run starts at a multiple of `L` in its page: the slot's base and its stride along every other axis are multiples of
526///   `L`, and so are the run's own steps;
527/// - the lane axis is the contraction or the axis walked inside the other, which a reduced axis folds through the lane
528///   tree; or a parallel column, which each lane keeps for itself, whichever axis it is walked as;
529/// - a select whose two sides share a read ([`Issue`]) keeps sharing only where its bound along the lane axis is a
530///   multiple of `L`, so every lane of a point takes the same side; otherwise both sides read;
531/// - no node of the body is a floor division: its divider already takes a pair a cycle, a stage per quotient bit, and a
532///   copy of it per lane is the costliest hardware the unit could repeat, for the few points a decode step divides.
533///
534/// Of the lane axes and widths an op allows, it takes the one that issues its points in the fewest cycles, its lane
535/// tree's levels counted; on a tie the fewest lanes, then the contraction before the inner axis before the outer.
536/// Returns the lanes, the lane axis and the issue schedule of that choice.
537#[allow(clippy::too_many_arguments)]
538fn lanes_of(
539    widest: usize,
540    row: &IterSpec,
541    col: &IterSpec,
542    k: Option<&IterSpec>,
543    maps: &[ElemAccess],
544    body: &TensorBody,
545    ports: &[usize],
546    order: &[usize],
547    lat: usize,
548) -> (usize, LaneAxis, Issue) {
549    let index_of = |s: usize| index_slot(maps, s);
550    let sides = exclusive_sides(body);
551    let scalar = Issue::of(ports, order, &index_of, lat, &sides);
552    let rows_inner = matches!(row.kind, IterKind::Reduce { .. }) && matches!(col.kind, IterKind::Parallel);
553    let extent = |v: LoopVar| -> usize {
554        if v == row.var {
555            row.extent
556        } else if v == col.var {
557            col.extent
558        } else {
559            k.map_or(1, |k| k.extent)
560        }
561    };
562    let kind = |v: LoopVar| -> IterKind {
563        if v == row.var {
564            row.kind
565        } else if v == col.var {
566            col.kind
567        } else {
568            IterKind::Reduce { pool: ReducePool::Sum }
569        }
570    };
571    let stride = |m: &ElemAccess, v: LoopVar| -> i64 { m.terms.iter().filter(|(t, _)| *t == v).map(|(_, s)| *s).sum() };
572    let axes: Vec<LoopVar> = [Some(row.var), Some(col.var), k.map(|k| k.var)].into_iter().flatten().collect();
573    let points = |v: LoopVar, l: usize| -> u64 { axes.iter().map(|a| if *a == v { extent(*a).div_ceil(l) } else { extent(*a) } as u64).product() };
574    // Each choice as (cycles, lanes, axis, issue); the scalar one first.
575    let mut choices = vec![(points(col.var, 1) * scalar.ii as u64, 1usize, LaneAxis::Mid, scalar)];
576    let scalar_only = body.post_order().iter().any(|n| matches!(n, TensorBody::FloorDiv(..)));
577    for lane in [LaneAxis::K, LaneAxis::Mid, LaneAxis::Out] {
578        // An outer lane axis is a parallel column each lane keeps: the outer axis is the column only where the row folds
579        // inside it.
580        if scalar_only || (lane == LaneAxis::K && k.is_none()) || (lane == LaneAxis::Out && !rows_inner) {
581            continue;
582        }
583        let v = lane_var(lane, row, col, k);
584        if extent(v) < 2 {
585            continue;
586        }
587        let levels = |l: usize| if matches!(kind(v), IterKind::Reduce { .. }) { l.trailing_zeros() as u64 } else { 0 };
588        let mut l = 2;
589        while l <= widest {
590            let aligned = maps.iter().enumerate().all(|(s, m)| match (index_of(s).is_some(), stride(m, v)) {
591                (true, moves) => moves == 0,
592                (false, 0) => true,
593                (false, 1) => m.base.rem_euclid(l as i64) == 0 && axes.iter().filter(|a| **a != v).all(|a| stride(m, *a).rem_euclid(l as i64) == 0),
594                _ => false,
595            });
596            if !aligned {
597                break;
598            }
599            let kept: Vec<(Pred, BTreeSet<usize>, BTreeSet<usize>)> = sides
600                .iter()
601                .filter(|(pred, _, _)| pred.var() != v || matches!(pred.rhs(), PredRhs::Const(b) if b % l == 0))
602                .cloned()
603                .collect();
604            let issue = Issue::of(ports, order, &index_of, lat, &kept);
605            choices.push((points(v, l) * issue.ii as u64 + levels(l), l, lane, issue));
606            l *= 2;
607        }
608    }
609    let (_, lanes, lane, issue) = choices.into_iter().min_by_key(|(cycles, lanes, lane, _)| (*cycles, *lanes, *lane)).expect("the scalar choice");
610    (lanes, lane, issue)
611}
612
613#[derive(Clone, Debug, PartialEq, Eq)]
614enum UnaryKind {
615    Requant { m: i64, s: u32 },
616    Bias,
617    Relu,
618    Scalar(ScalarBody),
619    Elementwise(ElementwiseOp),
620}
621
622/// The per-engine facts the printer and the twin both need.
623struct EnginePlan {
624    site: Site,
625    engine: Engine,
626    class: usize,
627    micro: Vec<Micro>,
628    item_start: usize,
629    item_count: usize,
630}
631
632struct Ctx<'a> {
633    p: &'a SpatialProgram,
634    te: usize,
635    tr: usize,
636    tc: usize,
637    grid_nx: usize,
638    plan_acc: AccPolicy,
639    /// Cycles from a memory read's address to its data: every op that reads a tensor or a buffer issues each address
640    /// at its phase and takes the data `lat` phases later.
641    lat: u64,
642    /// What every tensor, buffer and stage of the program holds: the width of each memory and of each datapath.
643    ranges: ProgramRanges,
644    /// The banks every memory of a site interleaves its elements over ([`SpatialProgram::mem_banks`]).
645    banks: usize,
646    /// The widest run the Generic unit takes a point: the target's vector lanes, no wider than the banks.
647    lanes: usize,
648    /// The tiles one page of each buffer holds ([`SpatialProgram::page_extents`]).
649    extent: Vec<usize>,
650    /// The constants the engine being printed compares its phase with, each read as the flag `ph_ge{k}`.
651    phase_flags: std::cell::RefCell<BTreeSet<u64>>,
652    /// The halts of a chip's program, in order: each the gather of the rendezvous a [`LinkPoint`] follows, and the link's
653    /// index. The root engine, past that gather, waits there for the host. Empty for a program that halts nowhere.
654    halts: Vec<(SemId, usize)>,
655    /// The widest product one multiplier slice takes ([`SiteSpec::multiplier`](crate::spatial::target::SiteSpec::multiplier)).
656    multiplier: Option<(u32, u32)>,
657    /// The tiles a tile-row of each tensor takes in its memory ([`tensor_pitch`]).
658    pitch: Vec<usize>,
659    /// Each buffer template whose pages keep their rows at a pitch: the rows' width and the pitch ([`page_pitches`]).
660    page_pitch: BTreeMap<BufferId, (usize, usize)>,
661}
662
663impl Ctx<'_> {
664    /// `ph >= k` as the register that holds it, `ph_ge{k}`, loaded from the next phase: no comparison of the phase stands
665    /// in front of what it guards. The engine being printed declares and loads every flag read.
666    fn ph_ge(&self, k: u64) -> String {
667        if k == 0 {
668            return "1'b1".into();
669        }
670        self.phase_flags.borrow_mut().insert(k);
671        format!("ph_ge{k}")
672    }
673    /// `ph < k`, as the flag of `ph >= k` negated.
674    fn ph_lt(&self, k: u64) -> String {
675        if k == 0 {
676            return "1'b0".into();
677        }
678        format!("!{}", self.ph_ge(k))
679    }
680    /// The accumulator policy of the section `stage` belongs to — a composition's sections each run under their own
681    /// plan — or the program's, for a program of one section.
682    fn acc_of(&self, stage: StageId) -> AccPolicy {
683        self.p
684            .sections
685            .iter()
686            .find(|s| (s.stages.0..s.stages.1).contains(&stage.idx()))
687            .and_then(|s| s.plan)
688            .map(|pl| pl.acc)
689            .unwrap_or(self.plan_acc)
690    }
691    fn page_elems(&self, b: BufferId) -> usize {
692        self.extent[b.idx()] * self.te
693    }
694    /// The words tensor `t`'s memory holds, its rows at the tensor's pitch: what `print_site` declares and the hex
695    /// image fills.
696    fn tensor_words(&self, t: &TensorDecl) -> usize {
697        tensor_words(t, self.pitch[t.id.idx()], self.te)
698    }
699    /// The words a page of `b` takes in its pool: its elements, its rows at their pitch where its template keeps one
700    /// ([`page_pitches`]).
701    fn page_words(&self, b: BufferId) -> usize {
702        match self.page_pitch.get(&self.template(b)) {
703            Some((w, pitch)) => self.page_elems(b) / w * pitch,
704            None => self.page_elems(b),
705        }
706    }
707    /// The words from one row of a page of `b` to the next, for rows of `width` elements: its template's pitch, or the
708    /// width where its rows lie end to end.
709    fn page_row(&self, b: BufferId, width: usize) -> usize {
710        self.page_pitch.get(&self.template(b)).map_or(width, |(_, pitch)| *pitch)
711    }
712    /// Whether a tile walk moves runs: its run is wider than an element, or it fills a page whose rows keep a pitch, whose
713    /// end of row the run walk steps over.
714    fn walks_runs(&self, f: &Fixed) -> bool {
715        self.run(f) > 1 || matches!(f, Fixed::ReadTiles { into, .. } if self.page_pitch.contains_key(&self.template(*into)))
716    }
717    fn depth(&self, b: BufferId) -> usize {
718        match self.p.buffer(b).kind {
719            BufferKind::Fifo { depth_pages, .. } | BufferKind::Interm { depth_pages, .. } => {
720                depth_pages
721            }
722            BufferKind::Accumulator { .. } => 1,
723        }
724    }
725    /// The width of a buffer's words: what every buffer of its template holds, since one FIFO module serves them all.
726    fn buffer_width(&self, b: BufferId) -> u32 {
727        let t = self.template(b);
728        self.p
729            .buffers
730            .iter()
731            .filter(|x| self.template(x.id) == t)
732            .filter_map(|x| self.ranges.buffers[x.id.idx()])
733            .map(signed_bits)
734            .max()
735            .unwrap_or(1)
736    }
737    fn template(&self, b: BufferId) -> BufferId {
738        self.p.buffer(b).origin.unwrap_or(b)
739    }
740    /// Cycles of a fixed op. An op that reads memory takes `lat` more, for its last read to return; a tile read through
741    /// a gather `lat` more again, for the index its addresses add.
742    fn duration(&self, f: &Fixed) -> u64 {
743        let te = self.te as u64;
744        let lat = self.lat;
745        match f {
746            Fixed::Push { .. }
747            | Fixed::Pop { .. }
748            | Fixed::Barrier
749            | Fixed::ReleaseAcc
750            | Fixed::SemSet { .. }
751            | Fixed::SemInc { .. }
752            | Fixed::McastSetValid { .. } => 1,
753            Fixed::ReadTiles {
754                block_h, block_w, at, ..
755            } => self.walk_cycles(f, *block_h, *block_w) + self.gather_skew(at) + lat,
756            Fixed::WriteTiles {
757                block_h, block_w, ..
758            } => self.walk_cycles(f, *block_h, *block_w) + lat,
759            Fixed::AcquireAcc { tiles, cols } => self.acc_walk_cycles(*tiles, *cols, self.acc_run(f)),
760            Fixed::PackAcc { tiles, accumulate, cols, .. } => self.acc_walk_cycles(*tiles, *cols, self.acc_run(f)) + if *accumulate { lat } else { 0 },
761            Fixed::Unary { tiles, .. } => *tiles as u64 * te + lat,
762            Fixed::Generic { unit, depth, .. } => unit.issue.cycles(unit.points(), *depth),
763            Fixed::Matmul { block, per_site_n, sub_rows, ins, reload, array, .. } => {
764                let (osh, ibw) = (block.osh as u64, block.ibw as u64);
765                let k = ibw * self.tc as u64;
766                let l = pipelined_latency(array.0, array.1) as u64;
767                let run = self.matmul_run(block, self.page_row(ins[1], *per_site_n), *per_site_n, *sub_rows);
768                let (reload, a, b) = self.matmul_loads(block, reload.is_some(), run);
769                if self.matvec(block) {
770                    // Each B element is multiplied as it lands and summed a cycle later; the sums join the accumulator
771                    // the cycle after the last one.
772                    reload + a + b + lat + 2
773                } else {
774                    reload + a + b + k + osh * self.tr as u64 + l + 1 + lat
775                }
776            }
777            Fixed::McastSend { buf, pages, .. } => (*pages * self.page_elems(*buf)) as u64 + 1 + lat,
778        }
779    }
780    /// Whether the site moves runs at all: its tiles are single elements, and its memories have more than one bank.
781    fn runs(&self) -> bool {
782        self.te == 1 && self.banks > 1
783    }
784    /// ★ A RUN IS A WORD OF ITS OWN ALIGNMENT: `w` consecutive elements from a multiple of `w`, `w` a power of two no
785    /// larger than the banks, each from its own bank at the one row every bank of the port shares. The run's `j`-th
786    /// element is in bank `start | j`, so a write puts lane `b & (w − 1)` in bank `b` and no port rotates by more than a
787    /// whole word of the run. A walk moves runs of the largest `w` every run it starts is aligned to, in every memory
788    /// it touches — the twin and the printer both take it from here, so a walk is never priced at a width the
789    /// hardware does not move.
790    ///
791    /// The largest such `w` for an address term `x`: every multiple of it keeps a run aligned.
792    fn align(&self, x: usize) -> usize {
793        if x == 0 {
794            self.banks
795        } else {
796            (1usize << x.trailing_zeros()).min(self.banks)
797        }
798    }
799    /// `rows` rows of `width` elements laid end to end from a word: the alignment every row's start keeps.
800    fn rows_align(&self, rows: usize, width: usize) -> usize {
801        if rows <= 1 {
802            self.banks
803        } else {
804            self.align(width)
805        }
806    }
807    /// A tile walk's tensor addresses, `block_h` rows `stride_h` apart from `at`: the alignment every row's start keeps —
808    /// the least of the base's, every loop stride's, every item coordinate of the op's section times its stride, and a
809    /// gather's tile stride, whatever index it reads.
810    fn tensor_align(&self, at: &TileAddr, block_h: usize, stride_h: usize, section: Option<usize>) -> usize {
811        let coords: Vec<(usize, usize)> = self
812            .p
813            .work
814            .items
815            .iter()
816            .filter(|it| section.is_none() || self.p.section_of_item(it.id) == section)
817            .map(|it| it.coord)
818            .collect();
819        let mut w = self.align(at.base).min(self.rows_align(block_h, stride_h));
820        if let Some(ind) = at.indirect {
821            w = w.min(self.align(ind.tile_stride));
822        }
823        for (v, stride) in &at.steps {
824            if *v == LV_ITEM_Y {
825                w = coords.iter().fold(w, |w, c| w.min(self.align(c.0 * stride)));
826            } else if *v == LV_ITEM_X {
827                w = coords.iter().fold(w, |w, c| w.min(self.align(c.1 * stride)));
828            } else {
829                w = w.min(self.align(*stride));
830            }
831        }
832        w
833    }
834    /// The elements a tile read or write moves a cycle: where its tiles are single elements laid one after another along
835    /// a row, a run as wide as both its tensor rows and its page rows are aligned to — a page starts on a word; one
836    /// element otherwise.
837    fn run(&self, f: &Fixed) -> usize {
838        match f {
839            Fixed::ReadTiles { at, block_h, block_w, stride_w, stride_h, section, into, .. } if self.runs() && *stride_w == 1 => {
840                self.rows_align(*block_h, self.page_row(*into, *block_w)).min(self.tensor_align(at, *block_h, *stride_h, *section))
841            }
842            Fixed::WriteTiles { at, block_h, block_w, stride_w, stride_h, section, .. } if self.runs() && *stride_w == 1 => {
843                self.rows_align(*block_h, *block_w).min(self.tensor_align(at, *block_h, *stride_h, *section))
844            }
845            _ => 1,
846        }
847    }
848    /// The elements an accumulator walk moves a cycle: where its tiles are single elements, a run of the region's row as
849    /// wide as the page it fills keeps its rows aligned — an acquire fills none; one element otherwise. A region's row is
850    /// its `cols` banks, which a run walks from column 0.
851    fn acc_run(&self, f: &Fixed) -> usize {
852        match f {
853            Fixed::AcquireAcc { .. } if self.runs() => self.banks,
854            Fixed::PackAcc { tiles, cols, .. } if self.runs() => self.rows_align(tiles.div_ceil((*cols).max(1)), *cols),
855            _ => 1,
856        }
857    }
858    /// The elements a matmul's loads move a cycle: where its tiles are single elements, a run as wide as every row of
859    /// every page it loads keeps — the partial's `osh` rows of `osw`, A's `osh` rows of `ibw` at its sub-block, B's
860    /// `ibw` rows of `osw` at its sub-block's columns, `b_row` apart (`per_site_n`, or its page's pitch); one element
861    /// otherwise.
862    fn matmul_run(&self, block: &BlockShape, b_row: usize, per_site_n: usize, sub_rows: usize) -> usize {
863        if !self.runs() {
864            return 1;
865        }
866        let (osh, osw, ibw) = (block.osh, block.osw, block.ibw);
867        let mut w = self.rows_align(osh, osw).min(self.rows_align(osh, ibw)).min(self.rows_align(ibw, b_row));
868        if sub_rows > 1 {
869            w = w.min(self.align(osh * ibw));
870        }
871        if per_site_n != osw {
872            w = w.min(self.align(osw));
873        }
874        w
875    }
876    /// ★ A MATMUL OF ONE ROW NEVER LOADS THE ARRAY. Loading the weight-stationary array costs its `k` rows and its fill
877    /// `k + n − 1` for one streamed row: a matrix-vector product — every matmul of a decode step — multiplies each B
878    /// element by its A element as it lands instead, one multiplier per column, and sums the column as the rows arrive.
879    fn matvec(&self, block: &BlockShape) -> bool {
880        block.osh * self.tr == 1
881    }
882    /// Cycles a matmul's loads take at `run` elements a cycle — the partial it reloads, its A rows, its B rows — each a
883    /// row's runs.
884    fn matmul_loads(&self, block: &BlockShape, reload: bool, run: usize) -> (u64, u64, u64) {
885        let (osh, osw, ibw, te) = (block.osh, block.osw, block.ibw, self.te);
886        let rows = |n: usize, width: usize| -> u64 { (n * (width * te).div_ceil(run)) as u64 };
887        (if reload { rows(osh, osw) } else { 0 }, rows(osh, ibw), rows(ibw, osw))
888    }
889    /// Cycles an accumulator walk over `tiles` tiles in rows of `cols` takes at `run` elements a cycle: each row's runs,
890    /// the last row's as far as the tiles reach.
891    fn acc_walk_cycles(&self, tiles: usize, cols: usize, run: usize) -> u64 {
892        let elems = tiles * self.te;
893        match run {
894            1 => elems as u64,
895            run => {
896                let cols = cols.max(1);
897                ((elems / cols) * cols.div_ceil(run) + (elems % cols).div_ceil(run)) as u64
898            }
899        }
900    }
901    /// Cycles a tile walk of `block_h` rows of `block_w` tiles takes: each row's runs, or each tile's elements.
902    fn walk_cycles(&self, f: &Fixed, block_h: usize, block_w: usize) -> u64 {
903        match self.run(f) {
904            1 => (block_h * block_w) as u64 * self.te as u64,
905            run => (block_h * block_w.div_ceil(run)) as u64,
906        }
907    }
908    /// The phases a tile read waits before its first address: a gather's addresses add the index it reads first.
909    fn gather_skew(&self, at: &TileAddr) -> u64 {
910        if at.indirect.is_some() {
911            self.lat
912        } else {
913            0
914        }
915    }
916}
917
918/// A tile walk's address on the fabric: at its tensor's pitch ([`tensor_pitch`]), and, on a site that moves runs, with
919/// only the terms that move it — an item coordinate's, or a loop's around it of more than one trip. A term of a variable
920/// no loop around the walk binds adds nothing to the address it issues ([`addr_v`]), and one of a loop that runs once
921/// adds zero; either would only narrow the runs the walk moves ([`Ctx::tensor_align`]).
922fn walk_addr(ctx: &Ctx, tensor: TensorId, at: &TileAddr, strides: (usize, usize), loops: &[(LoopVar, usize)]) -> (TileAddr, usize, usize) {
923    let (cols, pitch) = (ctx.p.tensor(tensor).tiles.cols_t, ctx.pitch[tensor.idx()]);
924    let mut at = at.clone();
925    if ctx.runs() {
926        at.steps.retain(|(v, _)| *v == LV_ITEM_Y || *v == LV_ITEM_X || loops.iter().any(|(l, count)| l == v && *count > 1));
927    }
928    (pitched_addr(&at, cols, pitch), pitched(strides.0, cols, pitch), pitched(strides.1, cols, pitch))
929}
930
931fn lower_ops(ctx: &Ctx, ops: &[TaggedOp], out: &mut Vec<Micro>) -> Result<(), SpatialLowerError> {
932    lower_ops_within(ctx, ops, &mut Vec::new(), out)
933}
934
935/// [`lower_ops`] inside `loops`, each with its trip count.
936fn lower_ops_within(ctx: &Ctx, ops: &[TaggedOp], loops: &mut Vec<(LoopVar, usize)>, out: &mut Vec<Micro>) -> Result<(), SpatialLowerError> {
937    for t in ops {
938        match &t.op {
939            ChoreoOp::Reserve { buf, pages } => out.push(Micro::Guarded(Guard::Reserve { buf: *buf, pages: *pages })),
940            ChoreoOp::Push { buf, pages } => out.push(Micro::Fixed(Fixed::Push { buf: *buf, pages: *pages })),
941            ChoreoOp::Wait { buf, pages } => out.push(Micro::Guarded(Guard::Wait { buf: *buf, pages: *pages })),
942            ChoreoOp::Pop { buf, pages } => out.push(Micro::Fixed(Fixed::Pop { buf: *buf, pages: *pages })),
943            ChoreoOp::ReadTiles { tensor, at, block_h, block_w, stride_w, stride_h, into } => {
944                let (at, stride_w, stride_h) = walk_addr(ctx, *tensor, at, (*stride_w, *stride_h), loops);
945                out.push(Micro::Fixed(Fixed::ReadTiles { tensor: *tensor, at, block_h: *block_h, block_w: *block_w, stride_w, stride_h, into: *into, section: ctx.p.section_of_stage(t.stage) }))
946            }
947            ChoreoOp::ReadBarrier | ChoreoOp::WriteBarrier | ChoreoOp::Flush { .. } => out.push(Micro::Fixed(Fixed::Barrier)),
948            ChoreoOp::WriteTiles { from, tensor, at, block_h, block_w, stride_w, stride_h } => {
949                let (at, stride_w, stride_h) = walk_addr(ctx, *tensor, at, (*stride_w, *stride_h), loops);
950                out.push(Micro::Fixed(Fixed::WriteTiles { from: *from, tensor: *tensor, at, block_h: *block_h, block_w: *block_w, stride_w, stride_h, section: ctx.p.section_of_stage(t.stage) }))
951            }
952            ChoreoOp::AcquireAcc { tiles } => out.push(Micro::Fixed(Fixed::AcquireAcc { tiles: *tiles, cols: 0 })),
953            ChoreoOp::ReleaseAcc => out.push(Micro::Fixed(Fixed::ReleaseAcc)),
954            ChoreoOp::Compute { op, ins, acc_reload } => {
955                // A pointwise op covers the tiles its region acquired, as the evaluator's does.
956                let region = out.iter().rev().find_map(|m| match m {
957                    Micro::Fixed(Fixed::AcquireAcc { tiles, .. }) => Some(Some(*tiles)),
958                    Micro::Fixed(Fixed::ReleaseAcc) => Some(None),
959                    _ => None,
960                });
961                let tiles = region.flatten().unwrap_or(ctx.p.acc_tiles_used).max(1);
962                match op {
963                    ComputeOp::MatmulTiles { block, sub_blocks, operand, .. } => {
964                        if ins.len() != 2 {
965                            return Err(SpatialLowerError::Unsupported("a tile matmul with two operand buffers"));
966                        }
967                        if precision_int_bits(*operand).is_none() {
968                            return Err(SpatialLowerError::Precision { needed: *operand });
969                        }
970                        out.push(Micro::Fixed(Fixed::Matmul { block: *block, per_site_n: sub_blocks.1 * block.osw, sub_rows: sub_blocks.0, operand: *operand, ins: [ins[0], ins[1]], reload: *acc_reload, tiles: block.osh * block.osw, stage: t.stage, array: (block.ibw * ctx.tc, block.osw * ctx.tc) }));
971                    }
972                    ComputeOp::Requant { m, s } => out.push(Micro::Fixed(Fixed::Unary { kind: UnaryKind::Requant { m: *m, s: *s }, ins: ins.clone(), tiles, stage: t.stage })),
973                    ComputeOp::Bias { .. } => out.push(Micro::Fixed(Fixed::Unary { kind: UnaryKind::Bias, ins: ins.clone(), tiles, stage: t.stage })),
974                    ComputeOp::Relu => out.push(Micro::Fixed(Fixed::Unary { kind: UnaryKind::Relu, ins: ins.clone(), tiles, stage: t.stage })),
975                    ComputeOp::Scalar(body) => out.push(Micro::Fixed(Fixed::Unary { kind: UnaryKind::Scalar(body.clone()), ins: ins.clone(), tiles, stage: t.stage })),
976                    // The general form, where the fabric can express it: identity maps and one
977                    // binary operation over two operands is exactly its `UnaryKind::Elementwise`
978                    // datapath. Anything else (a broadcast, a reduction, a transcendental) is refused
979                    // by name below — the integer fabric genuinely has no unit for those.
980                    ComputeOp::Generic { iters, ins: maps, out: omap, body: TensorBody::Add(a, b) }
981                        if ctx.p.generic_is_plain_pointwise(iters, maps, omap, ins) && matches!(**a, TensorBody::Operand(0)) && matches!(**b, TensorBody::Operand(1)) =>
982                    {
983                        out.push(Micro::Fixed(Fixed::Unary { kind: UnaryKind::Elementwise(ElementwiseOp::Add), ins: ins.clone(), tiles, stage: t.stage }))
984                    }
985                    ComputeOp::Generic { iters, ins: maps, out: omap, body: TensorBody::Mul(a, b) }
986                        if ctx.p.generic_is_plain_pointwise(iters, maps, omap, ins) && matches!(**a, TensorBody::Operand(0)) && matches!(**b, TensorBody::Operand(1)) =>
987                    {
988                        out.push(Micro::Fixed(Fixed::Unary { kind: UnaryKind::Elementwise(ElementwiseOp::Mul), ins: ins.clone(), tiles, stage: t.stage }))
989                    }
990                    // A general form the integer fabric cannot express (a broadcast, a reduction, a
991                    // transcendental), or the fused row spec — it genuinely has no unit for these.
992                    // ★ THE GENERAL FORM IS THE EVALUATOR'S INTEGER ARM IN HARDWARE: walk the space, read each slot
993                    // through its map (a gather through the value its index slot read), apply the body, fold. A
994                    // body with a transcendental, or an operand that is not an integer buffer, has no unit here.
995                    ComputeOp::Generic { iters, ins: maps, body, .. } => {
996                        let (row, col, k) = match iters.as_slice() {
997                            [r, c] => (*r, *c, None),
998                            [r, c, k] => (*r, *c, Some(*k)),
999                            _ => return Err(SpatialLowerError::Unsupported("a Generic whose space is not (rows, cols) or (rows, cols, contraction)")),
1000                        };
1001                        if k.is_some_and(|k| !matches!(k.kind, IterKind::Reduce { pool: ReducePool::Sum })) {
1002                            return Err(SpatialLowerError::Unsupported("a Generic whose contraction axis reduces by a pool other than a sum"));
1003                        }
1004                        if !body.integral() || ins.iter().any(|b| precision_int_bits(ctx.p.buffer(*b).dtype).is_none()) {
1005                            return Err(SpatialLowerError::Unsupported("a float row op on the integer fabric"));
1006                        }
1007                        if maps.len() != ins.len() || ctx.tr != ctx.tc {
1008                            return Err(SpatialLowerError::Unsupported("a Generic whose maps do not match its operands, or a non-square tile"));
1009                        }
1010                        let tiles = if matches!(col.kind, IterKind::Reduce { .. }) { 1 } else { col.extent.div_ceil(ctx.tr) };
1011                        let order = read_order(maps)?;
1012                        let ports: Vec<usize> = ins.iter().map(|b| ctx.template(*b).idx()).collect();
1013                        let (lanes, lane, issue) = lanes_of(ctx.lanes, &row, &col, k.as_ref(), maps, body, &ports, &order, ctx.lat as usize);
1014                        out.push(Micro::Fixed(Fixed::Generic {
1015                            unit: Box::new(GenUnit { row, col, k, maps: maps.clone(), body: body.clone(), issue, lanes, lane }),
1016                            ins: ins.clone(),
1017                            tiles,
1018                            stage: t.stage,
1019                            depth: 0,
1020                        }))
1021                    }
1022                }
1023            }
1024            ChoreoOp::PackAcc { to, tiles, accumulate, .. } => out.push(Micro::Fixed(Fixed::PackAcc { to: *to, tiles: *tiles, trunc32: ctx.acc_of(t.stage) == AccPolicy::I32, accumulate: *accumulate, cols: 0 })),
1025            ChoreoOp::SemSet { sem, value } => out.push(Micro::Fixed(Fixed::SemSet { sem: *sem, value: *value })),
1026            ChoreoOp::SemInc { sem, at, by } => out.push(Micro::Fixed(Fixed::SemInc { sem: *sem, at: *at, by: *by })),
1027            ChoreoOp::SemWait { sem, until } => {
1028                out.push(Micro::Guarded(Guard::SemWait { sem: *sem, until: *until }));
1029                if let Some(ordinal) = ctx.halts.iter().position(|(g, _)| g == sem) {
1030                    out.push(Micro::Guarded(Guard::Link { point: ctx.halts[ordinal].1, ordinal }));
1031                }
1032            }
1033            ChoreoOp::McastSetValid { sem, group } => out.push(Micro::Fixed(Fixed::McastSetValid { sem: *sem, group: *group })),
1034            ChoreoOp::McastSend { buf, pages, group, .. } => out.push(Micro::Fixed(Fixed::McastSend { buf: *buf, pages: *pages, group: *group })),
1035            ChoreoOp::Barrier { .. } => return Err(SpatialLowerError::Unsupported("a group barrier on the fabric")),
1036            ChoreoOp::Loop { count: LoopCount::Trip { .. }, .. } => {
1037                return Err(SpatialLowerError::Unsupported("a loop whose count the program reads from its data (a trip) — the fabric's loop micro-ops count to a constant"))
1038            }
1039            ChoreoOp::Loop { var, count, body } => {
1040                let start = out.len();
1041                out.push(Micro::LoopStart { var: *var, count: count.bound(), end: 0 });
1042                loops.push((*var, count.bound()));
1043                lower_ops_within(ctx, body, loops, out)?;
1044                loops.pop();
1045                let end = out.len();
1046                out.push(Micro::LoopEnd { var: *var, count: count.bound(), start });
1047                if let Micro::LoopStart { end: e, .. } = &mut out[start] {
1048                    *e = end;
1049                }
1050            }
1051        }
1052    }
1053    Ok(())
1054}
1055
1056/// The port signals an effect drives — a FIFO's push or pop, a semaphore's increment or set, a group's valid flag, or
1057/// none for a barrier or a release — or `None` for an op that is no effect.
1058fn effect_signals(ctx: &Ctx, f: &Fixed) -> Option<Vec<String>> {
1059    Some(match f {
1060        Fixed::Push { buf, .. } => vec![format!("f{}_push", ctx.template(*buf).idx())],
1061        Fixed::Pop { buf, .. } => vec![format!("f{}_pop", ctx.template(*buf).idx())],
1062        Fixed::SemInc { sem, .. } => vec![format!("s{}_inc", sem.idx())],
1063        Fixed::SemSet { sem, .. } => vec![format!("s{}_set", sem.idx())],
1064        Fixed::McastSetValid { sem, group } => vec![format!("mv{}_g{}", sem.idx(), group.idx())],
1065        Fixed::Barrier | Fixed::ReleaseAcc => Vec::new(),
1066        _ => return None,
1067    })
1068}
1069
1070/// Fold every effect that follows a fixed op into that op's last cycle ([`Micro::Then`]). An op's work commits at the
1071/// clock edge that ends its last cycle; the effect that signals it — a push of the page it filled, a pop of the page it
1072/// has read, an increment telling another engine it is done — commits at that same edge, where it took a cycle of its
1073/// own the edge after. Two effects on one port signal never share a cycle, and an effect after a guard or a loop's
1074/// bound keeps its own: a jump only ever lands on the op after a loop's bound, so none lands on an effect.
1075fn fold_effects(ctx: &Ctx, micro: &mut [Micro]) {
1076    let mut used: Option<BTreeSet<String>> = None;
1077    for m in micro.iter_mut() {
1078        let Micro::Fixed(f) = m else {
1079            used = None;
1080            continue;
1081        };
1082        let f = f.clone();
1083        let signals = effect_signals(ctx, &f);
1084        if let (Some(sig), Some(u)) = (&signals, used.as_mut()) {
1085            if sig.iter().all(|s| !u.contains(s)) {
1086                u.extend(sig.iter().cloned());
1087                *m = Micro::Then(f);
1088                continue;
1089            }
1090        }
1091        used = Some(signals.unwrap_or_default().into_iter().collect());
1092    }
1093}
1094
1095/// What an effect or a guard reads or moves: a buffer's FIFO, or a semaphore.
1096#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1097enum Resource {
1098    Fifo(BufferId),
1099    Sem(SemId),
1100    /// The host's continues, which no op of the fabric moves.
1101    Link,
1102}
1103
1104fn effect_resource(f: &Fixed) -> Option<Resource> {
1105    match f {
1106        Fixed::Push { buf, .. } | Fixed::Pop { buf, .. } => Some(Resource::Fifo(*buf)),
1107        Fixed::SemInc { sem, .. } | Fixed::SemSet { sem, .. } | Fixed::McastSetValid { sem, .. } => Some(Resource::Sem(*sem)),
1108        _ => None,
1109    }
1110}
1111
1112fn guard_resource(g: &Guard) -> Resource {
1113    match g {
1114        Guard::Reserve { buf, .. } | Guard::Wait { buf, .. } => Resource::Fifo(*buf),
1115        Guard::SemWait { sem, .. } => Resource::Sem(*sem),
1116        Guard::Link { .. } => Resource::Link,
1117    }
1118}
1119
1120/// What a state's own effects do to a FIFO or a semaphore in the cycle they commit: a FIFO's count and reservations,
1121/// a semaphore's count at this site — or that it is set outright, which no early check reasons past.
1122#[derive(Clone, Copy, Default)]
1123struct OwnDelta {
1124    count: i64,
1125    reserved: i64,
1126    sem: i64,
1127    set: bool,
1128}
1129
1130/// The own effects of the fixed op at `pc` and those folded into it.
1131fn own_deltas(micro: &[Micro], pc: usize, site: Site) -> BTreeMap<Resource, OwnDelta> {
1132    let mut out: BTreeMap<Resource, OwnDelta> = BTreeMap::new();
1133    for m in &micro[pc..after_effects(micro, pc)] {
1134        let (Micro::Fixed(f) | Micro::Then(f)) = m else { continue };
1135        let Some(r) = effect_resource(f) else { continue };
1136        let d = out.entry(r).or_default();
1137        match f {
1138            Fixed::Push { pages, .. } => {
1139                d.count += *pages as i64;
1140                d.reserved -= *pages as i64;
1141            }
1142            Fixed::Pop { pages, .. } => d.count -= *pages as i64,
1143            Fixed::SemInc { at, by, .. } if *at == site => d.sem += *by as i64,
1144            Fixed::SemInc { .. } => {}
1145            _ => d.set = true,
1146        }
1147    }
1148    out
1149}
1150
1151/// A guard checked a cycle early: the threshold its FIFO's or semaphore's registered state must reach once this
1152/// state's own effects — and the reservations of the guards checked before it — are counted, or `None` where no early
1153/// check is sound. Another engine only ever pushes what this one waits on, pops what it reserves, raises what it counts
1154/// up to, so a state that reaches the threshold a cycle early reaches it on time; an equality on a semaphore this state
1155/// moves, or one set outright, has no such order.
1156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1157enum Early {
1158    /// `count ≥ n` — an `n` of zero or less always holds.
1159    Count(BufferId, i64),
1160    /// `free ≥ n`.
1161    Free(BufferId, i64),
1162    /// `count ≥ n` of a semaphore.
1163    AtLeast(SemId, i64),
1164    /// `count == n` of a semaphore this state leaves alone.
1165    Equals(SemId, i64),
1166}
1167
1168fn early_check(g: &Guard, own: &BTreeMap<Resource, OwnDelta>) -> Option<Early> {
1169    let d = own.get(&guard_resource(g)).copied().unwrap_or_default();
1170    match g {
1171        Guard::Wait { buf, pages } => Some(Early::Count(*buf, *pages as i64 - d.count)),
1172        Guard::Reserve { buf, pages } => Some(Early::Free(*buf, *pages as i64 + d.count + d.reserved)),
1173        Guard::SemWait { sem, until } if !d.set => match until {
1174            SemCond::AtLeast(n) => Some(Early::AtLeast(*sem, *n as i64 - d.sem)),
1175            SemCond::Equals(n) if d.sem == 0 => Some(Early::Equals(*sem, *n as i64)),
1176            SemCond::Equals(_) => None,
1177        },
1178        Guard::SemWait { .. } => None,
1179        // Only the host opens a halt: no state of the fabric's reaches it a cycle early.
1180        Guard::Link { .. } => None,
1181    }
1182}
1183
1184/// A loop's bound a fixed op's last cycle takes itself: the end of a loop — its variable stepped, back to the body's
1185/// first op while it runs, past the loop when it is done — or the start of one, its variable cleared.
1186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1187enum Bound {
1188    End { var: LoopVar, count: usize, start: usize },
1189    Start { var: LoopVar, count: usize, end: usize },
1190}
1191
1192/// Where a fixed op's last cycle goes. Past its folded effects; through one loop bound that follows them; then, in each
1193/// branch the bound leaves (the loop running on, or done; for a start, its body or — at a count of zero — past it),
1194/// past the guards that branch lands on, each checked early ([`Early`]): all of a branch's open, the op takes their
1195/// reservations and goes past them; one not open, it lands on the first, whose own state waits as before.
1196#[derive(Clone, Debug, PartialEq, Eq)]
1197struct Exit {
1198    bound: Option<Bound>,
1199    /// Each branch: the state it lands on, and the guards there it checks early with their checks.
1200    branches: Vec<(usize, Vec<(usize, Early)>)>,
1201}
1202
1203fn exit_of(micro: &[Micro], pc: usize, site: Site) -> Exit {
1204    let own = own_deltas(micro, pc, site);
1205    let after = after_effects(micro, pc);
1206    let checked = |at: usize| -> Vec<(usize, Early)> {
1207        let mut own = own.clone();
1208        let mut out = Vec::new();
1209        while let Some(Micro::Guarded(g)) = micro.get(at + out.len()) {
1210            let Some(e) = early_check(g, &own) else { break };
1211            if let Guard::Reserve { buf, pages } = g {
1212                own.entry(Resource::Fifo(*buf)).or_default().reserved += *pages as i64;
1213            }
1214            out.push((at + out.len(), e));
1215        }
1216        out
1217    };
1218    match micro.get(after) {
1219        Some(Micro::LoopEnd { var, count, start }) => Exit {
1220            bound: Some(Bound::End { var: *var, count: *count, start: *start }),
1221            branches: vec![(start + 1, checked(start + 1)), (after + 1, checked(after + 1))],
1222        },
1223        Some(Micro::LoopStart { var, count, end }) => Exit {
1224            bound: Some(Bound::Start { var: *var, count: *count, end: *end }),
1225            branches: vec![(after + 1, checked(after + 1)), (end + 1, Vec::new())],
1226        },
1227        _ => Exit { bound: None, branches: vec![(after, checked(after))] },
1228    }
1229}
1230
1231/// The state after the fixed op at `pc`: past the effects folded into it.
1232fn after_effects(micro: &[Micro], pc: usize) -> usize {
1233    let mut n = pc + 1;
1234    while matches!(micro.get(n), Some(Micro::Then(_))) {
1235        n += 1;
1236    }
1237    n
1238}
1239
1240fn plans(ctx: &Ctx) -> Result<Vec<EnginePlan>, SpatialLowerError> {
1241    let p = ctx.p;
1242    let sched = p
1243        .schedule
1244        .as_ref()
1245        .ok_or(SpatialLowerError::Unsupported("a scheduled program"))?;
1246    let pl = p
1247        .placement
1248        .as_ref()
1249        .ok_or(SpatialLowerError::Unsupported("a placed program"))?;
1250    let mut classes: Vec<String> = Vec::new();
1251    let mut out = Vec::new();
1252    for prog in &sched.programs {
1253        let sig = format!("{:?}:{}", prog.engine, p.engine_signature(prog));
1254        let class = match classes.iter().position(|c| *c == sig) {
1255            Some(i) => i,
1256            None => {
1257                classes.push(sig);
1258                classes.len() - 1
1259            }
1260        };
1261        let mut micro = Vec::new();
1262        lower_ops(ctx, &prog.ops, &mut micro)?;
1263        let physical = micro.iter().fold((0, 0), |acc, m| match m {
1264            Micro::Fixed(Fixed::Matmul { array, .. }) => (acc.0.max(array.0), acc.1.max(array.1)),
1265            _ => acc,
1266        });
1267        for m in &mut micro {
1268            if let Micro::Fixed(Fixed::Matmul { array, .. }) = m {
1269                *array = physical;
1270            }
1271        }
1272        let regions = engine::acc_regions(&micro);
1273        for (m, region) in micro.iter_mut().zip(regions) {
1274            if let (Micro::Fixed(Fixed::AcquireAcc { cols, .. } | Fixed::PackAcc { cols, .. }), Some(r)) = (m, region) {
1275                *cols = r;
1276            }
1277        }
1278        fold_effects(ctx, &mut micro);
1279        let items = pl.items_at(prog.site);
1280        out.push(EnginePlan {
1281            site: prog.site,
1282            engine: prog.engine,
1283            class,
1284            micro,
1285            item_start: items.first().map(|i| i.idx()).unwrap_or(0),
1286            item_count: items.len(),
1287        });
1288    }
1289    engine::set_generic_depths(ctx, &mut out)?;
1290    Ok(out)
1291}
1292
1293// ── The discrete-event twin ───────────────────────────────────────────────────────────────────────────────
1294
1295#[derive(Clone, Default)]
1296struct FifoState {
1297    count: usize,
1298    reserved: usize,
1299}
1300
1301struct EngineState {
1302    pc: usize,
1303    ph: u64,
1304    lv: BTreeMap<LoopVar, usize>,
1305    done: bool,
1306}
1307
1308/// Predicted RTL cycles of `p`: every engine walks its micro-program in lock-step cycles against registered
1309/// buffer and semaphore state (an effect lands the cycle after it is issued), exactly as the FSMs do.
1310pub fn fabric_cycle_model(p: &SpatialProgram, target: &SpatialTarget) -> u64 {
1311    let Ok(ctx) = context(p, target) else { return 0 };
1312    let Ok(plans) = plans(&ctx) else { return 0 };
1313    twin(&ctx, &plans, &mut |_, _| {}, &mut Vec::new(), TWIN_LIMIT)
1314}
1315
1316/// The cycles past which the twin stops: a program still running there is reported at the limit plus one.
1317const TWIN_LIMIT: u64 = 50_000_000;
1318
1319/// The twin's cycles for `p` where it finishes within `limit` cycles; `None` where it does not, or where the fabric
1320/// cannot lower it. The twin stops at the limit, so asking costs at most `limit` cycles of simulation.
1321pub fn fabric_cycles_within(p: &SpatialProgram, target: &SpatialTarget, limit: u64) -> Option<u64> {
1322    let ctx = context(p, target).ok()?;
1323    let plans = plans(&ctx).ok()?;
1324    let cycles = twin(&ctx, &plans, &mut |_, _| {}, &mut Vec::new(), limit);
1325    (cycles <= limit).then_some(cycles)
1326}
1327
1328/// The fabric's cycle counter at each halt of `p`'s program ([`LinkPoint`]), in order, then at its end: what its board
1329/// replies to each `X`. The counter pauses while the fabric is halted, so each count is the cycles the program has run.
1330/// One count — [`fabric_cycle_model`]'s — for a program that halts nowhere; `None` where the fabric does not lower `p`.
1331pub fn fabric_cycle_segments(p: &SpatialProgram, target: &SpatialTarget) -> Option<Vec<u64>> {
1332    let ctx = context(p, target).ok()?;
1333    let plans = plans(&ctx).ok()?;
1334    let mut halts = Vec::new();
1335    let total = twin(&ctx, &plans, &mut |_, _| {}, &mut halts, TWIN_LIMIT);
1336    halts.push(total);
1337    Some(halts)
1338}
1339
1340/// What one engine did in one cycle of the twin.
1341#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1342pub enum Activity {
1343    /// Running an op of this kind, or passing a guard of it.
1344    Busy(&'static str),
1345    /// Held at a guard of this kind: a page not yet free or filled, a semaphore not yet raised.
1346    Waiting(&'static str),
1347    /// Entering or closing a loop.
1348    Looping,
1349    /// Finished, while another engine still runs.
1350    Done,
1351}
1352
1353/// One engine's cycles of a run, by what it did in each.
1354#[derive(Clone, Debug, PartialEq, Eq)]
1355pub struct EngineCycles {
1356    pub site: Site,
1357    pub engine: Engine,
1358    pub cycles: BTreeMap<Activity, u64>,
1359}
1360
1361/// Where a run's cycles go: its total, the twin's, and each engine's cycles by activity — every engine accounting for
1362/// every cycle of the run.
1363#[derive(Clone, Debug, PartialEq, Eq)]
1364pub struct CycleBreakdown {
1365    pub total: u64,
1366    pub engines: Vec<EngineCycles>,
1367}
1368
1369/// [`fabric_cycle_model`] with each engine's cycles attributed: what it ran, what held it, when it was done. `None`
1370/// where the fabric does not lower `p`.
1371pub fn fabric_cycle_breakdown(p: &SpatialProgram, target: &SpatialTarget) -> Option<CycleBreakdown> {
1372    let ctx = context(p, target).ok()?;
1373    let plans = plans(&ctx).ok()?;
1374    let mut engines: Vec<EngineCycles> =
1375        plans.iter().map(|pl| EngineCycles { site: pl.site, engine: pl.engine, cycles: BTreeMap::new() }).collect();
1376    let total = twin(&ctx, &plans, &mut |ei, a| *engines[ei].cycles.entry(a).or_default() += 1, &mut Vec::new(), TWIN_LIMIT);
1377    Some(CycleBreakdown { total, engines })
1378}
1379
1380/// Each engine's micro-program as the twin and the printer walk it: one line per state — the op, and a fixed op's
1381/// cycles; a tile walk's run, and an accumulator walk's region columns and run. `None` where the fabric does not lower
1382/// `p`.
1383pub fn fabric_micro_listing(p: &SpatialProgram, target: &SpatialTarget) -> Option<Vec<(Site, Engine, Vec<String>)>> {
1384    let ctx = context(p, target).ok()?;
1385    let plans = plans(&ctx).ok()?;
1386    Some(
1387        plans
1388            .iter()
1389            .map(|pl| {
1390                let regions = engine::acc_regions(&pl.micro);
1391                let lines = pl
1392                    .micro
1393                    .iter()
1394                    .enumerate()
1395                    .map(|(i, m)| match m {
1396                        Micro::Fixed(f @ (Fixed::ReadTiles { .. } | Fixed::WriteTiles { .. })) => {
1397                            format!("{} [{}] {f:?} run {}", fixed_kind(f), ctx.duration(f), ctx.run(f))
1398                        }
1399                        Micro::Fixed(f @ (Fixed::AcquireAcc { .. } | Fixed::PackAcc { .. })) => format!(
1400                            "{} [{}] {f:?} region {} run {}",
1401                            fixed_kind(f),
1402                            ctx.duration(f),
1403                            regions[i].unwrap_or(1),
1404                            ctx.acc_run(f)
1405                        ),
1406                        Micro::Fixed(f @ Fixed::Generic { unit, .. }) => format!("{} [{}] {f:?} points: {}", fixed_kind(f), ctx.duration(f), unit.points()),
1407                        Micro::Fixed(f) => format!("{} [{}] {f:?}", fixed_kind(f), ctx.duration(f)),
1408                        Micro::Then(f) => format!("  then {f:?}"),
1409                        Micro::Guarded(g) => format!("{} {g:?}", guard_kind(g)),
1410                        Micro::LoopStart { var, count, .. } => format!("loop {var:?} × {count}"),
1411                        Micro::LoopEnd { var, .. } => format!("end loop {var:?}"),
1412                    })
1413                    .collect();
1414                (pl.site, pl.engine, lines)
1415            })
1416            .collect(),
1417    )
1418}
1419
1420/// The kind a fixed op's cycles count under.
1421fn fixed_kind(f: &Fixed) -> &'static str {
1422    match f {
1423        Fixed::ReadTiles { .. } => "read tiles",
1424        Fixed::WriteTiles { .. } => "write tiles",
1425        Fixed::Push { .. } => "push",
1426        Fixed::Pop { .. } => "pop",
1427        Fixed::Barrier => "barrier",
1428        Fixed::AcquireAcc { .. } => "acquire",
1429        Fixed::ReleaseAcc => "release",
1430        Fixed::Matmul { .. } => "matmul",
1431        Fixed::Unary { .. } => "unary",
1432        Fixed::Generic { .. } => "generic",
1433        Fixed::PackAcc { .. } => "pack",
1434        Fixed::SemSet { .. } | Fixed::SemInc { .. } => "semaphore",
1435        Fixed::McastSetValid { .. } | Fixed::McastSend { .. } => "multicast",
1436    }
1437}
1438
1439fn guard_kind(g: &Guard) -> &'static str {
1440    match g {
1441        Guard::Reserve { .. } => "reserve",
1442        Guard::Wait { .. } => "wait",
1443        Guard::SemWait { .. } => "semaphore wait",
1444        Guard::Link { .. } => "link",
1445    }
1446}
1447
1448/// The twin: every engine in lock-step, `observe` told what each did in each cycle. Returns the cycles from `start` to
1449/// `done`.
1450fn twin(ctx: &Ctx, plans: &[EnginePlan], observe: &mut dyn FnMut(usize, Activity), halts: &mut Vec<u64>, limit: u64) -> u64 {
1451    let p = ctx.p;
1452    let mut fifos: BTreeMap<BufferId, FifoState> = p
1453        .buffers
1454        .iter()
1455        .map(|b| (b.id, FifoState::default()))
1456        .collect();
1457    let mut sems: BTreeMap<(SemId, Site), usize> = BTreeMap::new();
1458    for s in &p.semaphores {
1459        for site in &s.sites {
1460            sems.insert((s.id, *site), s.init as usize);
1461        }
1462    }
1463    let mut states: Vec<EngineState> = plans
1464        .iter()
1465        .map(|_| EngineState {
1466            pc: 0,
1467            ph: 0,
1468            lv: BTreeMap::new(),
1469            done: false,
1470        })
1471        .collect();
1472    let receivers_of = |g: GroupId| -> Vec<Site> {
1473        let pl = p.placement.as_ref().unwrap();
1474        let gr = &pl.groups[g.idx()];
1475        gr.rect
1476            .sites()
1477            .into_iter()
1478            .filter(|s| *s != gr.sender)
1479            .collect()
1480    };
1481    // The top counts from the `start` pulse to `done`: one cycle for every engine to latch `running`, then
1482    // the FSM rounds, then the cycle in which the last engine raises `done`.
1483    let mut cycles: u64 = 2;
1484    loop {
1485        if states.iter().all(|s| s.done) || cycles > limit {
1486            break;
1487        }
1488        // Phase 1: decide on registered state.
1489        let mut fifo_delta: BTreeMap<BufferId, (i64, i64)> = BTreeMap::new(); // (count, reserved)
1490        let mut sem_inc: BTreeMap<(SemId, Site), usize> = BTreeMap::new();
1491        let mut sem_set: BTreeMap<(SemId, Site), usize> = BTreeMap::new();
1492        let mut next: Vec<Option<(usize, u64, Option<(LoopVar, usize)>)>> = Vec::new();
1493        for (ei, plan) in plans.iter().enumerate() {
1494            let st = &states[ei];
1495            if st.done {
1496                observe(ei, Activity::Done);
1497                next.push(None);
1498                continue;
1499            }
1500            let Some(m) = plan.micro.get(st.pc) else {
1501                observe(ei, Activity::Done);
1502                next.push(Some((st.pc, 0, None)));
1503                continue;
1504            };
1505            observe(
1506                ei,
1507                match m {
1508                    Micro::Fixed(f) | Micro::Then(f) => Activity::Busy(fixed_kind(f)),
1509                    Micro::LoopStart { .. } | Micro::LoopEnd { .. } => Activity::Looping,
1510                    Micro::Guarded(g) => {
1511                        let open = match g {
1512                            Guard::Reserve { buf, pages } => {
1513                                let f = &fifos[buf];
1514                                ctx.depth(*buf) >= f.count + f.reserved + *pages
1515                            }
1516                            Guard::Wait { buf, pages } => fifos[buf].count >= *pages,
1517                            Guard::SemWait { sem, until } => {
1518                                let v = *sems.get(&(*sem, plan.site)).unwrap_or(&0);
1519                                match until {
1520                                    SemCond::Equals(n) => v == *n,
1521                                    SemCond::AtLeast(n) => v >= *n,
1522                                }
1523                            }
1524                            Guard::Link { .. } => true,
1525                        };
1526                        if open {
1527                            Activity::Busy(guard_kind(g))
1528                        } else {
1529                            Activity::Waiting(guard_kind(g))
1530                        }
1531                    }
1532                },
1533            );
1534            let open = |g: &Guard| match g {
1535                Guard::Reserve { buf, pages } => {
1536                    let f = &fifos[buf];
1537                    ctx.depth(*buf) >= f.count + f.reserved + *pages
1538                }
1539                Guard::Wait { buf, pages } => fifos[buf].count >= *pages,
1540                Guard::SemWait { sem, until } => {
1541                    let v = *sems.get(&(*sem, plan.site)).unwrap_or(&0);
1542                    match until {
1543                        SemCond::Equals(n) => v == *n,
1544                        SemCond::AtLeast(n) => v >= *n,
1545                    }
1546                }
1547                // The host continues a halted fabric at once: the halt costs the cycle that leaves it, and the counter,
1548                // paused while it is halted, reads the cycles run before it.
1549                Guard::Link { .. } => true,
1550            };
1551            // The count starts at two, the start's latch and the cycle that raises `done`; while the engines run, the
1552            // counter has counted only the first, so in any running cycle it reads one less than the count.
1553            if let Micro::Guarded(Guard::Link { .. }) = m {
1554                halts.push(cycles - 1);
1555            }
1556            let step = match m {
1557                Micro::Guarded(g) => {
1558                    let ok = open(g);
1559                    if ok {
1560                        if let Guard::Reserve { buf, pages } = g {
1561                            fifo_delta.entry(*buf).or_default().1 += *pages as i64;
1562                        }
1563                        Some((st.pc + 1, 0, None))
1564                    } else {
1565                        Some((st.pc, 0, None))
1566                    }
1567                }
1568                Micro::Fixed(f) => {
1569                    let d = ctx.duration(f);
1570                    if st.ph + 1 >= d {
1571                        // The op's own effect and every effect folded into its last cycle.
1572                        let next = after_effects(&plan.micro, st.pc);
1573                        let folded = plan.micro[st.pc + 1..next].iter().filter_map(|m| match m {
1574                            Micro::Then(e) => Some(e),
1575                            _ => None,
1576                        });
1577                        for e in std::iter::once(f).chain(folded) {
1578                            match e {
1579                                Fixed::Push { buf, pages } => {
1580                                    let e = fifo_delta.entry(*buf).or_default();
1581                                    e.0 += *pages as i64;
1582                                    e.1 -= *pages as i64;
1583                                }
1584                                Fixed::Pop { buf, pages } => fifo_delta.entry(*buf).or_default().0 -= *pages as i64,
1585                                Fixed::SemSet { sem, value } => {
1586                                    let v = match value {
1587                                        SemValue::Invalid => 0,
1588                                        SemValue::Valid => 1,
1589                                        SemValue::Count(n) => *n,
1590                                    };
1591                                    sem_set.insert((*sem, plan.site), v);
1592                                }
1593                                Fixed::SemInc { sem, at, by } => *sem_inc.entry((*sem, *at)).or_default() += *by,
1594                                Fixed::McastSetValid { sem, group } => {
1595                                    for r in receivers_of(*group) {
1596                                        sem_set.insert((*sem, r), 1);
1597                                    }
1598                                }
1599                                _ => {}
1600                            }
1601                        }
1602                        // Through the loop bound it takes itself, then past the guards of its branch it checks early.
1603                        let exit = exit_of(&plan.micro, st.pc, plan.site);
1604                        let count = |var: LoopVar, count: usize| if var == LV_ITEM { plan.item_count } else { count };
1605                        let (branch, lv) = match exit.bound {
1606                            None => (0, None),
1607                            Some(Bound::End { var, count: c, .. }) => {
1608                                let v = st.lv.get(&var).copied().unwrap_or(0) + 1;
1609                                (if v < count(var, c) { 0 } else { 1 }, Some((var, v)))
1610                            }
1611                            Some(Bound::Start { var, count: c, .. }) => (if count(var, c) == 0 { 1 } else { 0 }, Some((var, 0))),
1612                        };
1613                        let (at, checks) = &exit.branches[branch];
1614                        let holds = |e: &Early| match *e {
1615                            Early::Count(b, n) => fifos[&b].count as i64 >= n,
1616                            Early::Free(b, n) => {
1617                                let f = &fifos[&b];
1618                                ctx.depth(b) as i64 - f.count as i64 - f.reserved as i64 >= n
1619                            }
1620                            Early::AtLeast(sem, n) => *sems.get(&(sem, plan.site)).unwrap_or(&0) as i64 >= n,
1621                            Early::Equals(sem, n) => *sems.get(&(sem, plan.site)).unwrap_or(&0) as i64 == n,
1622                        };
1623                        if !checks.is_empty() && checks.iter().all(|(_, e)| holds(e)) {
1624                            for (g, _) in checks {
1625                                if let Micro::Guarded(Guard::Reserve { buf, pages }) = &plan.micro[*g] {
1626                                    fifo_delta.entry(*buf).or_default().1 += *pages as i64;
1627                                }
1628                            }
1629                            Some((checks[checks.len() - 1].0 + 1, 0, lv))
1630                        } else {
1631                            Some((*at, 0, lv))
1632                        }
1633                    } else {
1634                        Some((st.pc, st.ph + 1, None))
1635                    }
1636                }
1637                // A folded effect commits with the op before it; the sequencer never rests on one.
1638                Micro::Then(_) => Some((st.pc + 1, 0, None)),
1639                Micro::LoopStart { var, count, end } => {
1640                    if *count == 0 {
1641                        Some((end + 1, 0, None))
1642                    } else {
1643                        Some((st.pc + 1, 0, Some((*var, 0))))
1644                    }
1645                }
1646                Micro::LoopEnd { var, count, start } => {
1647                    let v = st.lv.get(var).copied().unwrap_or(0) + 1;
1648                    let count = if *var == LV_ITEM {
1649                        plan.item_count
1650                    } else {
1651                        *count
1652                    };
1653                    if v < count {
1654                        Some((start + 1, 0, Some((*var, v))))
1655                    } else {
1656                        Some((st.pc + 1, 0, Some((*var, v))))
1657                    }
1658                }
1659            };
1660            next.push(step);
1661        }
1662        // Phase 2: commit.
1663        for (ei, step) in next.into_iter().enumerate() {
1664            if let Some((pc, ph, lv)) = step {
1665                let st = &mut states[ei];
1666                st.pc = pc;
1667                st.ph = ph;
1668                if let Some((v, x)) = lv {
1669                    st.lv.insert(v, x);
1670                }
1671                if pc >= plans[ei].micro.len() {
1672                    st.done = true;
1673                }
1674            }
1675        }
1676        for (b, (dc, dr)) in fifo_delta {
1677            let f = fifos.get_mut(&b).unwrap();
1678            f.count = (f.count as i64 + dc).max(0) as usize;
1679            f.reserved = (f.reserved as i64 + dr).max(0) as usize;
1680        }
1681        for (k, v) in sem_set {
1682            sems.insert(k, v);
1683        }
1684        for (k, n) in sem_inc {
1685            *sems.entry(k).or_insert(0) += n;
1686        }
1687        cycles += 1;
1688    }
1689    cycles
1690}
1691
1692fn context<'a>(
1693    p: &'a SpatialProgram,
1694    target: &'a SpatialTarget,
1695) -> Result<Ctx<'a>, SpatialLowerError> {
1696    let plan = p
1697        .work
1698        .plan
1699        .ok_or(SpatialLowerError::Unsupported("a tiled program"))?;
1700    let lat = target
1701        .site
1702        .local_mem_read_latency
1703        .ok_or(SpatialLowerError::Unsupported("a fabric target that states its local memory's read latency"))?;
1704    if !p.mem_banks.is_power_of_two() {
1705        return Err(SpatialLowerError::Unsupported("memory banks that are not a power of two"));
1706    }
1707    if !target.site.vector_lanes.is_power_of_two() {
1708        return Err(SpatialLowerError::Unsupported("a site whose vector unit has lanes that are not a power of two"));
1709    }
1710    Ok(Ctx {
1711        p,
1712        te: p.tile.elems(),
1713        tr: p.tile.rows,
1714        tc: p.tile.cols,
1715        grid_nx: plan.grid.1,
1716        plan_acc: plan.acc,
1717        lat: lat as u64,
1718        ranges: program_ranges(p),
1719        banks: p.mem_banks,
1720        lanes: target.site.vector_lanes.min(p.mem_banks),
1721        extent: p.page_extents(),
1722        phase_flags: Default::default(),
1723        halts: halts_of(p)?,
1724        multiplier: target.site.multiplier,
1725        pitch: p.tensors.iter().map(|t| tensor_pitch(p, t)).collect(),
1726        page_pitch: page_pitches(p),
1727    })
1728}
1729
1730/// The halts of `p`'s links: each link after a section another follows halts at that section's rendezvous, past its
1731/// gather. A link before the first section is the host's to serve before the start, one after the last at the end.
1732fn halts_of(p: &SpatialProgram) -> Result<Vec<(SemId, usize)>, SpatialLowerError> {
1733    let mut out = Vec::new();
1734    for (i, l) in p.links.iter().enumerate() {
1735        let Some(k) = l.after.filter(|k| k + 1 < p.sections.len()) else { continue };
1736        let r = p.sections[k]
1737            .barrier_after
1738            .ok_or(SpatialLowerError::Unsupported("a link's halt after a section with no barrier after it"))?;
1739        out.push((r.gather, i));
1740    }
1741    Ok(out)
1742}
1743
1744/// The width of tensor `t`'s words. A tensor the host carries — an `Input` it supplies, a `State` it keeps between
1745/// steps — is held at its own precision, so any word the host writes reads back whole; every other tensor at the width
1746/// of what it holds.
1747fn tensor_width(t: &TensorDecl, ranges: &ProgramRanges) -> u32 {
1748    match t.role {
1749        TensorRole::Input | TensorRole::State => precision_int_bits(t.dtype).unwrap_or(64),
1750        TensorRole::Param | TensorRole::Activation | TensorRole::Output => ranges.tensors[t.id.idx()].map_or(1, signed_bits),
1751    }
1752}
1753
1754/// A memory word's lanes, `(low bit, width)`: one lane up to 32 bits, else 32-bit lanes. A block RAM wider than 32 bits
1755/// maps to the 72-bit simple dual port, whose parity lanes the open 7-series flow wires wrong; no lane ever is.
1756fn lanes(width: u32) -> Vec<(u32, u32)> {
1757    (0..width.div_ceil(32)).map(|l| (l * 32, (width - l * 32).min(32))).collect()
1758}
1759
1760/// Lane `l` of memory `mem` that has `n` lanes.
1761fn lane(mem: &str, l: usize, n: usize) -> String {
1762    if n == 1 {
1763        mem.to_string()
1764    } else {
1765        format!("{mem}_l{l}")
1766    }
1767}
1768
1769/// Bank `b` of memory `mem` that has `banks` banks: the memory itself where it has one.
1770fn bank(mem: &str, b: usize, banks: usize) -> String {
1771    if banks == 1 {
1772        mem.to_string()
1773    } else {
1774        format!("{mem}_b{b}")
1775    }
1776}
1777
1778/// `log2` of a bank count, a power of two.
1779fn log2_banks(banks: usize) -> u32 {
1780    banks.trailing_zeros()
1781}
1782
1783/// The word holding element `addr` — a constant — of memory `mem` of `width` bits in `banks` banks: its lanes, high
1784/// first.
1785fn memory_word(mem: &str, addr: usize, width: u32, banks: usize) -> String {
1786    let m = bank(mem, addr % banks, banks);
1787    let row = addr / banks;
1788    let n = lanes(width).len();
1789    let parts: Vec<String> = (0..n).rev().map(|l| format!("{}[{row}]", lane(&m, l, n))).collect();
1790    format!("{{{}}}", parts.join(", "))
1791}
1792
1793/// The run of up to `banks` elements of `width` bits from `lanes`, a concatenation of `banks` lanes of `from` bits
1794/// each — the lowest the first — every lane brought to `to` bits, sign-extended or cut: the values a run moves hold in
1795/// both widths, so neither changes one.
1796fn resize_lanes(src: &str, from: u32, to: u32, banks: usize) -> String {
1797    let parts: Vec<String> = (0..banks)
1798        .rev()
1799        .map(|j| {
1800            let lo = j as u32 * from;
1801            if to > from {
1802                format!("{{{{{}{{{src}[{}]}}}}, {src}[{}:{lo}]}}", to - from, lo + from - 1, lo + from - 1)
1803            } else {
1804                format!("{src}[{}:{lo}]", lo + to - 1)
1805            }
1806        })
1807        .collect();
1808    format!("{{{}}}", parts.join(", "))
1809}
1810
1811/// The wires that write memory `mem` of `width` bits in `banks` banks, named by `tag`, and the statements that write it:
1812/// `n` elements from element `addr` in a run of `2^lw`, `data`'s low `width` bits the first and `hi` the rest, lane
1813/// after lane. Every bank writes the row `addr` sits at. A run starts at a multiple of its width ([`Ctx::run`]), so its
1814/// element in bank `b` is lane `b mod 2^lw`, in the banks of its word; a single element is a run of one. The port carries
1815/// the run widths `runs` ([`port_widths`]) and no others. With one bank, `data` at `addr`. Under simulation, a run that
1816/// does not start on its word, or of a width the port does not carry, stops the run loudly.
1817#[allow(clippy::too_many_arguments)]
1818fn memory_write(tag: &str, mem: &str, addr: &str, n: &str, lw: &str, data: &str, hi: &str, width: u32, banks: usize, runs: &BTreeSet<usize>) -> (String, String) {
1819    let ls = lanes(width);
1820    let put = |m: &str, a: &str, v: &str| -> String {
1821        ls.iter()
1822            .enumerate()
1823            .map(|(l, (lo, w))| format!("{}[{a}] <= {v}[{}:{lo}];", lane(m, l, ls.len()), lo + w - 1))
1824            .collect::<Vec<_>>()
1825            .join(" ")
1826    };
1827    if banks == 1 {
1828        return (String::new(), put(mem, addr, data));
1829    }
1830    let lg = log2_banks(banks);
1831    let mut wires = format!(
1832        "  wire [31:0] {tag}_a = {addr}; wire [31:0] {tag}_r = {tag}_a >> {lg}; wire [{}:0] {tag}_o = {tag}_a[{}:0]; wire [{}:0] {tag}_v = {data}; wire [{}:0] {tag}_h = {hi};",
1833        lg - 1,
1834        lg - 1,
1835        width - 1,
1836        (banks as u32 - 1) * width - 1
1837    );
1838    let lane_of = |j: usize| if j == 0 { format!("{tag}_v") } else { format!("{tag}_h[{}:{}]", j as u32 * width - 1, (j as u32 - 1) * width) };
1839    // Bank `b` under a run of `2^w`: its lane, and whether the run's word holds it.
1840    let arm = |b: usize, w: u32| -> (String, String) {
1841        let j = b & ((1usize << w) - 1);
1842        let word = if w == lg { "1'b1".to_string() } else { format!("({tag}_o[{}:{w}] == {}'d{})", lg - 1, lg - w, b >> w) };
1843        (lane_of(j), format!("{word} && ({}'d{j} < {n})", lg + 1))
1844    };
1845    let carried: Vec<u32> = runs.iter().map(|w| w.trailing_zeros()).collect();
1846    let mut stmts = Vec::new();
1847    for b in 0..banks {
1848        if let [w] = carried[..] {
1849            let (d, e) = arm(b, w);
1850            wires.push_str(&format!(" wire [{}:0] {tag}_d{b} = {d}; wire {tag}_e{b} = {e};", width - 1));
1851        } else {
1852            let arms: String = carried
1853                .iter()
1854                .map(|w| {
1855                    let (d, e) = arm(b, *w);
1856                    format!(" 4'd{w}: begin {tag}_d{b} = {d}; {tag}_e{b} = {e}; end")
1857                })
1858                .collect();
1859            wires.push_str(&format!(
1860                " reg [{}:0] {tag}_d{b}; reg {tag}_e{b}; always @* case ({lw}){arms} default: begin {tag}_d{b} = {tag}_v; {tag}_e{b} = 1'b0; end endcase",
1861                width - 1
1862            ));
1863        }
1864        stmts.push(format!("if ({tag}_e{b}) begin {} end", put(&bank(mem, b, banks), &format!("{tag}_r"), &format!("{tag}_d{b}"))));
1865    }
1866    let known = carried.iter().map(|w| format!("{lw} == 4'd{w}")).collect::<Vec<_>>().join(" || ");
1867    stmts.push(format!(
1868        "\n// synthesis translate_off\nif (!({known}) || ({tag}_o & ((5'd1 << {lw}) - 5'd1)) != 0) begin $display(\"FATAL: {tag}: a run of 2^%0d from element %0d is not on its word, or not a width the port carries\", {lw}, {tag}_a); $finish; end\n// synthesis translate_on\n"
1869    ));
1870    (wires, stmts.join(" "))
1871}
1872/// The image files of tensor `t`'s memory holding `words` in `banks` banks: one per bank and lane, each bank's rows in
1873/// order, each word at its lane's width.
1874fn tensor_hex(t: &TensorDecl, words: &[i64], width: u32, banks: usize) -> Vec<(String, String)> {
1875    let ls = lanes(width);
1876    let rows = words.len().max(1).div_ceil(banks);
1877    let mut out = Vec::new();
1878    for b in 0..banks {
1879        for (l, (lo, w)) in ls.iter().enumerate() {
1880            let stem = bank(&format!("t{}", t.id.idx()), b, banks);
1881            let name = if ls.len() == 1 { format!("{stem}.hex") } else { format!("{stem}_l{l}.hex") };
1882            let digits = (*w as usize).div_ceil(4);
1883            let mask = if *w >= 64 { u64::MAX } else { (1u64 << w) - 1 };
1884            let mut body = String::with_capacity(rows * (digits + 1));
1885            for r in 0..rows {
1886                let x = words.get(r * banks + b).copied().unwrap_or(0);
1887                body.push_str(&format!("{:0digits$x}\n", ((x as u64) >> lo) & mask));
1888            }
1889            out.push((name, body));
1890        }
1891    }
1892    out
1893}
1894
1895/// The compute-bound floor: the busiest site's compute cycles alone (no fill, no stall, no movement).
1896fn cycle_floor(ctx: &Ctx, plans: &[EnginePlan]) -> u64 {
1897    let mut per_site: BTreeMap<Site, u64> = BTreeMap::new();
1898    for plan in plans {
1899        if plan.engine != Engine::Compute {
1900            continue;
1901        }
1902        let mut total = 0u64;
1903        let mut mult: Vec<u64> = vec![1];
1904        for m in &plan.micro {
1905            match m {
1906                Micro::LoopStart { var, count, .. } => {
1907                    let c = if *var == LV_ITEM {
1908                        plan.item_count
1909                    } else {
1910                        *count
1911                    };
1912                    let top = *mult.last().unwrap();
1913                    mult.push(top * c as u64);
1914                }
1915                Micro::LoopEnd { .. } => {
1916                    mult.pop();
1917                }
1918                Micro::Fixed(f @ (Fixed::Matmul { .. } | Fixed::Unary { .. })) => {
1919                    total += ctx.duration(f) * *mult.last().unwrap()
1920                }
1921                _ => {}
1922            }
1923        }
1924        *per_site.entry(plan.site).or_default() += total;
1925    }
1926    per_site.values().copied().max().unwrap_or(0)
1927}
1928
1929// ── Element layout ────────────────────────────────────────────────────────────────────────────────────────
1930
1931/// The word of a site's image of `t` that holds element `(i, j)` — tiles in row-major order, a row of tiles at the
1932/// tensor's pitch (`tensor_pitch`), each tile's elements row-major within it — and so the address a host writes or
1933/// reads that element at.
1934pub fn host_word(p: &SpatialProgram, t: &TensorDecl, i: usize, j: usize) -> usize {
1935    word_at(p, tensor_pitch(p, t), i, j)
1936}
1937
1938/// [`host_word`] at a pitch of `pitch` tiles a tile-row.
1939fn word_at(p: &SpatialProgram, pitch: usize, i: usize, j: usize) -> usize {
1940    let (tr, tc) = (p.tile.rows, p.tile.cols);
1941    let tile = (i / tr) * pitch + j / tc;
1942    tile * tr * tc + (i % tr) * tc + j % tc
1943}
1944
1945/// The words a memory of tensor `t` holds: its tile rows, each `pitch` tiles ([`tensor_pitch`]) of `te` elements.
1946fn tensor_words(t: &TensorDecl, pitch: usize, te: usize) -> usize {
1947    t.tiles.rows_t * pitch * te
1948}
1949
1950/// Tile-major element order of a tensor's padded tile grid (the memory image every site holds), its rows at the tensor's
1951/// pitch.
1952fn tiled_words(
1953    p: &SpatialProgram,
1954    t: &TensorDecl,
1955    values: &dyn Fn(usize, usize) -> i64,
1956) -> Vec<i64> {
1957    let pitch = tensor_pitch(p, t);
1958    let mut out = vec![0i64; tensor_words(t, pitch, p.tile.elems())];
1959    for i in 0..t.elems.rows {
1960        for j in 0..t.elems.cols {
1961            out[word_at(p, pitch, i, j)] = values(i, j);
1962        }
1963    }
1964    out
1965}
1966
1967// ── Row pitch ──────────────────────────────────────────────────────────────────────────────────────────────
1968
1969/// The word a row of `width` elements moves in on a site of `banks` banks: the narrowest power of two that holds the row,
1970/// no wider than the banks.
1971fn row_word(width: usize, banks: usize) -> usize {
1972    width.next_power_of_two().min(banks).max(1)
1973}
1974
1975/// ★ A ROW STARTS ON A WORD. A run is aligned to its own width ([`Ctx::align`]), so a row of `w` elements laid end to end
1976/// after another row of `w` starts wherever `w` leaves it and moves in runs no wider than `w`'s alignment: the decode
1977/// step's tied head, sixteen rows of fourteen, moves two elements a cycle. A memory that keeps each row at a pitch of
1978/// whole words — the row's width rounded up to the word it moves in ([`row_word`]) — moves every row in runs of that
1979/// word; the padding past a row's end is never read.
1980///
1981/// The tiles a tile-row of tensor `t` takes in its memory: its columns, or — on a site of one-element tiles and more than
1982/// one bank, for a tensor of more than one row whose rows are not whole words — its columns rounded up to the word, where
1983/// every walk the program makes of the tensor keeps within a row ([`walk_splits`]), so that each address it issues has a
1984/// row and a column the pitch can place. A tensor no walk reads or writes takes the pitch too: only the host addresses it,
1985/// through [`host_word`].
1986pub(crate) fn tensor_pitch(p: &SpatialProgram, t: &TensorDecl) -> usize {
1987    let cols = t.tiles.cols_t;
1988    let word = row_word(cols, p.mem_banks);
1989    if p.tile.elems() != 1 || p.mem_banks <= 1 || t.tiles.rows_t <= 1 || cols % word == 0 {
1990        return cols;
1991    }
1992    let splits = tensor_walks(p).into_iter().all(|(op, loops, section)| match op {
1993        ChoreoOp::ReadTiles { tensor, at, block_h, block_w, stride_w, stride_h, .. }
1994        | ChoreoOp::WriteTiles { tensor, at, block_h, block_w, stride_w, stride_h, .. } => {
1995            let coords: Vec<(usize, usize)> = p
1996                .work
1997                .items
1998                .iter()
1999                .filter(|it| section.is_none() || p.section_of_item(it.id) == section)
2000                .map(|it| it.coord)
2001                .collect();
2002            *tensor != t.id || walk_splits(at, (*block_h, *block_w, *stride_w, *stride_h), &loops, &coords, cols)
2003        }
2004        _ => true,
2005    });
2006    if splits {
2007        cols.next_multiple_of(word)
2008    } else {
2009        cols
2010    }
2011}
2012
2013/// Every tile walk the program's engines make — each read and write of a tensor — with the counts of the loops that
2014/// enclose it and the section it belongs to.
2015fn tensor_walks(p: &SpatialProgram) -> Vec<(&ChoreoOp, BTreeMap<LoopVar, usize>, Option<usize>)> {
2016    fn visit<'a>(
2017        p: &SpatialProgram,
2018        ops: &'a [TaggedOp],
2019        loops: &mut Vec<(LoopVar, usize)>,
2020        out: &mut Vec<(&'a ChoreoOp, BTreeMap<LoopVar, usize>, Option<usize>)>,
2021    ) {
2022        for t in ops {
2023            match &t.op {
2024                ChoreoOp::ReadTiles { .. } | ChoreoOp::WriteTiles { .. } => {
2025                    out.push((&t.op, loops.iter().copied().collect(), p.section_of_stage(t.stage)))
2026                }
2027                ChoreoOp::Loop { var, count, body } => {
2028                    loops.push((*var, count.bound()));
2029                    visit(p, body, loops, out);
2030                    loops.pop();
2031                }
2032                _ => {}
2033            }
2034        }
2035    }
2036    let mut out = Vec::new();
2037    for prog in p.schedule.iter().flat_map(|s| &s.programs) {
2038        visit(p, &prog.ops, &mut Vec::new(), &mut out);
2039    }
2040    out
2041}
2042
2043/// Whether a walk from `at` — `block_h` rows `stride_h` apart, of `block_w` tiles `stride_w` apart — of a tensor of `cols`
2044/// tile columns keeps every address it issues within one row: each term's stride is whole rows and a column offset, and
2045/// the column offsets its terms add up to stay below `cols` at the largest value each term takes — a loop's count less
2046/// one, the largest of the items' `coords` (the section's work items), a gather's bound. A term of a variable no loop in
2047/// `loops` binds adds nothing, as it adds nothing to the address the walk issues ([`addr_v`]).
2048fn walk_splits(
2049    at: &TileAddr,
2050    (block_h, block_w, stride_w, stride_h): (usize, usize, usize, usize),
2051    loops: &BTreeMap<LoopVar, usize>,
2052    coords: &[(usize, usize)],
2053    cols: usize,
2054) -> bool {
2055    let col = |s: usize| s % cols;
2056    let mut reach = col(at.base) + block_h.saturating_sub(1) * col(stride_h) + block_w.saturating_sub(1) * col(stride_w);
2057    for (v, s) in &at.steps {
2058        let top = if *v == LV_ITEM_Y {
2059            coords.iter().map(|c| c.0).max().unwrap_or(0)
2060        } else if *v == LV_ITEM_X {
2061            coords.iter().map(|c| c.1).max().unwrap_or(0)
2062        } else {
2063            loops.get(v).map_or(0, |c| c.saturating_sub(1))
2064        };
2065        reach += top * col(*s);
2066    }
2067    if let Some(ind) = at.indirect {
2068        reach += ind.bound.1 / ind.elems_per_tile.max(1) * col(ind.tile_stride);
2069    }
2070    reach < cols
2071}
2072
2073/// Tile offset `s` of a tensor of `cols` tile columns, in a memory that keeps its rows `pitch` tiles apart: its whole rows
2074/// at the pitch, its column kept.
2075fn pitched(s: usize, cols: usize, pitch: usize) -> usize {
2076    s / cols * pitch + s % cols
2077}
2078
2079/// A walk's address at a tensor's pitch: its base, every term's stride and a gather's tile stride each placed by
2080/// [`pitched`] — exact for a walk that keeps within a row ([`walk_splits`]).
2081fn pitched_addr(at: &TileAddr, cols: usize, pitch: usize) -> TileAddr {
2082    TileAddr {
2083        base: pitched(at.base, cols, pitch),
2084        steps: at.steps.iter().map(|(v, s)| (*v, pitched(*s, cols, pitch))).collect(),
2085        indirect: at.indirect.map(|ind| IndirectTile { tile_stride: pitched(ind.tile_stride, cols, pitch), ..ind }),
2086    }
2087}
2088
2089/// ★ A PAGE'S ROWS START ON WORDS TOO. A matmul's B page holds its `ibw` rows of the site's columns end to end, so a row
2090/// that is not a whole word moves in its alignment's runs into the page and out of it. Each buffer template whose pages
2091/// a tile walk of contiguous rows fills and a matmul reads only as its B — every fill and every read rows of one width,
2092/// more than one row a page, the width not a whole word — keeps its pages' rows at that width rounded up to the word
2093/// ([`row_word`]); every other page keeps its rows end to end. The template's row width and pitch, by template.
2094fn page_pitches(p: &SpatialProgram) -> BTreeMap<BufferId, (usize, usize)> {
2095    #[derive(Default)]
2096    struct Use {
2097        widths: BTreeSet<usize>,
2098        rows: usize,
2099        other: bool,
2100    }
2101    fn visit(p: &SpatialProgram, ops: &[TaggedOp], uses: &mut BTreeMap<BufferId, Use>) {
2102        let tp = |b: BufferId| p.buffer(b).origin.unwrap_or(b);
2103        let other = |b: BufferId, uses: &mut BTreeMap<BufferId, Use>| uses.entry(tp(b)).or_default().other = true;
2104        for t in ops {
2105            match &t.op {
2106                ChoreoOp::Reserve { .. } | ChoreoOp::Push { .. } | ChoreoOp::Wait { .. } | ChoreoOp::Pop { .. } => {}
2107                ChoreoOp::ReadTiles { at, block_h, block_w, stride_w, into, .. } => {
2108                    let u = uses.entry(tp(*into)).or_default();
2109                    u.widths.insert(*block_w);
2110                    u.rows = u.rows.max(*block_h);
2111                    u.other |= *stride_w != 1;
2112                    if let Some(ind) = at.indirect {
2113                        other(ind.index, uses);
2114                    }
2115                }
2116                ChoreoOp::WriteTiles { from, at, .. } => {
2117                    other(*from, uses);
2118                    if let Some(ind) = at.indirect {
2119                        other(ind.index, uses);
2120                    }
2121                }
2122                ChoreoOp::Compute { op, ins, acc_reload } => match op.shape() {
2123                    Ok(ComputeShape::Tiles { block, sub_blocks, .. }) if ins.len() == 2 => {
2124                        other(ins[0], uses);
2125                        let u = uses.entry(tp(ins[1])).or_default();
2126                        u.widths.insert(sub_blocks.1 * block.osw);
2127                        u.rows = u.rows.max(block.ibw);
2128                        if let Some(r) = acc_reload {
2129                            other(*r, uses);
2130                        }
2131                    }
2132                    _ => {
2133                        for b in ins.iter().chain(acc_reload) {
2134                            other(*b, uses);
2135                        }
2136                    }
2137                },
2138                ChoreoOp::PackAcc { to, .. } => other(*to, uses),
2139                ChoreoOp::McastSend { buf, .. } => other(*buf, uses),
2140                ChoreoOp::Loop { body, .. } => visit(p, body, uses),
2141                ChoreoOp::ReadBarrier
2142                | ChoreoOp::WriteBarrier
2143                | ChoreoOp::McastSetValid { .. }
2144                | ChoreoOp::Flush { .. }
2145                | ChoreoOp::SemSet { .. }
2146                | ChoreoOp::SemInc { .. }
2147                | ChoreoOp::SemWait { .. }
2148                | ChoreoOp::AcquireAcc { .. }
2149                | ChoreoOp::ReleaseAcc
2150                | ChoreoOp::Barrier { .. } => {}
2151            }
2152        }
2153    }
2154    let mut uses: BTreeMap<BufferId, Use> = BTreeMap::new();
2155    if p.tile.elems() != 1 || p.mem_banks <= 1 {
2156        return BTreeMap::new();
2157    }
2158    for prog in p.schedule.iter().flat_map(|s| &s.programs) {
2159        visit(p, &prog.ops, &mut uses);
2160    }
2161    for st in p.stages.iter().filter(|st| matches!(st.kind, StageKind::Recv { .. })) {
2162        for b in &st.outs {
2163            uses.entry(p.buffer(*b).origin.unwrap_or(*b)).or_default().other = true;
2164        }
2165    }
2166    let extents = p.page_extents();
2167    uses.into_iter()
2168        .filter_map(|(tpl, u)| {
2169            let [w] = u.widths.iter().copied().collect::<Vec<_>>()[..] else { return None };
2170            let word = row_word(w, p.mem_banks);
2171            let whole_rows = p.buffers.iter().filter(|b| b.origin.unwrap_or(b.id) == tpl).all(|b| extents[b.id.idx()] % w == 0);
2172            (!u.other && u.rows > 1 && w % word != 0 && whole_rows).then(|| (tpl, (w, w.next_multiple_of(word))))
2173        })
2174        .collect()
2175}
2176
2177/// `$readmemh` files for the program's input tensors, in the sites' tile-major element order.
2178pub fn input_hex_files(p: &SpatialProgram, inputs: &Inputs) -> Vec<(String, String)> {
2179    let ranges = program_ranges(p);
2180    let mut out = Vec::new();
2181    for t in &p.tensors {
2182        if t.role == TensorRole::Output {
2183            continue;
2184        }
2185        let Some(v) = inputs.tensors.get(&t.id) else {
2186            continue;
2187        };
2188        let cols = t.elems.cols;
2189        let words = match v {
2190            Value::Int(x) => tiled_words(p, t, &|i, j| x.get(i * cols + j).copied().unwrap_or(0)),
2191            Value::Float(x) => tiled_words(p, t, &|i, j| {
2192                x.get(i * cols + j).copied().unwrap_or(0.0) as i64
2193            }),
2194        };
2195        out.extend(tensor_hex(t, &words, tensor_width(t, &ranges), p.mem_banks));
2196    }
2197    out
2198}
2199
2200// ── The printer ───────────────────────────────────────────────────────────────────────────────────────────
2201
2202fn bits_for(n: usize) -> usize {
2203    (usize::BITS - n.max(2).saturating_sub(1).leading_zeros()).max(1) as usize
2204}
2205
2206fn var_name(v: LoopVar) -> String {
2207    format!("lv{}", v.0)
2208}
2209
2210/// The phase `by` cycles behind the FSM's: `ph − by`, or `ph` itself.
2211fn ph_less(by: u64) -> String {
2212    if by == 0 {
2213        "ph".into()
2214    } else {
2215        format!("(ph - {by})")
2216    }
2217}
2218
2219/// `body` from phase `from` on: guarded by the flag of `ph >= from`, or unguarded from phase 0.
2220fn from_phase(ctx: &Ctx, from: u64, body: &str) -> String {
2221    if from == 0 {
2222        body.to_string()
2223    } else {
2224        format!("if ({}) begin {body} end", ctx.ph_ge(from))
2225    }
2226}
2227
2228/// The tile index of an address in the FSM's registers.
2229/// A tile address: its base, each loop or item coordinate times its stride, and — for a gather — the evaluator's
2230/// `indirect_offset`, `(v div elems_per_tile) · tile_stride` for the index `v` at element 0 of its buffer's front page.
2231fn addr_v(at: &TileAddr, bound: &BTreeSet<LoopVar>, (oy, ox): (&str, &str), ctx: &Ctx) -> String {
2232    let mut terms = vec![format!("{}", at.base)];
2233    if let Some(ind) = at.indirect {
2234        let ki = ctx.template(ind.index).idx();
2235        terms.push(format!(
2236            "(($unsigned(f{ki}_rd_data) / {}) * {})",
2237            ind.elems_per_tile.max(1),
2238            ind.tile_stride
2239        ));
2240    }
2241    for (v, stride) in &at.steps {
2242        if *stride == 0 {
2243            continue;
2244        }
2245        let name = if *v == LV_ITEM_Y {
2246            oy.to_string()
2247        } else if *v == LV_ITEM_X {
2248            ox.to_string()
2249        } else if bound.contains(v) {
2250            var_name(*v)
2251        } else {
2252            continue;
2253        };
2254        terms.push(format!("{name} * {stride}"));
2255    }
2256    terms.join(" + ")
2257}
2258
2259struct Emit {
2260    s: String,
2261}
2262
2263impl Emit {
2264    fn l(&mut self, line: impl AsRef<str>) {
2265        self.s.push_str(line.as_ref());
2266        self.s.push('\n');
2267    }
2268}
2269
2270/// Every buffer an engine touches, with the roles it takes.
2271#[derive(Default)]
2272struct BufUse {
2273    /// Reserves and pushes: the buffer's producer drives its reserve / push wires.
2274    produces: BTreeSet<BufferId>,
2275    /// Waits and pops: the consumer drives its pop wires.
2276    consumes: BTreeSet<BufferId>,
2277    writes: BTreeSet<BufferId>,
2278    reads: BTreeSet<BufferId>,
2279}
2280
2281impl BufUse {
2282    fn all(&self) -> BTreeSet<BufferId> {
2283        self.produces
2284            .iter()
2285            .chain(&self.consumes)
2286            .chain(&self.writes)
2287            .chain(&self.reads)
2288            .copied()
2289            .collect()
2290    }
2291}
2292
2293fn buffer_uses(micro: &[Micro]) -> BufUse {
2294    let mut u = BufUse::default();
2295    for m in micro {
2296        match m {
2297            Micro::Guarded(Guard::Reserve { buf, .. }) => {
2298                u.produces.insert(*buf);
2299            }
2300            Micro::Guarded(Guard::Wait { buf, .. }) => {
2301                u.consumes.insert(*buf);
2302            }
2303            Micro::Fixed(f) | Micro::Then(f) => match f {
2304                Fixed::Push { buf, .. } => {
2305                    u.produces.insert(*buf);
2306                }
2307                Fixed::Pop { buf, .. } => {
2308                    u.consumes.insert(*buf);
2309                }
2310                Fixed::ReadTiles { into, at, .. } => {
2311                    u.writes.insert(*into);
2312                    if let Some(ind) = at.indirect {
2313                        u.reads.insert(ind.index);
2314                    }
2315                }
2316                Fixed::WriteTiles { from, at, .. } => {
2317                    u.reads.insert(*from);
2318                    if let Some(ind) = at.indirect {
2319                        u.reads.insert(ind.index);
2320                    }
2321                }
2322                Fixed::Matmul { ins, reload, .. } => {
2323                    u.reads.insert(ins[0]);
2324                    u.reads.insert(ins[1]);
2325                    if let Some(r) = reload {
2326                        u.reads.insert(*r);
2327                    }
2328                }
2329                Fixed::Unary { ins, .. } | Fixed::Generic { ins, .. } => {
2330                    for b in ins {
2331                        u.reads.insert(*b);
2332                    }
2333                }
2334                Fixed::PackAcc { to, accumulate, .. } => {
2335                    u.writes.insert(*to);
2336                    if *accumulate {
2337                        // Reading the page it adds into.
2338                        u.reads.insert(*to);
2339                    }
2340                }
2341                Fixed::McastSend { buf, .. } => {
2342                    u.reads.insert(*buf);
2343                }
2344                _ => {}
2345            },
2346            _ => {}
2347        }
2348    }
2349    u
2350}
2351
2352/// A memory port an op moves data through.
2353#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2354enum Port {
2355    FifoWr(BufferId),
2356    FifoRd(BufferId),
2357    TensorWr(TensorId),
2358    TensorRd(TensorId),
2359}
2360
2361/// ★ THE RUN WIDTHS EACH PORT CARRIES: the width of every run an op of `micro` moves through it ([`Ctx::run`],
2362/// [`Ctx::acc_run`], [`Ctx::matmul_run`]), and one for every op that moves single elements. A port's lane logic covers
2363/// exactly these: a memory only element ops touch keeps no lanes, and one whose runs are whole words rotates nothing.
2364fn port_widths(ctx: &Ctx, micro: &[Micro]) -> BTreeMap<Port, BTreeSet<usize>> {
2365    fn put(out: &mut BTreeMap<Port, BTreeSet<usize>>, port: Port, w: usize) {
2366        out.entry(port).or_default().insert(w);
2367    }
2368    let mut out = BTreeMap::new();
2369    for m in micro {
2370        let (Micro::Fixed(f) | Micro::Then(f)) = m else { continue };
2371        match f {
2372            Fixed::ReadTiles { tensor, at, into, .. } => {
2373                let w = ctx.run(f);
2374                put(&mut out, Port::TensorRd(*tensor), w);
2375                put(&mut out, Port::FifoWr(*into), w);
2376                if let Some(ind) = at.indirect {
2377                    put(&mut out, Port::FifoRd(ind.index), 1);
2378                }
2379            }
2380            Fixed::WriteTiles { from, tensor, at, .. } => {
2381                let w = ctx.run(f);
2382                put(&mut out, Port::FifoRd(*from), w);
2383                put(&mut out, Port::TensorWr(*tensor), w);
2384                if let Some(ind) = at.indirect {
2385                    put(&mut out, Port::FifoRd(ind.index), 1);
2386                }
2387            }
2388            Fixed::PackAcc { to, accumulate, .. } => {
2389                let w = ctx.acc_run(f);
2390                put(&mut out, Port::FifoWr(*to), w);
2391                if *accumulate {
2392                    put(&mut out, Port::FifoRd(*to), w);
2393                }
2394            }
2395            Fixed::Matmul { block, per_site_n, sub_rows, ins, reload, .. } => {
2396                let w = ctx.matmul_run(block, ctx.page_row(ins[1], *per_site_n), *per_site_n, *sub_rows);
2397                for b in ins.iter().chain(reload) {
2398                    put(&mut out, Port::FifoRd(*b), w);
2399                }
2400            }
2401            Fixed::Unary { ins, .. } => {
2402                for b in ins {
2403                    put(&mut out, Port::FifoRd(*b), 1);
2404                }
2405            }
2406            // A Generic slot reads a run of its lanes a point where its map moves along the lane axis, else one element.
2407            Fixed::Generic { unit, ins, .. } => {
2408                for (s, b) in ins.iter().enumerate() {
2409                    put(&mut out, Port::FifoRd(*b), if unit.broadcast(s) || unit.gathers(s) { 1 } else { unit.lanes });
2410                }
2411            }
2412            Fixed::McastSend { buf, .. } => put(&mut out, Port::FifoRd(*buf), 1),
2413            Fixed::Push { .. }
2414            | Fixed::Pop { .. }
2415            | Fixed::Barrier
2416            | Fixed::AcquireAcc { .. }
2417            | Fixed::ReleaseAcc
2418            | Fixed::SemSet { .. }
2419            | Fixed::SemInc { .. }
2420            | Fixed::McastSetValid { .. } => {}
2421        }
2422    }
2423    out
2424}
2425
2426/// Each reading engine's port on each buffer's FIFO at `site`, numbered in engine order per template, and the ports each
2427/// template has there. An engine whose vector Generic ops gather from a buffer takes a port for each lane past the first
2428/// beside its own, numbered after it ([`gather_lanes`]).
2429fn read_ports(ctx: &Ctx, plans: &[EnginePlan], site: Site) -> (BTreeMap<(usize, BufferId), usize>, BTreeMap<BufferId, usize>) {
2430    let mut readers: BTreeMap<BufferId, usize> = BTreeMap::new();
2431    let mut index: BTreeMap<(usize, BufferId), usize> = BTreeMap::new();
2432    for (ei, plan) in plans.iter().enumerate().filter(|(_, pl)| pl.site == site) {
2433        let lanes = gather_lanes(ctx, &plan.micro);
2434        for b in buffer_uses(&plan.micro).reads {
2435            let n = readers.entry(ctx.template(b)).or_default();
2436            index.insert((ei, b), *n);
2437            *n += lanes.get(&ctx.template(b)).copied().unwrap_or(1);
2438        }
2439    }
2440    (index, readers)
2441}
2442
2443/// ★ A GATHER TAKES LANES THROUGH PORTS OF ITS OWN. Each buffer template a vector Generic op of `micro` gathers from, with
2444/// the most lanes any such op takes: lane 0 reads the page through the engine's port, and every lane past it through a
2445/// port of its own, each at the element its own lane of the index slot names.
2446pub(super) fn gather_lanes(ctx: &Ctx, micro: &[Micro]) -> BTreeMap<BufferId, usize> {
2447    let mut out: BTreeMap<BufferId, usize> = BTreeMap::new();
2448    for m in micro {
2449        let Micro::Fixed(Fixed::Generic { unit, ins, .. }) = m else { continue };
2450        if unit.lanes > 1 {
2451            for s in (0..unit.slots()).filter(|s| unit.gathers(*s)) {
2452                let lanes = out.entry(ctx.template(ins[s])).or_insert(1);
2453                *lanes = (*lanes).max(unit.lanes);
2454            }
2455        }
2456    }
2457    out
2458}
2459
2460/// The widths of `port` over the engines `ei` of `widths`, or a single element where nothing moves runs through it.
2461fn widths_or_element(found: BTreeSet<usize>) -> BTreeSet<usize> {
2462    if found.is_empty() {
2463        BTreeSet::from([1])
2464    } else {
2465        found
2466    }
2467}
2468
2469fn tensors_read(micro: &[Micro]) -> BTreeSet<TensorId> {
2470    micro
2471        .iter()
2472        .filter_map(|m| {
2473            if let Micro::Fixed(Fixed::ReadTiles { tensor, .. }) = m {
2474                Some(*tensor)
2475            } else {
2476                None
2477            }
2478        })
2479        .collect()
2480}
2481fn tensors_written(micro: &[Micro]) -> BTreeSet<TensorId> {
2482    micro
2483        .iter()
2484        .filter_map(|m| {
2485            if let Micro::Fixed(Fixed::WriteTiles { tensor, .. }) = m {
2486                Some(*tensor)
2487            } else {
2488                None
2489            }
2490        })
2491        .collect()
2492}
2493/// Whether an engine halts for the host: its program holds a link's halt.
2494fn halts_in(micro: &[Micro]) -> bool {
2495    micro.iter().any(|m| matches!(m, Micro::Guarded(Guard::Link { .. })))
2496}
2497fn sems_local(micro: &[Micro]) -> BTreeSet<SemId> {
2498    let mut s = BTreeSet::new();
2499    for m in micro {
2500        match m {
2501            Micro::Guarded(Guard::SemWait { sem, .. })
2502            | Micro::Fixed(Fixed::SemSet { sem, .. })
2503            | Micro::Then(Fixed::SemSet { sem, .. }) => {
2504                s.insert(*sem);
2505            }
2506            _ => {}
2507        }
2508    }
2509    s
2510}
2511fn sem_incs(micro: &[Micro]) -> BTreeSet<SemId> {
2512    micro
2513        .iter()
2514        .filter_map(|m| {
2515            if let Micro::Fixed(Fixed::SemInc { sem, .. }) | Micro::Then(Fixed::SemInc { sem, .. }) = m {
2516                Some(*sem)
2517            } else {
2518                None
2519            }
2520        })
2521        .collect()
2522}
2523fn mcast_valids(micro: &[Micro]) -> BTreeSet<(SemId, GroupId)> {
2524    micro
2525        .iter()
2526        .filter_map(|m| {
2527            if let Micro::Fixed(Fixed::McastSetValid { sem, group }) | Micro::Then(Fixed::McastSetValid { sem, group }) = m {
2528                Some((*sem, *group))
2529            } else {
2530                None
2531            }
2532        })
2533        .collect()
2534}
2535fn mcast_sends(micro: &[Micro]) -> BTreeSet<GroupId> {
2536    micro
2537        .iter()
2538        .filter_map(|m| {
2539            if let Micro::Fixed(Fixed::McastSend { group, .. }) = m {
2540                Some(*group)
2541            } else {
2542                None
2543            }
2544        })
2545        .collect()
2546}
2547fn loop_vars(micro: &[Micro]) -> BTreeSet<LoopVar> {
2548    micro
2549        .iter()
2550        .filter_map(|m| {
2551            if let Micro::LoopStart { var, .. } = m {
2552                Some(*var)
2553            } else {
2554                None
2555            }
2556        })
2557        .collect()
2558}
2559
2560/// The width a PE column's partial sum needs: a product of two `bits`-wide signed operands fits `2·bits` bits — the
2561/// largest, `(−2^(bits−1))²`, is `2^(2·bits−2)` — and a pass sums at most `k` of them, the array's rows. A sum across
2562/// passes accumulates in the engine's own 64-bit accumulator, never in the array.
2563fn pe_acc_bits(bits: u32, k: usize) -> u32 {
2564    (2 * bits + (usize::BITS - k.max(1).saturating_sub(1).leading_zeros())).min(64)
2565}
2566
2567/// The PE array an engine's matmuls run on — the largest of them, `(rows, columns, operand bits)` — if it has any.
2568fn pe_array(micro: &[Micro]) -> Option<(usize, usize, u32)> {
2569    micro.iter().fold(None, |acc, m| match m {
2570        Micro::Fixed(Fixed::Matmul { operand, array, .. }) => {
2571            let bits = precision_int_bits(*operand).unwrap_or(64);
2572            Some((array.0, array.1, acc.map_or(bits, |a: (usize, usize, u32)| a.2.max(bits))))
2573        }
2574        _ => acc,
2575    })
2576}
2577
2578/// A composition's sections each number their own work items, and an op's item coordinate is the coordinate of the
2579/// item of ITS section the item loop is at — the evaluator's `item_coord`. The tables that hold them are one site's
2580/// items, so a composition is lowered onto one site.
2581fn require_composition_on_one_site(p: &SpatialProgram) -> Result<(), SpatialLowerError> {
2582    if !p.sections.is_empty() && p.sites().len() > 1 {
2583        return Err(SpatialLowerError::Unsupported(
2584            "a composition across more than one site — its item coordinates are decoded per section from one site's items",
2585        ));
2586    }
2587    Ok(())
2588}
2589
2590/// The read of a gather's index — element 0 of the front page of its buffer — that a tile address with an indirect term
2591/// drives in the same cycle, or nothing.
2592fn index_read(ctx: &Ctx, at: &TileAddr) -> String {
2593    match at.indirect {
2594        Some(ind) => {
2595            let ki = ctx.template(ind.index).idx();
2596            format!(" f{ki}_rd_slot = f{ki}_rd_slot0; f{ki}_rd_elem = 0;")
2597        }
2598        None => String::new(),
2599    }
2600}
2601
2602/// The coordinate expressions an op's tile address reads: an uncomposed program's `oy`/`ox` (the item decoded on the
2603/// plan's grid), or a section's table looked up at the item loop.
2604struct CoordNames(String, String);
2605
2606impl CoordNames {
2607    fn as_tuple(&self) -> (&str, &str) {
2608        (&self.0, &self.1)
2609    }
2610}
2611
2612fn item_coord_names(section: Option<usize>) -> CoordNames {
2613    match section {
2614        Some(k) => CoordNames(format!("item_y_s{k}(lv5_or_zero)"), format!("item_x_s{k}(lv5_or_zero)")),
2615        None => CoordNames("oy".into(), "ox".into()),
2616    }
2617}
2618
2619// ── The Generic engine's literals and folds ─────────────────────────────────────────────────────────────
2620
2621/// A signed 64-bit literal.
2622fn lit64(v: i64) -> String {
2623    if v < 0 {
2624        format!("(-64'sd{})", v.unsigned_abs())
2625    } else {
2626        format!("64'sd{v}")
2627    }
2628}
2629
2630/// One step of a fold, as the evaluator's `IntFold` takes it: seeded by the first value along the axis (`first`),
2631/// `Sum` adds, `Max` keeps the larger, `ArgMax` keeps the first strictly greater and its position `at`. Returns the
2632/// value and position the fold holds after the step.
2633fn fold_step(pool: ReducePool, first: &str, val_reg: &str, idx_reg: &str, v: &str, at: &str) -> (String, String) {
2634    match pool {
2635        ReducePool::Sum => (format!("({first} ? {v} : ($signed({val_reg}) + {v}))"), "32'd0".into()),
2636        ReducePool::Max => (
2637            format!("({first} ? {v} : (($signed({v}) > $signed({val_reg})) ? {v} : $signed({val_reg})))"),
2638            "32'd0".into(),
2639        ),
2640        ReducePool::ArgMax => {
2641            let take = format!("({first} || ($signed({v}) > $signed({val_reg})))");
2642            (format!("({take} ? {v} : $signed({val_reg}))"), format!("({take} ? {at} : {idx_reg})"))
2643        }
2644    }
2645}
2646
2647/// A memory of `words` elements of `width` bits in `banks` banks — element `e` in bank `e mod banks`, at row
2648/// `e div banks` — each bank in [`lanes`], read `lat` cycles after its address. Read through a registered port (`lat`
2649/// one or more), each bank takes the store that wastes the least of the part: a bank of one row is a register; a banked
2650/// memory's bank up to 64 rows is distributed RAM, 32 or 64 rows of a few bits a LUT, where a flop array of even four
2651/// rows costs more LUTs in its read mux than the RAM does; past 64 rows, and an unbanked memory of more than a few
2652/// words, block RAM. A bank read combinationally (`lat` zero) is a flop array.
2653/// How a bank of a memory is held, the choice `memory_decl` makes by yosys's `ram_style`: flip-flops with no
2654/// attribute when its read is unregistered, else registers for a shallow bank, distributed LUT-RAM for a bank up
2655/// to 64 rows deep across more than one bank, and block RAM otherwise. The budget reads the same choice per bank,
2656/// so the resources it counts are the ones the printer declares.
2657#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2658enum Storage {
2659    Flops,
2660    Registers,
2661    Distributed,
2662    Block,
2663}
2664
2665impl Storage {
2666    /// The storage a memory of `words` elements in `banks` banks, registered `lat` times, is printed in.
2667    fn of(words: usize, banks: usize, lat: u64) -> Storage {
2668        let rows = words.max(1).div_ceil(banks);
2669        if lat == 0 {
2670            Storage::Flops
2671        } else if rows <= 1 || (banks == 1 && rows <= 4) {
2672            Storage::Registers
2673        } else if rows <= 64 && banks > 1 {
2674            Storage::Distributed
2675        } else {
2676            Storage::Block
2677        }
2678    }
2679
2680    /// The `(* ram_style = … *)` prefix the declaration carries, exactly as printed.
2681    fn attr(self) -> &'static str {
2682        match self {
2683            Storage::Flops => "",
2684            Storage::Registers => "(* ram_style = \"registers\" *) ",
2685            Storage::Distributed => "(* ram_style = \"distributed\" *) ",
2686            Storage::Block => "(* ram_style = \"block\" *) ",
2687        }
2688    }
2689}
2690
2691fn memory_decl(name: &str, words: usize, width: u32, banks: usize, lat: u64) -> String {
2692    let rows = words.max(1).div_ceil(banks);
2693    let style = Storage::of(words, banks, lat).attr();
2694    let ls = lanes(width);
2695    let mut out = Vec::new();
2696    for b in 0..banks {
2697        for (l, (_, w)) in ls.iter().enumerate() {
2698            out.push(format!("  {style}reg [{}:0] {} [0:{}];", w - 1, lane(&bank(name, b, banks), l, ls.len()), rows - 1));
2699        }
2700    }
2701    out.join("\n")
2702}
2703
2704/// The read port `data` of memory `mem` of `width` bits in `banks` banks, at element `addr`, registered `lat` times:
2705/// `data`, the element sign-extended to 64 bits, and — with more than one bank — `data_hi`, the elements after it in a
2706/// run, lane after lane. Every bank reads the row `addr` sits at; the element is its own bank's, and a run, which starts
2707/// at a multiple of its width ([`Ctx::run`]), takes its lanes from the banks of its word. The port carries the run widths
2708/// `runs` ([`port_widths`]).
2709fn memory_read(data: &str, mem: &str, addr: &str, width: u32, banks: usize, lat: u64, runs: &BTreeSet<usize>) -> String {
2710    let ls = lanes(width);
2711    let read = |m: &str, row: &str, q: &str| -> String {
2712        let mut s = String::new();
2713        if lat == 0 {
2714            let parts: Vec<String> = (0..ls.len()).rev().map(|l| format!("{}[{row}]", lane(m, l, ls.len()))).collect();
2715            s.push_str(&format!(" wire [{}:0] {q} = {{{}}};", width - 1, parts.join(", ")));
2716            return s;
2717        }
2718        for (l, (_, w)) in ls.iter().enumerate() {
2719            s.push_str(&format!(" reg [{}:0] {q}_l{l}_q0; always @(posedge clk) {q}_l{l}_q0 <= {}[{row}];", w - 1, lane(m, l, ls.len())));
2720            for n in 1..lat {
2721                s.push_str(&format!(" reg [{0}:0] {q}_l{l}_q{n}; always @(posedge clk) {q}_l{l}_q{n} <= {q}_l{l}_q{1};", w - 1, n - 1));
2722            }
2723        }
2724        let parts: Vec<String> = (0..ls.len()).rev().map(|l| format!("{q}_l{l}_q{}", lat - 1)).collect();
2725        s.push_str(&format!(" wire [{}:0] {q} = {{{}}};", width - 1, parts.join(", ")));
2726        s
2727    };
2728    if banks == 1 {
2729        let s = format!("{} wire [63:0] {data} = $signed({data}_w);", read(mem, addr, &format!("{data}_w")));
2730        return format!("  {}", s.trim_start());
2731    }
2732    let lg = log2_banks(banks);
2733    let mut s = format!("  wire [31:0] {data}_a = {addr}; wire [31:0] {data}_r = {data}_a >> {lg}; wire [{}:0] {data}_o0 = {data}_a[{}:0];", lg - 1, lg - 1);
2734    for b in 0..banks {
2735        s.push_str(&read(&bank(mem, b, banks), &format!("{data}_r"), &format!("{data}_k{b}")));
2736    }
2737    // The element's bank, as its data lands.
2738    for n in 1..=lat {
2739        s.push_str(&format!(" reg [{}:0] {data}_o{n}; always @(posedge clk) {data}_o{n} <= {data}_o{};", lg - 1, n - 1));
2740    }
2741    // Lane `j` of a run is bank `start | j`. A run that reaches lane `j` is wider than `j` — the narrowest such run the
2742    // port carries sets the multiple it starts at — so the lane chooses among those banks only; a lane no run the port
2743    // carries reaches reads nothing.
2744    for j in 0..banks {
2745        let Some(step) = runs.iter().copied().filter(|w| *w > j).min() else {
2746            s.push_str(&format!(" wire [{}:0] {data}_v{j} = {}'d0;", width - 1, width));
2747            continue;
2748        };
2749        if step == banks {
2750            s.push_str(&format!(" wire [{}:0] {data}_v{j} = {data}_k{j};", width - 1));
2751            continue;
2752        }
2753        let sh = step.trailing_zeros();
2754        s.push_str(&format!(" reg [{}:0] {data}_v{j}; always @* case ({data}_o{lat}[{}:{sh}])", width - 1, lg - 1));
2755        for m in 0..banks / step {
2756            s.push_str(&format!(" {}'d{m}: {data}_v{j} = {data}_k{};", lg - sh, m * step + j));
2757        }
2758        s.push_str(" endcase");
2759    }
2760    let hi: Vec<String> = (1..banks).rev().map(|j| format!("{data}_v{j}")).collect();
2761    s.push_str(&format!(" wire [63:0] {data} = $signed({data}_v0); wire [{}:0] {data}_hi = {{{}}};", (banks as u32 - 1) * width - 1, hi.join(", ")));
2762    s
2763}
2764
2765/// The assignment of `name` to `src` delayed `lat` cycles through registers (`src` itself when `lat` is zero).
2766fn delayed(name: &str, src: &str, width: usize, lat: u64) -> String {
2767    if lat == 0 {
2768        return format!(" assign {name} = {src};");
2769    }
2770    let mut s = String::new();
2771    for n in 0..lat {
2772        let from = if n == 0 { src.to_string() } else { format!("{name}_d{}", n - 1) };
2773        s.push_str(&format!(" reg [{}:0] {name}_d{n}; always @(posedge clk) {name}_d{n} <= {from};", width - 1));
2774    }
2775    s.push_str(&format!(" assign {name} = {name}_d{};", lat - 1));
2776    s
2777}
2778
2779/// `v`, a slot of a ring of `depth` plus at most `depth` more — below `2 · depth`, in `[hi:0]` — wrapped onto the ring: a
2780/// mask where the depth is a power of two, else one compare and one subtraction at `v`'s own width. A remainder by a
2781/// constant that is no power of two is a divider as wide as its context.
2782fn ring_wrap(v: &str, depth: usize, hi: usize) -> String {
2783    if depth.is_power_of_two() {
2784        format!("{v} % {depth}")
2785    } else {
2786        let w = hi + 1;
2787        format!("({v} >= {w}'d{depth}) ? {v} - {w}'d{depth} : {v}")
2788    }
2789}
2790
2791/// A page FIFO's control, module `name`: `depth` pages, and the reservations, pushes, waits and pops that are the IR's
2792/// ops — `free`, `count`, the slot the next fill takes and the slot the next read takes. Its pages live in the pool
2793/// memory its phase colors it into (`site_pools`).
2794pub fn fifo_control_verilog(name: &str, depth: usize) -> String {
2795    let (sw, cw) = (bits_for(depth), bits_for(depth + 1));
2796    // The read slot plus a count or a pop: below twice the depth.
2797    let aw = bits_for(2 * depth);
2798    let mut e = Emit { s: String::new() };
2799    e.l(format!("module {name}("));
2800    e.l("  input clk, input rst,");
2801    e.l(format!("  input reserve, input [{cw}:0] reserve_n, input push, input [{cw}:0] push_n, input pop, input [{cw}:0] pop_n,"));
2802    e.l(format!("  output [{cw}:0] free, output [{cw}:0] count, output [{sw}:0] fill_slot0, output [{sw}:0] rd_slot0"));
2803    e.l(");");
2804    e.l(format!("  reg [{cw}:0] cnt, rsv; reg [{sw}:0] rd;"));
2805    e.l(format!("  wire [{aw}:0] fill_at = rd + cnt, rd_at = rd + pop_n;"));
2806    e.l(format!("  assign free = {depth} - cnt - rsv;"));
2807    e.l("  assign count = cnt;");
2808    e.l(format!("  assign fill_slot0 = {};", ring_wrap("fill_at", depth, aw)));
2809    e.l("  assign rd_slot0 = rd;");
2810    e.l("  always @(posedge clk) begin");
2811    e.l("    if (rst) begin cnt <= 0; rsv <= 0; rd <= 0; end");
2812    e.l("    else begin");
2813    e.l("      cnt <= cnt + (push ? push_n : 0) - (pop ? pop_n : 0);");
2814    e.l("      rsv <= rsv + (reserve ? reserve_n : 0) - (push ? push_n : 0);");
2815    e.l(format!("      if (pop) rd <= {};", ring_wrap("rd_at", depth, aw)));
2816    e.l("    end");
2817    e.l("  end");
2818    e.l("endmodule");
2819    e.s
2820}
2821
2822/// A pool: the memory every buffer of one color keeps its pages in ([`site_pools`]) — `words` elements of `width` bits in
2823/// `banks` banks, one write port and `nread` read ports, each `lat` cycles from address to data, the write port carrying
2824/// the run widths `wr` and read port `r` those of `rd[r]`. A write takes `wr_n` elements of a run of `2^wr_lw` from
2825/// `wr_addr`; a read returns the rest of its run in `rd_hi`.
2826#[allow(clippy::too_many_arguments)]
2827fn print_pool(name: &str, words: usize, width: u32, banks: usize, lat: u64, nread: usize, wr: &BTreeSet<usize>, rd: &[BTreeSet<usize>]) -> String {
2828    let lg = log2_banks(banks);
2829    let hi = (banks as u32 - 1) * width;
2830    let mut e = Emit { s: String::new() };
2831    let mut ports = vec!["input clk".to_string(), "input rst".into(), "input wr_en".into(), "input [31:0] wr_addr".into(), "input [63:0] wr_data".into()];
2832    if banks > 1 {
2833        ports.push(format!("input [{lg}:0] wr_n"));
2834        ports.push("input [3:0] wr_lw".into());
2835        ports.push(format!("input [{}:0] wr_hi", hi - 1));
2836    }
2837    for r in 0..nread {
2838        ports.push(format!("input [31:0] rd{r}_addr"));
2839        ports.push(format!("output [63:0] rd{r}_data"));
2840        if banks > 1 {
2841            ports.push(format!("output [{}:0] rd{r}_hi", hi - 1));
2842        }
2843    }
2844    e.l(format!("module {name}({});", ports.join(", ")));
2845    e.l(memory_decl("mem", words, width, banks, lat));
2846    for (r, runs) in rd.iter().enumerate() {
2847        e.l(memory_read(&format!("rd{r}_word"), "mem", &format!("rd{r}_addr"), width, banks, lat, runs));
2848        e.l(format!("  assign rd{r}_data = rd{r}_word;"));
2849        if banks > 1 {
2850            e.l(format!("  assign rd{r}_hi = rd{r}_word_hi;"));
2851        }
2852    }
2853    let (wires, put) = memory_write("wm", "mem", "wr_addr", "wr_n", "wr_lw", "wr_data", "wr_hi", width, banks, wr);
2854    if !wires.is_empty() {
2855        e.l(wires);
2856    }
2857    e.l(format!("  always @(posedge clk) if (wr_en && !rst) begin {put} end"));
2858    e.l("endmodule");
2859    e.s
2860}
2861
2862/// One pool of a site: the buffers whose pages it holds, its words and width, its read ports and the run widths each of
2863/// its ports carries.
2864struct Pool {
2865    members: Vec<BufferId>,
2866    words: usize,
2867    width: u32,
2868    nread: usize,
2869    wr: BTreeSet<usize>,
2870    rd: Vec<BTreeSet<usize>>,
2871}
2872
2873/// The elements one page of `b` takes in its pool: its page, padded to start every page on a word.
2874fn page_stride(ctx: &Ctx, b: BufferId) -> usize {
2875    ctx.page_words(b).next_multiple_of(ctx.banks)
2876}
2877
2878/// ★ A SITE'S FIFO PAGES LIVE IN POOLS, ONE PER BUFFER A PHASE HAS LIVE AT ONCE. Every engine of a site passes a
2879/// rendezvous between two phases, and every op's own reads and writes land within its cycles, so two buffers of
2880/// different phases never move data in the same cycle: they can keep their pages in one memory behind one set of ports.
2881/// Each phase's buffers, largest first, take pools 0, 1, 2, …, so the site has as many pools as its busiest phase has
2882/// buffers — not one memory and one set of ports per buffer of the program. Buffers outside every section are all one
2883/// phase, each in a pool of its own. A pool is as deep and as wide as its largest member, and each of its ports carries
2884/// every run width its members' ports carry ([`port_widths`]).
2885///
2886/// Every read port of a pool is a copy of all of it, so buffers share a pool only with buffers read through as many
2887/// ports: those read through one take pools 0, 1, 2, … as above, and those read through more — a page a gather's lanes
2888/// read through ports of their own ([`gather_lanes`]) — take pools of their own after them, by their count of ports.
2889fn site_pools(ctx: &Ctx, plans: &[EnginePlan], site: Site) -> Vec<Pool> {
2890    let p = ctx.p;
2891    let phases = p.section_phases();
2892    let (rd_index, readers) = read_ports(ctx, plans, site);
2893    let widths: BTreeMap<usize, BTreeMap<Port, BTreeSet<usize>>> =
2894        plans.iter().enumerate().filter(|(_, pl)| pl.site == site).map(|(ei, pl)| (ei, port_widths(ctx, &pl.micro))).collect();
2895    let bus_fed: BTreeSet<BufferId> = p
2896        .stages
2897        .iter()
2898        .filter(|st| st.site == Some(site) && matches!(st.kind, StageKind::Recv { .. }))
2899        .flat_map(|st| st.outs.iter().copied())
2900        .collect();
2901    let nread = |b: BufferId| readers.get(&ctx.template(b)).copied().unwrap_or(1).max(1);
2902    let mut by_ports: BTreeMap<usize, BTreeMap<Option<usize>, Vec<BufferId>>> = BTreeMap::new();
2903    for b in p.buffers.iter().filter(|b| b.site == Some(site) && !matches!(b.kind, BufferKind::Accumulator { .. })) {
2904        by_ports.entry(nread(b.id)).or_default().entry(p.section_of_buffer(b.id).map(|k| phases[k])).or_default().push(b.id);
2905    }
2906    let words = |b: BufferId| ctx.depth(b) * page_stride(ctx, b);
2907    let mut pools: Vec<Pool> = Vec::new();
2908    for (ports, by_phase) in &mut by_ports {
2909        let first = pools.len();
2910        for members in by_phase.values_mut() {
2911            members.sort_by_key(|b| (std::cmp::Reverse(words(*b) * ctx.buffer_width(*b) as usize), b.idx()));
2912            for (i, b) in members.iter().enumerate() {
2913                if pools.len() == first + i {
2914                    pools.push(Pool { members: Vec::new(), words: 0, width: 1, nread: *ports, wr: BTreeSet::new(), rd: Vec::new() });
2915                }
2916                let pool = &mut pools[first + i];
2917                pool.members.push(*b);
2918                pool.words = pool.words.max(words(*b));
2919                pool.width = pool.width.max(ctx.buffer_width(*b));
2920                for found in widths.values().filter_map(|w| w.get(&Port::FifoWr(*b))) {
2921                    pool.wr.extend(found);
2922                }
2923                if bus_fed.contains(b) {
2924                    pool.wr.insert(1);
2925                }
2926            }
2927        }
2928    }
2929    for pool in &mut pools {
2930        pool.rd = vec![BTreeSet::new(); pool.nread];
2931        for ((ei, b), r) in &rd_index {
2932            if pool.members.contains(b) {
2933                if let Some(found) = widths.get(ei).and_then(|w| w.get(&Port::FifoRd(*b))) {
2934                    pool.rd[*r].extend(found);
2935                }
2936            }
2937        }
2938        pool.wr = widths_or_element(std::mem::take(&mut pool.wr));
2939        pool.rd = pool.rd.drain(..).map(widths_or_element).collect();
2940    }
2941    pools
2942}
2943
2944impl SpatialBackend for FabricBackend {
2945    fn name(&self) -> &str {
2946        self.target.name
2947    }
2948    fn target(&self) -> &SpatialTarget {
2949        &self.target
2950    }
2951    fn lower(&self, p: &SpatialProgram) -> Result<SpatialArtifact, SpatialLowerError> {
2952        require_verified(p)?;
2953        require_composition_on_one_site(p)?;
2954        let ctx = context(p, &self.target)?;
2955        let plans = plans(&ctx)?;
2956        let pl = p.placement.as_ref().unwrap();
2957        if p.semaphores.iter().any(|s| s.init != 0) {
2958            return Err(SpatialLowerError::Unsupported(
2959                "a semaphore whose initial value is not the counter's reset value",
2960            ));
2961        }
2962        // A site's items must be consecutive: the engines derive `item_id` as `ITEM_START + lv`.
2963        if pl
2964            .items_of_site
2965            .iter()
2966            .any(|(_, items)| items.windows(2).any(|w| w[1].idx() != w[0].idx() + 1))
2967        {
2968            return Err(SpatialLowerError::Unsupported(
2969                "a strided item enumeration on the fabric",
2970            ));
2971        }
2972        for t in &p.tensors {
2973            if precision_int_bits(t.dtype).is_none() {
2974                return Err(SpatialLowerError::Precision { needed: t.dtype });
2975            }
2976        }
2977        // Refuse a fabric past its part's resources before a line of RTL is printed. Only when the target names a
2978        // part; an abstract target has no budget to exceed.
2979        budget::check_capacity(p, &self.target)?;
2980        let mut rtl = Emit { s: String::new() };
2981        rtl.l(format!("// spatial_top — {} lowered to our fabric from the spatial IR; every module is one IR object.", p.name));
2982        rtl.l("`timescale 1ns/1ps");
2983        // Primitives.
2984        rtl.s.push_str(&emit_semaphore_verilog("sem_prim", 16));
2985        // A section rendezvous is its own IR object — the counting semaphore every engine program arrives at between
2986        // two sections — so it is its own primitive, built on the one semaphore.
2987        if p.semaphores.iter().any(|s| matches!(s.role, SemRole::Barrier { .. })) {
2988            rtl.l("// barrier_prim — a section rendezvous: the counting semaphore every engine program arrives at between two sections.");
2989            rtl.l("module barrier_prim(input clk, input rst, input inc, input [15:0] inc_n, input set, input [15:0] set_val, input [15:0] thresh, output [15:0] count, output ge);");
2990            rtl.l("  sem_prim u(.clk(clk), .rst(rst), .inc(inc), .inc_n(inc_n), .set(set), .set_val(set_val), .thresh(thresh), .count(count), .ge(ge));");
2991            rtl.l("endmodule");
2992        }
2993        let mut bus_widths: BTreeSet<usize> = BTreeSet::new();
2994        for g in &pl.groups {
2995            bus_widths.insert(g.rect.len() - 1);
2996        }
2997        for n in &bus_widths {
2998            rtl.s.push_str(&emit_mcast_bus_verilog(
2999                &format!("mcast_bus_prim_{n}"),
3000                80,
3001                *n,
3002            ));
3003        }
3004        // Buffer templates.
3005        let mut templates: BTreeMap<BufferId, usize> = BTreeMap::new();
3006        for b in &p.buffers {
3007            if matches!(b.kind, BufferKind::Accumulator { .. }) {
3008                continue;
3009            }
3010            templates.entry(ctx.template(b.id)).or_insert(ctx.depth(b.id));
3011        }
3012        for (t, depth) in &templates {
3013            rtl.s.push_str(&fifo_control_verilog(&format!("fifo_tpl_{}", t.idx()), *depth));
3014        }
3015        // Engine classes: representative = first plan of the class.
3016        let mut class_rep: BTreeMap<usize, usize> = BTreeMap::new();
3017        for (i, plan) in plans.iter().enumerate() {
3018            class_rep.entry(plan.class).or_insert(i);
3019        }
3020        let mut grid_names: BTreeMap<usize, String> = BTreeMap::new();
3021        for (class, rep) in &class_rep {
3022            let plan = &plans[*rep];
3023            // A scalar body's placed grid.
3024            for m in &plan.micro {
3025                if let Micro::Fixed(Fixed::Unary {
3026                    kind: UnaryKind::Scalar(body),
3027                    ..
3028                }) = m
3029                {
3030                    let g = route_loop_body(&body.cells, body.n_in, &[body.result]);
3031                    let gname = format!("c{class}");
3032                    rtl.s.push_str(&emit_grid_verilog(&gname, &g));
3033                    grid_names.insert(*class, gname);
3034                    break;
3035                }
3036            }
3037            if let Some((k, n, bits)) = pe_array(&plan.micro) {
3038                rtl.s.push_str(&emit_systolic_pipelined_loadable_verilog_starting(
3039                    &format!("pe_engine_{class}"),
3040                    k,
3041                    n,
3042                    bits,
3043                    pe_acc_bits(bits, k),
3044                    WeightStart::Zero,
3045                ));
3046            }
3047            let class_plans: Vec<&EnginePlan> = plans.iter().filter(|pl| pl.class == *class).collect();
3048            rtl.s.push_str(&engine::print_engine(
3049                &ctx,
3050                plan,
3051                &class_plans,
3052                &format!("engine_{class}"),
3053                &grid_names,
3054            )?);
3055        }
3056        // Sites.
3057        let sites: Vec<Site> = pl.items_of_site.iter().map(|(s, _)| *s).collect();
3058        let mut hex: Vec<(String, String)> = Vec::new();
3059        for t in &p.tensors {
3060            if t.role == TensorRole::Output {
3061                continue;
3062            }
3063            let cols = t.elems.cols;
3064            let words = match &t.init {
3065                TensorInit::Values(v) => {
3066                    tiled_words(p, t, &|i, j| v.get(i * cols + j).copied().unwrap_or(0))
3067                }
3068                TensorInit::Pattern { .. } => {
3069                    tiled_words(p, t, &|i, j| t.init.element(i * cols + j).unwrap_or(0))
3070                }
3071                // External data arrives through `input_hex_files`; the artifact still ships a zero image so
3072                // the memory's `$readmemh` resolves during elaboration and synthesis.
3073                _ => vec![0i64; ctx.tensor_words(t)],
3074            };
3075            hex.extend(tensor_hex(t, &words, tensor_width(t, &ctx.ranges), ctx.banks));
3076        }
3077        for site in &sites {
3078            rtl.s.push_str(&print_site(&ctx, &plans, *site)?);
3079        }
3080        // Top.
3081        rtl.s.push_str(&print_top(&ctx, &plans, &sites)?);
3082        let census = Census::of(p);
3083        let cycle_model = fabric_cycle_model(p, &self.target);
3084        let floor = cycle_floor(&ctx, &plans);
3085        let testbench = print_testbench(&ctx, &sites, cycle_model);
3086        Ok(SpatialArtifact::Verilog(VerilogArtifact {
3087            top: "spatial_top".into(),
3088            rtl: rtl.s,
3089            testbench,
3090            hex,
3091            census,
3092            cycle_model,
3093            cycle_floor: floor,
3094        }))
3095    }
3096}
3097
3098/// A site: its tensor memories, its buffers, its semaphores and its engines, with the cross-site signals
3099/// (semaphore increments, valid sets, multicast data) as ports.
3100fn print_site(ctx: &Ctx, plans: &[EnginePlan], site: Site) -> Result<String, SpatialLowerError> {
3101    let p = ctx.p;
3102    let pl = p.placement.as_ref().unwrap();
3103    let mine: Vec<(usize, &EnginePlan)> = plans
3104        .iter()
3105        .enumerate()
3106        .filter(|(_, pl)| pl.site == site)
3107        .collect();
3108    let bufs: Vec<&Buffer> = p
3109        .buffers
3110        .iter()
3111        .filter(|b| b.site == Some(site) && !matches!(b.kind, BufferKind::Accumulator { .. }))
3112        .collect();
3113    // Read-port index per (buffer template, reading engine) at this site.
3114    let (rd_index, readers) = read_ports(ctx, plans, site);
3115    let widths: BTreeMap<usize, BTreeMap<Port, BTreeSet<usize>>> = mine.iter().map(|(ei, pl)| (*ei, port_widths(ctx, &pl.micro))).collect();
3116    let tensor_widths = |ei: usize, port: Port| -> BTreeSet<usize> { widths.get(&ei).and_then(|w| w.get(&port)).cloned().unwrap_or_default() };
3117    let sems: Vec<&Semaphore> = p
3118        .semaphores
3119        .iter()
3120        .filter(|s| s.sites.contains(&site))
3121        .collect();
3122    // Which buffers are fed by a multicast bus (the site is a receiver of a group whose Recv stage fills them).
3123    let mut bus_fed: BTreeMap<BufferId, GroupId> = BTreeMap::new();
3124    for st in &p.stages {
3125        if st.site == Some(site) {
3126            if let StageKind::Recv { group } = st.kind {
3127                for b in &st.outs {
3128                    bus_fed.insert(*b, group);
3129                }
3130            }
3131        }
3132    }
3133    // Remote semaphore increments arriving here: every engine (any site) whose micro-program targets (sem, site).
3134    let mut inc_sources: BTreeMap<SemId, Vec<usize>> = BTreeMap::new();
3135    let mut valid_sources: BTreeMap<SemId, Vec<(usize, GroupId)>> = BTreeMap::new();
3136    for (ei, plan) in plans.iter().enumerate() {
3137        for m in &plan.micro {
3138            match m {
3139                Micro::Fixed(Fixed::SemInc { sem, at, .. }) | Micro::Then(Fixed::SemInc { sem, at, .. }) if *at == site => {
3140                    inc_sources.entry(*sem).or_default().push(ei)
3141                }
3142                Micro::Fixed(Fixed::McastSetValid { sem, group }) | Micro::Then(Fixed::McastSetValid { sem, group }) => {
3143                    let g = &pl.groups[group.idx()];
3144                    if g.rect.sites().contains(&site) && g.sender != site {
3145                        valid_sources.entry(*sem).or_default().push((ei, *group));
3146                    }
3147                }
3148                _ => {}
3149            }
3150        }
3151    }
3152    let mut e = Emit { s: String::new() };
3153    let mut ports = vec![
3154        "input clk".to_string(),
3155        "input rst".to_string(),
3156        "input start".to_string(),
3157        "output done".to_string(),
3158        "input host_we".to_string(),
3159        "input host_re".to_string(),
3160        "input [15:0] host_tensor".to_string(),
3161        "input [31:0] host_addr".to_string(),
3162        "input [63:0] host_wdata".to_string(),
3163        "output [63:0] host_rdata".to_string(),
3164    ];
3165    for (sem, srcs) in &inc_sources {
3166        for ei in srcs {
3167            ports.push(format!("input inc_s{}_from_e{ei}", sem.idx()));
3168            ports.push(format!("input [15:0] inc_s{}_n_from_e{ei}", sem.idx()));
3169        }
3170    }
3171    for (sem, srcs) in &valid_sources {
3172        for (ei, g) in srcs {
3173            ports.push(format!(
3174                "input valid_s{}_from_e{ei}_g{}",
3175                sem.idx(),
3176                g.idx()
3177            ));
3178        }
3179    }
3180    // Outgoing cross-site pulses from this site's engines.
3181    for (ei, plan) in &mine {
3182        for sem in sem_incs(&plan.micro) {
3183            ports.push(format!("output e{ei}_s{}_inc_remote", sem.idx()));
3184            ports.push(format!("output [15:0] e{ei}_s{}_inc_n", sem.idx()));
3185        }
3186        for (sem, g) in mcast_valids(&plan.micro) {
3187            ports.push(format!("output e{ei}_mv{}_g{}", sem.idx(), g.idx()));
3188        }
3189        for g in mcast_sends(&plan.micro) {
3190            ports.push(format!("output e{ei}_mc{}_send", g.idx()));
3191            ports.push(format!("output [79:0] e{ei}_mc{}_din", g.idx()));
3192        }
3193    }
3194    for (b, g) in &bus_fed {
3195        ports.push(format!("input bus_g{}_push_b{}", g.idx(), b.idx()));
3196        ports.push(format!("input [79:0] bus_g{}_dout_b{}", g.idx(), b.idx()));
3197    }
3198    let halting: Vec<usize> = mine.iter().filter(|(_, pl)| halts_in(&pl.micro)).map(|(ei, _)| *ei).collect();
3199    if !halting.is_empty() {
3200        ports.push("input [7:0] link_n".to_string());
3201        ports.push("output halted".to_string());
3202        ports.push("output [15:0] halt_at".to_string());
3203    }
3204    e.l(format!("module site_{}_{}(", site.x, site.y));
3205    e.l(format!("  {}", ports.join(",\n  ")));
3206    e.l(");");
3207    // Tensor memories: inputs as ROM copies, outputs as this site's RAM, each in the site's banks.
3208    let banks = ctx.banks;
3209    let hi_of = |width: u32| (banks as u32 - 1) * width;
3210    for t in &p.tensors {
3211        let id = t.id.idx();
3212        let words = ctx.tensor_words(t);
3213        let rows = words.max(1).div_ceil(banks);
3214        let width = tensor_width(t, &ctx.ranges);
3215        let n = lanes(width).len();
3216        e.l(memory_decl(&format!("mem_t{id}"), words, width, banks, ctx.lat));
3217        for b in 0..banks {
3218            for l in 0..n {
3219                let mem = lane(&bank(&format!("mem_t{id}"), b, banks), l, n);
3220                if t.role != TensorRole::Output {
3221                    let stem = bank(&format!("t{id}"), b, banks);
3222                    let file = if n == 1 { format!("{stem}.hex") } else { format!("{stem}_l{l}.hex") };
3223                    e.l(format!("  initial $readmemh(\"{file}\", {mem});"));
3224                } else {
3225                    let zi = format!("zi{id}_{}{l}", if banks == 1 { String::new() } else { format!("{b}_") });
3226                    e.l(format!("  integer {zi}; initial for ({zi} = 0; {zi} < {rows}; {zi} = {zi} + 1) {mem}[{zi}] = 0;"));
3227                }
3228            }
3229        }
3230    }
3231    // Buffers.
3232    for b in &bufs {
3233        let k = b.id.idx();
3234        let (sw, ew, cw) = (
3235            bits_for(ctx.depth(b.id)),
3236            bits_for(ctx.page_words(b.id)),
3237            bits_for(ctx.depth(b.id) + 1),
3238        );
3239        let nrd = readers
3240            .get(&ctx.template(b.id))
3241            .copied()
3242            .unwrap_or(1)
3243            .max(1);
3244        e.l(format!("  wire f{k}_reserve, f{k}_push, f{k}_pop, f{k}_wr_en; wire [{cw}:0] f{k}_reserve_n, f{k}_push_n, f{k}_pop_n, f{k}_free, f{k}_count; wire [{sw}:0] f{k}_fill_slot0, f{k}_rd_slot0, f{k}_wr_slot; wire [{ew}:0] f{k}_wr_elem; wire [63:0] f{k}_wr_data;"));
3245        let bw = ctx.buffer_width(b.id);
3246        if banks > 1 {
3247            e.l(format!("  wire [{}:0] f{k}_wr_n; wire [3:0] f{k}_wr_lw; wire [{}:0] f{k}_wr_hi;", log2_banks(banks), hi_of(bw) - 1));
3248        }
3249        for r in 0..nrd {
3250            e.l(format!("  wire [{sw}:0] f{k}_rd{r}_slot; wire [{ew}:0] f{k}_rd{r}_elem; wire [63:0] f{k}_rd{r}_data;"));
3251            if banks > 1 {
3252                e.l(format!("  wire [{}:0] f{k}_rd{r}_hi;", hi_of(bw) - 1));
3253            }
3254        }
3255        e.l(format!(
3256            "  fifo_tpl_{} fifo_{}_{}_{}(.clk(clk), .rst(rst), .reserve(f{k}_reserve), .reserve_n(f{k}_reserve_n), .push(f{k}_push), .push_n(f{k}_push_n), .pop(f{k}_pop), .pop_n(f{k}_pop_n), .free(f{k}_free), .count(f{k}_count), .fill_slot0(f{k}_fill_slot0), .rd_slot0(f{k}_rd_slot0));",
3257            ctx.template(b.id).idx(),
3258            ident(&b.name),
3259            site.x,
3260            site.y,
3261        ));
3262    }
3263    // Pools: each pool's ports take its members' requests — one member moves data at a time, and an idle member's
3264    // port is all zeros, so the requests merge by OR — and hand each member the data its own port reads.
3265    let pools = site_pools(ctx, plans, site);
3266    for (i, pool) in pools.iter().enumerate() {
3267        let pn = format!("pool{i}");
3268        let gated = |en: &str, v: &str, zero: &str| -> String { format!("({en} ? {v} : {zero})") };
3269        let addr = |b: BufferId, slot: &str, elem: &str| format!("(f{0}_{slot} * {1} + f{0}_{elem})", b.idx(), page_stride(ctx, b));
3270        let hi_w = hi_of(pool.width);
3271        e.l(format!("  wire {pn}_we = {};", pool.members.iter().map(|b| format!("f{}_wr_en", b.idx())).collect::<Vec<_>>().join(" | ")));
3272        e.l(format!(
3273            "  wire [31:0] {pn}_wa = {};",
3274            pool.members.iter().map(|b| gated(&format!("f{}_wr_en", b.idx()), &addr(*b, "wr_slot", "wr_elem"), "32'd0")).collect::<Vec<_>>().join(" | ")
3275        ));
3276        e.l(format!(
3277            "  wire [63:0] {pn}_wd = {};",
3278            pool.members.iter().map(|b| gated(&format!("f{}_wr_en", b.idx()), &format!("f{}_wr_data", b.idx()), "64'd0")).collect::<Vec<_>>().join(" | ")
3279        ));
3280        let mut conn = format!(".clk(clk), .rst(rst), .wr_en({pn}_we), .wr_addr({pn}_wa), .wr_data({pn}_wd)");
3281        if banks > 1 {
3282            let lgb = log2_banks(banks);
3283            e.l(format!(
3284                "  wire [{lgb}:0] {pn}_wn = {}; wire [3:0] {pn}_wl = {};",
3285                pool.members.iter().map(|b| gated(&format!("f{}_wr_en", b.idx()), &format!("f{}_wr_n", b.idx()), &format!("{}'d0", lgb + 1))).collect::<Vec<_>>().join(" | "),
3286                pool.members.iter().map(|b| gated(&format!("f{}_wr_en", b.idx()), &format!("f{}_wr_lw", b.idx()), "4'd0")).collect::<Vec<_>>().join(" | ")
3287            ));
3288            e.l(format!(
3289                "  wire [{}:0] {pn}_wh = {};",
3290                hi_w - 1,
3291                pool.members
3292                    .iter()
3293                    .map(|b| gated(&format!("f{}_wr_en", b.idx()), &resize_lanes(&format!("f{}_wr_hi", b.idx()), ctx.buffer_width(*b), pool.width, banks - 1), &format!("{hi_w}'d0")))
3294                    .collect::<Vec<_>>()
3295                    .join(" | ")
3296            ));
3297            conn.push_str(&format!(", .wr_n({pn}_wn), .wr_lw({pn}_wl), .wr_hi({pn}_wh)"));
3298        }
3299        if pool.members.len() > 1 {
3300            e.l("  // synthesis translate_off");
3301            e.l(format!(
3302                "  always @(posedge clk) if ({} > 1) begin $display(\"FATAL: {pn}: two of its buffers write in one cycle\"); $finish; end",
3303                pool.members.iter().map(|b| format!("f{}_wr_en", b.idx())).collect::<Vec<_>>().join(" + ")
3304            ));
3305            e.l("  // synthesis translate_on");
3306        }
3307        for r in 0..pool.nread {
3308            let on: Vec<BufferId> = pool.members.iter().copied().filter(|b| r < readers.get(&ctx.template(*b)).copied().unwrap_or(1).max(1)).collect();
3309            let ra = if on.is_empty() {
3310                "32'd0".to_string()
3311            } else {
3312                on.iter().map(|b| addr(*b, &format!("rd{r}_slot"), &format!("rd{r}_elem"))).collect::<Vec<_>>().join(" | ")
3313            };
3314            e.l(format!("  wire [31:0] {pn}_ra{r} = {ra}; wire [63:0] {pn}_rd{r};"));
3315            conn.push_str(&format!(", .rd{r}_addr({pn}_ra{r}), .rd{r}_data({pn}_rd{r})"));
3316            if banks > 1 {
3317                e.l(format!("  wire [{}:0] {pn}_rh{r};", hi_w - 1));
3318                conn.push_str(&format!(", .rd{r}_hi({pn}_rh{r})"));
3319            }
3320            for b in &on {
3321                e.l(format!("  assign f{0}_rd{r}_data = {pn}_rd{r};", b.idx()));
3322                if banks > 1 {
3323                    e.l(format!("  assign f{0}_rd{r}_hi = {1};", b.idx(), resize_lanes(&format!("{pn}_rh{r}"), pool.width, ctx.buffer_width(*b), banks - 1)));
3324                }
3325            }
3326        }
3327        e.l(format!("  pool_{}_{}_{i} {pn}_{}_{}({conn});", site.x, site.y, site.x, site.y));
3328    }
3329    // Semaphores.
3330    for s in &sems {
3331        let k = s.idx_str();
3332        let incs = inc_sources.get(&s.id).cloned().unwrap_or_default();
3333        let valids = valid_sources.get(&s.id).cloned().unwrap_or_default();
3334        let inc_any = if incs.is_empty() {
3335            "1'b0".to_string()
3336        } else {
3337            incs.iter()
3338                .map(|ei| format!("inc_s{k}_from_e{ei}"))
3339                .collect::<Vec<_>>()
3340                .join(" | ")
3341        };
3342        let inc_sum = if incs.is_empty() {
3343            "16'd0".to_string()
3344        } else {
3345            incs.iter()
3346                .map(|ei| format!("(inc_s{k}_from_e{ei} ? inc_s{k}_n_from_e{ei} : 16'd0)"))
3347                .collect::<Vec<_>>()
3348                .join(" + ")
3349        };
3350        let local: Vec<usize> = mine
3351            .iter()
3352            .filter(|(_, pl)| sems_local(&pl.micro).contains(&s.id))
3353            .map(|(ei, _)| *ei)
3354            .collect();
3355        let local_set = if local.is_empty() {
3356            "1'b0".to_string()
3357        } else {
3358            local
3359                .iter()
3360                .map(|ei| format!("e{ei}_s{k}_set"))
3361                .collect::<Vec<_>>()
3362                .join(" | ")
3363        };
3364        let local_val = local
3365            .iter()
3366            .map(|ei| format!("e{ei}_s{k}_set ? e{ei}_s{k}_set_val :"))
3367            .collect::<Vec<_>>()
3368            .join(" ");
3369        let valid_any = if valids.is_empty() {
3370            "1'b0".to_string()
3371        } else {
3372            valids
3373                .iter()
3374                .map(|(ei, g)| format!("valid_s{k}_from_e{ei}_g{}", g.idx()))
3375                .collect::<Vec<_>>()
3376                .join(" | ")
3377        };
3378        for ei in &local {
3379            e.l(format!(
3380                "  wire e{ei}_s{k}_set; wire [15:0] e{ei}_s{k}_set_val;"
3381            ));
3382        }
3383        e.l(format!("  wire [15:0] s{k}_count; wire s{k}_set_any = ({local_set}) | ({valid_any}); wire [15:0] s{k}_set_v = {local_val} 16'd1;"));
3384        let prim = if matches!(s.role, SemRole::Barrier { .. }) { "barrier_prim" } else { "sem_prim" };
3385        e.l(format!("  {prim} sem_{k}_{}_{}(.clk(clk), .rst(rst), .inc({inc_any}), .inc_n({inc_sum}), .set(s{k}_set_any), .set_val(s{k}_set_v), .thresh(16'd0), .count(s{k}_count), .ge());", site.x, site.y));
3386        let _ = s.init; // the initial value is applied by the top's reset sequence (a set at start)
3387    }
3388    // Engines.
3389    let mut dones = Vec::new();
3390    for (ei, plan) in &mine {
3391        let uses = buffer_uses(&plan.micro);
3392        let gathered = gather_lanes(ctx, &plan.micro);
3393        let bufs_used: BTreeSet<BufferId> = uses.all();
3394        let mut conn = vec![
3395            ".clk(clk)".to_string(),
3396            ".rst(rst)".to_string(),
3397            ".start(start)".to_string(),
3398            format!(".done(e{ei}_done)"),
3399        ];
3400        e.l(format!("  wire e{ei}_done;"));
3401        dones.push(format!("e{ei}_done"));
3402        for b in &bufs_used {
3403            // The port carries the buffer TEMPLATE's number (the module is shared); the wire carries this
3404            // site's instance.
3405            let (k, w) = (ctx.template(*b).idx(), b.idx());
3406            if uses.produces.contains(b) {
3407                for n in ["reserve", "reserve_n", "push", "push_n", "free"] {
3408                    conn.push(format!(".f{k}_{n}(f{w}_{n})"));
3409                }
3410            }
3411            if uses.consumes.contains(b) {
3412                for n in ["pop", "pop_n", "count"] {
3413                    conn.push(format!(".f{k}_{n}(f{w}_{n})"));
3414                }
3415            }
3416            if uses.writes.contains(b) {
3417                for n in ["wr_en", "wr_slot", "wr_elem", "wr_data", "fill_slot0"] {
3418                    conn.push(format!(".f{k}_{n}(f{w}_{n})"));
3419                }
3420                if banks > 1 {
3421                    conn.push(format!(".f{k}_wr_n(f{w}_wr_n), .f{k}_wr_lw(f{w}_wr_lw), .f{k}_wr_hi(f{w}_wr_hi)"));
3422                }
3423            }
3424            if uses.reads.contains(b) {
3425                let r = rd_index.get(&(*ei, *b)).copied().unwrap_or(0);
3426                for n in ["slot", "elem", "data"] {
3427                    conn.push(format!(".f{k}_rd_{n}(f{w}_rd{r}_{n})"));
3428                }
3429                for l in 1..gathered.get(&ctx.template(*b)).copied().unwrap_or(1) {
3430                    for n in ["slot", "elem", "data"] {
3431                        conn.push(format!(".f{k}_l{l}_rd_{n}(f{w}_rd{}_{n})", r + l));
3432                    }
3433                }
3434                if banks > 1 {
3435                    conn.push(format!(".f{k}_rd_hi(f{w}_rd{r}_hi)"));
3436                }
3437                conn.push(format!(".f{k}_rd_slot0(f{w}_rd_slot0)"));
3438                conn.push(format!(".f{k}_fill_slot0_r(f{w}_fill_slot0)"));
3439            }
3440        }
3441        let twidth = |t: TensorId| tensor_width(p.tensor(t), &ctx.ranges);
3442        for t in tensors_read(&plan.micro) {
3443            e.l(format!("  wire [31:0] e{ei}_t{0}_rd_addr; wire [63:0] e{ei}_t{0}_rd_data;", t.idx()));
3444            conn.push(format!(".t{0}_rd_addr(e{ei}_t{0}_rd_addr)", t.idx()));
3445            conn.push(format!(".t{0}_rd_data(e{ei}_t{0}_rd_data)", t.idx()));
3446            if banks > 1 {
3447                e.l(format!("  wire [{}:0] e{ei}_t{}_rd_hi;", hi_of(twidth(t)) - 1, t.idx()));
3448                conn.push(format!(".t{0}_rd_hi(e{ei}_t{0}_rd_hi)", t.idx()));
3449            }
3450        }
3451        for t in tensors_written(&plan.micro) {
3452            e.l(format!("  wire e{ei}_t{0}_wr_en; wire [31:0] e{ei}_t{0}_wr_addr; wire [63:0] e{ei}_t{0}_wr_data;", t.idx()));
3453            conn.push(format!(".t{0}_wr_en(e{ei}_t{0}_wr_en)", t.idx()));
3454            conn.push(format!(".t{0}_wr_addr(e{ei}_t{0}_wr_addr)", t.idx()));
3455            conn.push(format!(".t{0}_wr_data(e{ei}_t{0}_wr_data)", t.idx()));
3456            if banks > 1 {
3457                e.l(format!("  wire [{0}:0] e{ei}_t{1}_wr_n; wire [3:0] e{ei}_t{1}_wr_lw; wire [{2}:0] e{ei}_t{1}_wr_hi;", log2_banks(banks), t.idx(), hi_of(twidth(t)) - 1));
3458                conn.push(format!(".t{0}_wr_n(e{ei}_t{0}_wr_n), .t{0}_wr_lw(e{ei}_t{0}_wr_lw), .t{0}_wr_hi(e{ei}_t{0}_wr_hi)", t.idx()));
3459            }
3460        }
3461        for s in sems_local(&plan.micro) {
3462            let k = s.idx();
3463            conn.push(format!(".s{k}_count(s{k}_count)"));
3464            conn.push(format!(".s{k}_set(e{ei}_s{k}_set)"));
3465            conn.push(format!(".s{k}_set_val(e{ei}_s{k}_set_val)"));
3466        }
3467        for s in sem_incs(&plan.micro) {
3468            conn.push(format!(".s{0}_inc_remote(e{ei}_s{0}_inc_remote)", s.idx()));
3469            conn.push(format!(".s{0}_inc_n(e{ei}_s{0}_inc_n)", s.idx()));
3470        }
3471        for (s, g) in mcast_valids(&plan.micro) {
3472            conn.push(format!(".mv{0}_g{1}(e{ei}_mv{0}_g{1})", s.idx(), g.idx()));
3473        }
3474        for g in mcast_sends(&plan.micro) {
3475            conn.push(format!(".mc{0}_send(e{ei}_mc{0}_send)", g.idx()));
3476            conn.push(format!(".mc{0}_din(e{ei}_mc{0}_din)", g.idx()));
3477        }
3478        if halting.contains(ei) {
3479            e.l(format!("  wire e{ei}_halted; wire [15:0] e{ei}_halt_at;"));
3480            conn.push(format!(".link_n(link_n), .halted(e{ei}_halted), .halt_at(e{ei}_halt_at)"));
3481        }
3482        e.l(format!(
3483            "  engine_{} #(.ITEM_START({}), .ITEM_COUNT({})) engine_inst_{ei}({});",
3484            plan.class,
3485            plan.item_start,
3486            plan.item_count,
3487            conn.join(", ")
3488        ));
3489    }
3490    if !halting.is_empty() {
3491        e.l(format!("  assign halted = {};", halting.iter().map(|ei| format!("e{ei}_halted")).collect::<Vec<_>>().join(" | ")));
3492        let at: String = halting.iter().map(|ei| format!("e{ei}_halted ? e{ei}_halt_at : ")).collect();
3493        e.l(format!("  assign halt_at = {at}16'd0;"));
3494    }
3495    // Each tensor memory's ports. The host's write takes the memory's write port — the first writing engine's,
3496    // merged — and its read the first reading engine's read port, or a port of its own where no engine reads the
3497    // tensor: the host reaches a memory only while the fabric is idle or halted (every engine then waits at a
3498    // rendezvous), so neither ever contends with an engine, and every memory keeps one write port and one read port
3499    // beside any more engines it serves.
3500    let mut host_words: Vec<String> = Vec::new();
3501    for t in &p.tensors {
3502        let id = t.id.idx();
3503        let writers: Vec<usize> = mine.iter().filter(|(_, pl)| tensors_written(&pl.micro).contains(&t.id)).map(|(ei, _)| *ei).collect();
3504        let readers: Vec<usize> = mine.iter().filter(|(_, pl)| tensors_read(&pl.micro).contains(&t.id)).map(|(ei, _)| *ei).collect();
3505        let width = tensor_width(t, &ctx.ranges);
3506        let mem = format!("mem_t{id}");
3507        e.l(format!("  wire t{id}_hw = host_we && host_tensor == 16'd{id}; wire t{id}_hr = host_re && host_tensor == 16'd{id};"));
3508        // A host's write is one element: a run of one, whatever the banks.
3509        let zero_hi = format!("{}'d0", hi_of(width).max(1));
3510        let put = |e: &mut Emit, tag: &str, en: &str, addr: &str, n: &str, lw: &str, data: &str, hi: &str, runs: &BTreeSet<usize>| {
3511            let (wires, stmts) = memory_write(tag, &mem, addr, n, lw, data, hi, width, banks, runs);
3512            if !wires.is_empty() {
3513                e.l(wires);
3514            }
3515            e.l(format!("  always @(posedge clk) if ({en}) begin {stmts} end"));
3516        };
3517        match writers.split_first() {
3518            Some((w0, more)) => {
3519                e.l(format!("  wire [31:0] t{id}_wa = t{id}_hw ? host_addr : e{w0}_t{id}_wr_addr; wire [63:0] t{id}_wd = t{id}_hw ? host_wdata : e{w0}_t{id}_wr_data;"));
3520                let (n, lw, hi) = if banks > 1 {
3521                    e.l(format!(
3522                        "  wire [{0}:0] t{id}_wn = t{id}_hw ? {0}'d1 : e{w0}_t{id}_wr_n; wire [3:0] t{id}_wl = t{id}_hw ? 4'd0 : e{w0}_t{id}_wr_lw; wire [{1}:0] t{id}_wh = t{id}_hw ? {zero_hi} : e{w0}_t{id}_wr_hi;",
3523                        log2_banks(banks),
3524                        hi_of(width) - 1
3525                    ));
3526                    (format!("t{id}_wn"), format!("t{id}_wl"), format!("t{id}_wh"))
3527                } else {
3528                    (String::new(), String::new(), String::new())
3529                };
3530                // The first writer's port is also the host's, which writes an element.
3531                let mut runs = tensor_widths(*w0, Port::TensorWr(t.id));
3532                runs.insert(1);
3533                put(&mut e, &format!("t{id}_w0"), &format!("t{id}_hw || e{w0}_t{id}_wr_en"), &format!("t{id}_wa"), &n, &lw, &format!("t{id}_wd"), &hi, &runs);
3534                for w in more {
3535                    put(
3536                        &mut e,
3537                        &format!("t{id}_w{w}"),
3538                        &format!("e{w}_t{id}_wr_en"),
3539                        &format!("e{w}_t{id}_wr_addr"),
3540                        &format!("e{w}_t{id}_wr_n"),
3541                        &format!("e{w}_t{id}_wr_lw"),
3542                        &format!("e{w}_t{id}_wr_data"),
3543                        &format!("e{w}_t{id}_wr_hi"),
3544                        &widths_or_element(tensor_widths(*w, Port::TensorWr(t.id))),
3545                    );
3546                }
3547            }
3548            None => put(&mut e, &format!("t{id}_wh"), &format!("t{id}_hw"), "host_addr", "1", "4'd0", "host_wdata", &zero_hi, &BTreeSet::from([1])),
3549        }
3550        let first = match readers.first() {
3551            Some(r0) => format!("t{id}_hr ? host_addr : e{r0}_t{id}_rd_addr"),
3552            None => "host_addr".to_string(),
3553        };
3554        // The first reader's port is also the host's, which reads an element.
3555        let mut first_runs = readers.first().map(|r0| tensor_widths(*r0, Port::TensorRd(t.id))).unwrap_or_default();
3556        first_runs.insert(1);
3557        e.l(memory_read(&format!("t{id}_rd0"), &mem, &first, width, banks, ctx.lat, &first_runs));
3558        if let Some((r0, more)) = readers.split_first() {
3559            e.l(format!("  assign e{r0}_t{id}_rd_data = t{id}_rd0;"));
3560            if banks > 1 {
3561                e.l(format!("  assign e{r0}_t{id}_rd_hi = t{id}_rd0_hi;"));
3562            }
3563            for r in more {
3564                e.l(memory_read(&format!("t{id}_rd_e{r}"), &mem, &format!("e{r}_t{id}_rd_addr"), width, banks, ctx.lat, &widths_or_element(tensor_widths(*r, Port::TensorRd(t.id)))));
3565                e.l(format!("  assign e{r}_t{id}_rd_data = t{id}_rd_e{r};"));
3566                if banks > 1 {
3567                    e.l(format!("  assign e{r}_t{id}_rd_hi = t{id}_rd_e{r}_hi;"));
3568                }
3569            }
3570        }
3571        host_words.push(format!("(host_sel == 16'd{id}) ? t{id}_rd0 :"));
3572    }
3573    // The word a host read returns, selected by the tensor it named `lat` cycles before, when its data lands.
3574    e.l(format!("  wire [15:0] host_sel;{}", delayed("host_sel", "host_tensor", 16, ctx.lat)));
3575    e.l(format!("  assign host_rdata = {} 64'd0;", host_words.join(" ")));
3576    // Tie off every buffer port no engine and no bus drives.
3577    let mut driven_prod: BTreeSet<BufferId> = BTreeSet::new();
3578    let mut driven_cons: BTreeSet<BufferId> = BTreeSet::new();
3579    let mut driven_wr: BTreeSet<BufferId> = BTreeSet::new();
3580    let mut driven_rd: BTreeSet<BufferId> = BTreeSet::new();
3581    for (_, plan) in &mine {
3582        let u = buffer_uses(&plan.micro);
3583        driven_prod.extend(u.produces.iter().copied());
3584        driven_cons.extend(u.consumes.iter().copied());
3585        driven_wr.extend(u.writes.iter().copied());
3586        driven_rd.extend(u.reads.iter().copied());
3587    }
3588    driven_wr.extend(bus_fed.keys().copied());
3589    for b in &bufs {
3590        let k = b.id.idx();
3591        if !driven_prod.contains(&b.id) {
3592            e.l(format!("  assign f{k}_reserve = 1'b0; assign f{k}_reserve_n = 0; assign f{k}_push = 1'b0; assign f{k}_push_n = 0;"));
3593        }
3594        if !driven_cons.contains(&b.id) {
3595            e.l(format!("  assign f{k}_pop = 1'b0; assign f{k}_pop_n = 0;"));
3596        }
3597        if !driven_wr.contains(&b.id) {
3598            e.l(format!("  assign f{k}_wr_en = 1'b0; assign f{k}_wr_slot = 0; assign f{k}_wr_elem = 0; assign f{k}_wr_data = 64'd0;"));
3599            if banks > 1 {
3600                e.l(format!("  assign f{k}_wr_n = 1; assign f{k}_wr_lw = 0; assign f{k}_wr_hi = 0;"));
3601            }
3602        }
3603        let nrd = readers
3604            .get(&ctx.template(b.id))
3605            .copied()
3606            .unwrap_or(1)
3607            .max(1);
3608        for r in 0..nrd {
3609            if !driven_rd.contains(&b.id) {
3610                e.l(format!(
3611                    "  assign f{k}_rd{r}_slot = 0; assign f{k}_rd{r}_elem = 0;"
3612                ));
3613            }
3614        }
3615    }
3616    // A bus-fed buffer's write port comes from the bus.
3617    for (b, g) in &bus_fed {
3618        let k = b.idx();
3619        e.l(format!("  assign f{k}_wr_en = bus_g{0}_push_b{k}; assign f{k}_wr_slot = f{k}_fill_slot0; assign f{k}_wr_elem = bus_g{0}_dout_b{k}[{1}:64]; assign f{k}_wr_data = bus_g{0}_dout_b{k}[63:0];", g.idx(), 64 + bits_for(ctx.page_elems(*b))));
3620        if banks > 1 {
3621            e.l(format!("  assign f{k}_wr_n = 1; assign f{k}_wr_lw = 0; assign f{k}_wr_hi = 0;"));
3622        }
3623    }
3624    e.l(format!(
3625        "  assign done = {};",
3626        if dones.is_empty() {
3627            "1'b1".to_string()
3628        } else {
3629            dones.join(" & ")
3630        }
3631    ));
3632    e.l("endmodule");
3633    let modules: String = pools
3634        .iter()
3635        .enumerate()
3636        .map(|(i, pool)| print_pool(&format!("pool_{}_{}_{i}", site.x, site.y), pool.words, pool.width, banks, ctx.lat, pool.nread, &pool.wr, &pool.rd))
3637        .collect();
3638    Ok(format!("{modules}{}", e.s))
3639}
3640
3641trait SemIdx {
3642    fn idx_str(&self) -> String;
3643}
3644impl SemIdx for Semaphore {
3645    fn idx_str(&self) -> String {
3646        self.id.idx().to_string()
3647    }
3648}
3649
3650fn ident(s: &str) -> String {
3651    s.chars()
3652        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
3653        .collect()
3654}
3655
3656fn print_top(ctx: &Ctx, plans: &[EnginePlan], sites: &[Site]) -> Result<String, SpatialLowerError> {
3657    let p = ctx.p;
3658    let pl = p.placement.as_ref().unwrap();
3659    let mut e = Emit { s: String::new() };
3660    // The host port: while the fabric is idle, a host writes any tensor's word — into every site's copy of it — and reads
3661    // any word back from the site it names, `lat` cycles after its address. A chip's program that halts for the host
3662    // also takes its continues (`link_go`, a pulse) and says when it is halted, and at which link.
3663    e.l("module spatial_top(input clk, input rst, input start, output done, output reg [31:0] cycles,");
3664    e.l("  input host_we, input host_re, input [15:0] host_site, input [15:0] host_tensor, input [31:0] host_addr,");
3665    if ctx.halts.is_empty() {
3666        e.l("  input [63:0] host_wdata, output [63:0] host_rdata);");
3667    } else {
3668        e.l("  input [63:0] host_wdata, output [63:0] host_rdata, input link_go, output halted, output [15:0] halt_at);");
3669        // The continues of this step: the reset that starts a step clears them.
3670        e.l("  reg [7:0] link_n; always @(posedge clk) if (rst) link_n <= 8'd0; else if (link_go) link_n <= link_n + 8'd1;");
3671    }
3672    // Cross-site wires.
3673    for (ei, plan) in plans.iter().enumerate() {
3674        for s in sem_incs(&plan.micro) {
3675            e.l(format!(
3676                "  wire e{ei}_s{0}_inc_remote; wire [15:0] e{ei}_s{0}_inc_n;",
3677                s.idx()
3678            ));
3679        }
3680        for (s, g) in mcast_valids(&plan.micro) {
3681            e.l(format!("  wire e{ei}_mv{}_g{};", s.idx(), g.idx()));
3682        }
3683        for g in mcast_sends(&plan.micro) {
3684            e.l(format!(
3685                "  wire e{ei}_mc{0}_send; wire [79:0] e{ei}_mc{0}_din;",
3686                g.idx()
3687            ));
3688        }
3689    }
3690    // Multicast buses: one per group, sender = the engine that sends on it.
3691    for g in &pl.groups {
3692        let n = g.rect.len() - 1;
3693        let sender = plans
3694            .iter()
3695            .enumerate()
3696            .find(|(_, pl)| mcast_sends(&pl.micro).contains(&g.id))
3697            .map(|(ei, _)| ei);
3698        let Some(sender) = sender else { continue };
3699        e.l(format!(
3700            "  wire [{}:0] bus_g{}_push; wire [79:0] bus_g{}_dout;",
3701            n - 1,
3702            g.id.idx(),
3703            g.id.idx()
3704        ));
3705        e.l(format!("  mcast_bus_prim_{n} mcast_bus_{}(.clk(clk), .rst(rst), .send(e{sender}_mc{0}_send), .din(e{sender}_mc{0}_din), .push(bus_g{0}_push), .dout(bus_g{0}_dout));", g.id.idx()));
3706    }
3707    let mut dones = Vec::new();
3708    let mut site_words = Vec::new();
3709    let mut halted_sites: Vec<Site> = Vec::new();
3710    for (si, site) in sites.iter().enumerate() {
3711        let mut conn = vec![
3712            ".clk(clk)".to_string(),
3713            ".rst(rst)".to_string(),
3714            ".start(start)".to_string(),
3715            format!(".done(site_done_{}_{})", site.x, site.y),
3716            ".host_we(host_we)".to_string(),
3717            format!(".host_re(host_re && host_site == 16'd{si})"),
3718            ".host_tensor(host_tensor)".to_string(),
3719            ".host_addr(host_addr)".to_string(),
3720            ".host_wdata(host_wdata)".to_string(),
3721            format!(".host_rdata(host_rdata_s{si})"),
3722        ];
3723        e.l(format!("  wire [63:0] host_rdata_s{si};"));
3724        site_words.push(format!("(host_from == 16'd{si}) ? host_rdata_s{si} :"));
3725        e.l(format!("  wire site_done_{}_{};", site.x, site.y));
3726        dones.push(format!("site_done_{}_{}", site.x, site.y));
3727        if plans.iter().any(|pl| pl.site == *site && halts_in(&pl.micro)) {
3728            e.l(format!("  wire site_halted_{0}_{1}; wire [15:0] site_halt_at_{0}_{1};", site.x, site.y));
3729            conn.push(format!(".link_n(link_n), .halted(site_halted_{0}_{1}), .halt_at(site_halt_at_{0}_{1})", site.x, site.y));
3730            halted_sites.push(*site);
3731        }
3732        // Incoming increments.
3733        for (ei, plan) in plans.iter().enumerate() {
3734            for m in &plan.micro {
3735                match m {
3736                    Micro::Fixed(Fixed::SemInc { sem, at, .. }) | Micro::Then(Fixed::SemInc { sem, at, .. }) if *at == *site => {
3737                        conn.push(format!(
3738                            ".inc_s{0}_from_e{ei}(e{ei}_s{0}_inc_remote)",
3739                            sem.idx()
3740                        ));
3741                        conn.push(format!(
3742                            ".inc_s{0}_n_from_e{ei}(e{ei}_s{0}_inc_n)",
3743                            sem.idx()
3744                        ));
3745                    }
3746                    Micro::Fixed(Fixed::McastSetValid { sem, group }) | Micro::Then(Fixed::McastSetValid { sem, group }) => {
3747                        let g = &pl.groups[group.idx()];
3748                        if g.rect.sites().contains(site) && g.sender != *site {
3749                            conn.push(format!(
3750                                ".valid_s{0}_from_e{ei}_g{1}(e{ei}_mv{0}_g{1})",
3751                                sem.idx(),
3752                                group.idx()
3753                            ));
3754                        }
3755                    }
3756                    _ => {}
3757                }
3758            }
3759        }
3760        // Outgoing pulses.
3761        for (ei, plan) in plans.iter().enumerate().filter(|(_, pl)| pl.site == *site) {
3762            for s in sem_incs(&plan.micro) {
3763                conn.push(format!(
3764                    ".e{ei}_s{0}_inc_remote(e{ei}_s{0}_inc_remote)",
3765                    s.idx()
3766                ));
3767                conn.push(format!(".e{ei}_s{0}_inc_n(e{ei}_s{0}_inc_n)", s.idx()));
3768            }
3769            for (s, g) in mcast_valids(&plan.micro) {
3770                conn.push(format!(
3771                    ".e{ei}_mv{0}_g{1}(e{ei}_mv{0}_g{1})",
3772                    s.idx(),
3773                    g.idx()
3774                ));
3775            }
3776            for g in mcast_sends(&plan.micro) {
3777                conn.push(format!(".e{ei}_mc{0}_send(e{ei}_mc{0}_send)", g.idx()));
3778                conn.push(format!(".e{ei}_mc{0}_din(e{ei}_mc{0}_din)", g.idx()));
3779            }
3780        }
3781        // Bus-fed buffers at this site.
3782        for st in &p.stages {
3783            if st.site == Some(*site) {
3784                if let StageKind::Recv { group } = st.kind {
3785                    let g = &pl.groups[group.idx()];
3786                    let receivers: Vec<Site> = g
3787                        .rect
3788                        .sites()
3789                        .into_iter()
3790                        .filter(|s| *s != g.sender)
3791                        .collect();
3792                    let idx = receivers.iter().position(|s| s == site).unwrap_or(0);
3793                    for b in &st.outs {
3794                        conn.push(format!(
3795                            ".bus_g{0}_push_b{1}(bus_g{0}_push[{idx}])",
3796                            group.idx(),
3797                            b.idx()
3798                        ));
3799                        conn.push(format!(
3800                            ".bus_g{0}_dout_b{1}(bus_g{0}_dout)",
3801                            group.idx(),
3802                            b.idx()
3803                        ));
3804                    }
3805                }
3806            }
3807        }
3808        e.l(format!(
3809            "  site_{}_{} site_inst_{}_{}({});",
3810            site.x,
3811            site.y,
3812            site.x,
3813            site.y,
3814            conn.join(", ")
3815        ));
3816    }
3817    e.l(format!("  assign done = {};", dones.join(" & ")));
3818    e.l(format!("  wire [15:0] host_from;{}", delayed("host_from", "host_site", 16, ctx.lat)));
3819    e.l(format!("  assign host_rdata = {} 64'd0;", site_words.join(" ")));
3820    e.l("  reg started;");
3821    e.l("  always @(posedge clk) begin");
3822    e.l("    if (rst) begin cycles <= 0; started <= 1'b0; end");
3823    if ctx.halts.is_empty() {
3824        e.l("    else begin if (start) started <= 1'b1; if ((start || started) && !done) cycles <= cycles + 1; end");
3825    } else {
3826        // The count pauses while the fabric waits for the host: it is the cycles the program has run.
3827        e.l("    else begin if (start) started <= 1'b1; if ((start || started) && !done && !halted) cycles <= cycles + 1; end");
3828    }
3829    e.l("  end");
3830    if !ctx.halts.is_empty() {
3831        let names = |f: &str| -> Vec<String> { halted_sites.iter().map(|s| format!("{f}_{}_{}", s.x, s.y)).collect() };
3832        e.l(format!("  assign halted = {};", names("site_halted").join(" | ")));
3833        let at: String = halted_sites.iter().map(|s| format!("site_halted_{0}_{1} ? site_halt_at_{0}_{1} : ", s.x, s.y)).collect();
3834        e.l(format!("  assign halt_at = {at}16'd0;"));
3835    }
3836    e.l("endmodule");
3837    Ok(e.s)
3838}
3839
3840fn print_testbench(ctx: &Ctx, sites: &[Site], predicted: u64) -> String {
3841    let p = ctx.p;
3842    let pl = p.placement.as_ref().unwrap();
3843    // Every result the program hands back — each `Output` and each `State` it carries to its next step, or the stored
3844    // tensor of a program that only updates in place — printed whole, each tile from the site whose work item stores
3845    // it (the items of the section that writes it, in a composition).
3846    let mut outs: Vec<&TensorDecl> =
3847        p.tensors.iter().filter(|t| matches!(t.role, TensorRole::Output | TensorRole::State)).collect();
3848    if outs.is_empty() {
3849        outs.extend(p.output_tensor());
3850    }
3851    let (tr, tc) = (ctx.tr, ctx.tc);
3852    let owner_of = |out: &TensorDecl| -> BTreeMap<usize, Site> {
3853        let section = p.sections.iter().find(|s| s.output == Some(out.id));
3854        let mut owner = BTreeMap::new();
3855        for (site, items) in &pl.items_of_site {
3856            for it in items {
3857                if section.is_some_and(|s| !(s.items.0..s.items.1).contains(&it.idx())) {
3858                    continue;
3859                }
3860                let r = p.work.items[it.idx()].out;
3861                for ty in r.row0_t..r.row0_t + r.h_t {
3862                    for tx in r.col0_t..r.col0_t + r.w_t {
3863                        owner.insert(ty * out.tiles.cols_t + tx, *site);
3864                    }
3865                }
3866            }
3867        }
3868        owner
3869    };
3870    let mut e = Emit { s: String::new() };
3871    e.l("`timescale 1ns/1ps");
3872    e.l("module tb;");
3873    e.l("  reg clk = 0, rst = 1, start = 0; wire done; wire [31:0] cycles;");
3874    e.l("  spatial_top dut(.clk(clk), .rst(rst), .start(start), .done(done), .cycles(cycles), .host_we(1'b0), .host_re(1'b0),");
3875    e.l("    .host_site(16'd0), .host_tensor(16'd0), .host_addr(32'd0), .host_wdata(64'd0), .host_rdata());");
3876    e.l("  always #5 clk = ~clk;");
3877    e.l("  integer guard;");
3878    e.l("  initial begin");
3879    e.l("    #12 rst = 0; #10 start = 1; #10 start = 0;");
3880    e.l("    guard = 0;");
3881    // The watchdog is sized from the design's OWN predicted cycle count, not a round number. A fixed
3882    // twenty-million-cycle bound is not a bound in practice: on a multi-site design that is hours of event
3883    // evaluation, so a testbench that should fail in seconds instead parks a whole test run. Eight times the
3884    // model, floored, is generous enough that a correct design never trips it and small enough that a
3885    // deadlocked one reports rather than hangs.
3886    let guard_bound = predicted.saturating_mul(8).max(20_000);
3887    e.l(format!(
3888        "    while (!done && guard < {guard_bound}) begin #10 guard = guard + 1; end"
3889    ));
3890    e.l("    #10;");
3891    e.l("    $display(\"CYCLES %0d\", cycles);");
3892    for out in outs {
3893        let owner = owner_of(out);
3894        for i in 0..out.elems.rows {
3895            for j in 0..out.elems.cols {
3896                let tile = (i / tr) * out.tiles.cols_t + j / tc;
3897                let site = owner.get(&tile).copied().unwrap_or(sites[0]);
3898                let addr = host_word(p, out, i, j);
3899                let word = memory_word(
3900                    &format!("dut.site_inst_{}_{}.mem_t{}", site.x, site.y, out.id.idx()),
3901                    addr,
3902                    tensor_width(out, &ctx.ranges),
3903                    ctx.banks,
3904                );
3905                e.l(format!("    $display(\"OUT {} {} %0d\", $signed({word}));", out.id.idx(), i * out.elems.cols + j));
3906            }
3907        }
3908    }
3909    e.l(format!("    if (done) $display(\"PASS: spatial_top done\"); else $display(\"FAIL: timeout after %0d cycles (bound {guard_bound}, model {predicted})\", cycles);"));
3910    e.l("    $finish;");
3911    e.l("  end");
3912    e.l("endmodule");
3913    e.s
3914}
3915
3916#[cfg(test)]
3917#[path = "../../../../logicaffeine_tests/unit/logicaffeine_compile/spatial/lower/fabric/tests.rs"]
3918mod tests;