Skip to main content

logicaffeine_compile/codegen_sva/
fabric_target.rs

1//! fabric_target — the ADL spine: a `FabricTarget` descriptor + `place_with_target`.
2//!
3//! One certified lowering, retargeted across fabric shapes (the MLAR ask). A `FabricTarget` names the array
4//! size, the ALU/functional-unit capability set, and the memory depth; `place_with_target` runs the
5//! certified `route_loop_body` placer THROUGH that descriptor and pads onto the described array. Every
6//! setting is either provably `≡VM` (certified by `fabric_target_lock`, reusing `verified_placer_lock`'s SAT)
7//! or refused with a typed [`Unplaceable`] — a correct grid or a named refusal, never a silently-wrong chip.
8
9use std::collections::BTreeSet;
10
11use super::fabric_config::ConfigTable;
12use super::fabric_grid::{pad_grid, route_loop_body, Grid};
13use super::fabric_rtl::{decision_fabric_from_source, AluOp, FuncUnit, LoopCell, Src};
14use super::simd::LaneTy;
15
16/// Every combinational ALU op the fabric places (the `AluOp` universe).
17pub const ALL_ALU_OPS: &[AluOp] = &[
18    AluOp::Add,
19    AluOp::Sub,
20    AluOp::Mul,
21    AluOp::And,
22    AluOp::Or,
23    AluOp::Xor,
24    AluOp::Shl,
25    AluOp::Shr,
26    AluOp::UShr,
27];
28
29/// Every sequential functional unit the fabric places (the `FuncUnit` universe).
30pub const ALL_FUNC_UNITS: &[FuncUnit] = &[
31    FuncUnit::DivS,
32    FuncUnit::ModS,
33    FuncUnit::FloorDivS,
34    FuncUnit::Pow,
35    FuncUnit::MulHiU,
36    FuncUnit::ApproxEqF,
37    FuncUnit::FAddF,
38    FuncUnit::FSubF,
39    FuncUnit::FMulF,
40    FuncUnit::FDivF,
41    FuncUnit::FSqrtF,
42    FuncUnit::FFloorF,
43    FuncUnit::MulOvfS,
44    FuncUnit::IntToF,
45    FuncUnit::FLtF,
46    FuncUnit::FGtF,
47    FuncUnit::FLeF,
48    FuncUnit::FGeF,
49    FuncUnit::FEqF,
50    FuncUnit::FNeF,
51    FuncUnit::PopCount,
52];
53
54/// The GEMM dataflow a target realizes. `Reduction` is the universal baseline — the 1-D dot-product MAC lane
55/// every fabric ships today; `WeightStationary` / `OutputStationary` are the 2-D systolic modes (Phase 1+).
56#[derive(
57    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
58)]
59pub enum Dataflow {
60    /// 1-D unrolled dot-product → one accumulator (today's MAC lane). Every target has it.
61    Reduction,
62    /// 2-D systolic array: weights stationary per PE, activations streamed, partial sums accumulate spatially.
63    WeightStationary,
64    /// 2-D systolic array: outputs stationary per PE, weights + activations streamed.
65    OutputStationary,
66}
67
68/// A numeric format a target's compute realizes (the precision dial, Phase 4).
69///
70/// `Int32` is the integer type a 32-bit datapath computes indices and addresses in — the SN40L's PCU
71/// supports BF16, FP32, INT32 and INT8 and nothing wider, so a gathered row, an arg-max or a block start
72/// lowered for an RDU rides `Int32`, never `Int64`.
73#[derive(
74    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
75)]
76pub enum Precision {
77    Int8,
78    Int16,
79    Int32,
80    Int64,
81    Fp8,
82    Bf16,
83    Fp16,
84    Fp32,
85}
86
87/// A tier of the memory hierarchy a schedule demands (Phase 5).
88#[derive(
89    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
90)]
91pub enum MemTier {
92    /// On-chip scratchpad SRAM (the PMU-equivalent).
93    Sram,
94    /// On-package HBM.
95    Hbm,
96    /// Off-package DDR.
97    Ddr,
98}
99
100/// The on-/off-chip memory hierarchy a target provides, in the SN40L three-tier shape (Phase 5).
101#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
102pub struct MemHierarchy {
103    pub sram_kib: usize,
104    pub hbm_gib: usize,
105    pub ddr_gib: usize,
106}
107
108impl MemHierarchy {
109    /// No memory hierarchy at all — a pure combinational scalar array.
110    pub const NONE: MemHierarchy = MemHierarchy {
111        sram_kib: 0,
112        hbm_gib: 0,
113        ddr_gib: 0,
114    };
115}
116
117/// A capability a program cell requires from a target — the unit of the ADL's typed refusal.
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum Capability {
120    /// A combinational ALU op (`LoopCell::Arith`).
121    Alu(AluOp),
122    /// A sequential functional unit (`LoopCell::Func` — division / float / popcount / …).
123    Func(FuncUnit),
124    /// The predicated-select mux (`LoopCell::Select`) — every fabric carries it.
125    Select,
126    /// Any state-plane access (`LoopCell::{MemRead,HeapRead,HeapWrite,HeapAlloc}`) — needs a heap.
127    Memory,
128    /// A packed-SIMD tile (`LoopCell::VectorArith`) of a given lane type — needs a matching `target.simd`.
129    Simd(LaneTy),
130    /// The GEMM dataflow a lowering demands (`Dataflow::Reduction` is the universal baseline).
131    Dataflow(Dataflow),
132    /// A numeric MAC format a lowering demands.
133    Precision(Precision),
134    /// A memory tier a schedule demands (on-chip SRAM / HBM / DDR).
135    MemTier(MemTier),
136    /// A fusion window a lowering demands — the target must fuse at least this many ops into one stage.
137    Fusion(usize),
138    /// A metapipeline depth a lowering demands — the target sustains at least this many pipelined stages of
139    /// compute between buffers (Plasticine: the PCU's pipeline stages; a scalar array: 1).
140    Metapipeline(usize),
141    /// A scratchpad banking factor a lowering demands — the target's memory unit serves at least this many
142    /// concurrent lane accesses per cycle (Plasticine PMU: 16 banks; a flat heap: 1).
143    Banks(usize),
144    /// A nested-counter depth a lowering demands — the loop-nest depth the target's control can drive without
145    /// a CPU (Plasticine: the PCU's counter chain; at least 1 everywhere).
146    CounterNest(usize),
147}
148
149/// Why a program cannot be placed on a given target — a TYPED refusal, never a silently-wrong grid.
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub enum Unplaceable {
152    /// The target lacks a capability the program needs; `needed` names exactly what is missing.
153    MissingCapability {
154        needed: Capability,
155        target: &'static str,
156    },
157    /// The placed dataflow is larger than the described array.
158    TooSmall {
159        needed_rows: usize,
160        needed_cols: usize,
161        target_rows: usize,
162        target_cols: usize,
163        target: &'static str,
164    },
165}
166
167/// Real-silicon facts about a *physical* target that do not map onto the placement fields (which model the
168/// per-datapath array). Present only for a target backed by a real chip; `None` for the abstract presets. Every
169/// value must be a published spec cited at the constructor — this is descriptor metadata, not a claim we place.
170#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
171pub struct SiliconSpec {
172    /// Compute-tile count (e.g. SambaNova PCUs).
173    pub compute_tiles: usize,
174    /// Memory-tile count (e.g. SambaNova PMUs).
175    pub mem_tiles: usize,
176    /// Process node in nanometres.
177    pub node_nm: u32,
178    /// Published peak bf16 throughput in PFLOPS.
179    pub peak_bf16_pflops: f64,
180}
181
182/// A described fabric — the ADL. One certified lowering (`place_with_target`) retargets across shapes by
183/// varying this descriptor; each setting is either provably `≡VM` or refused with an [`Unplaceable`].
184#[derive(Clone, Debug, PartialEq, serde::Serialize)]
185pub struct FabricTarget {
186    /// Human name (carried into refusal errors).
187    pub name: &'static str,
188    /// Described array dimensions.
189    pub rows: usize,
190    pub cols: usize,
191    /// Datapath bit width (metadata for the reconfig/SIMD proofs; the SAT proof width is separate).
192    pub width: u32,
193    /// The combinational ALU ops this target provides.
194    pub alu_ops: BTreeSet<AluOp>,
195    /// The sequential functional units this target provides.
196    pub func_units: BTreeSet<FuncUnit>,
197    /// Heap depth in words; `0` means no state plane (memory cells are refused).
198    pub mem_depth: usize,
199    /// Whether the target carries a decoupled address-generation unit (Phase 1).
200    pub agu: bool,
201    /// The packed-SIMD lane configuration this target provides, if any (Phase 2). `None` ⇒ scalar-only.
202    pub simd: Option<LaneTy>,
203    /// The GEMM dataflow modes this target realizes — always includes `Dataflow::Reduction` (the baseline).
204    pub dataflows: BTreeSet<Dataflow>,
205    /// The numeric MAC formats this target realizes (the precision dial).
206    pub precisions: BTreeSet<Precision>,
207    /// The memory hierarchy this target provides (on-chip SRAM / HBM / DDR).
208    pub mem: MemHierarchy,
209    /// How many ops the target fuses into one spatial pipeline stage (`1` = no cross-op fusion).
210    pub fusion_window: usize,
211    /// Real-chip metadata (compute/memory tiles, node, roofline) for a physically-backed target; `None` for the
212    /// abstract presets. Cited to public sources at the constructor.
213    pub silicon: Option<SiliconSpec>,
214    /// Pipelined compute stages between buffers the target sustains (`1` = a single combinational stage).
215    pub metapipeline_depth: usize,
216    /// Scratchpad banks a memory unit serves concurrently (`1` = a flat single-port heap).
217    pub banks: usize,
218    /// Loop-nest depth the target's own control (counter chain) drives without a CPU (`1` = one loop).
219    pub counter_nest: usize,
220}
221
222impl FabricTarget {
223    /// The superset target: every ALU op + every functional unit + a heap. The "can place anything that fits"
224    /// shape used to certify the placer without capability refusals getting in the way.
225    pub fn full(rows: usize, cols: usize, width: u32) -> FabricTarget {
226        FabricTarget {
227            name: "full",
228            rows,
229            cols,
230            width,
231            alu_ops: ALL_ALU_OPS.iter().copied().collect(),
232            func_units: ALL_FUNC_UNITS.iter().copied().collect(),
233            mem_depth: 1 << 16,
234            agu: true,
235            simd: Some(LaneTy::I32x2),
236            dataflows: [
237                Dataflow::Reduction,
238                Dataflow::WeightStationary,
239                Dataflow::OutputStationary,
240            ]
241            .into_iter()
242            .collect(),
243            // Only WITNESSED precisions are advertised (int8/int16/int64 + bf16 + fp32 — each pinned by
244            // `precision_gemm_lock`); Fp8/Fp16 stay enum variants but are unclaimed until their witness ships —
245            // the precision ratchet stays honest.
246            precisions: [
247                Precision::Int8,
248                Precision::Int16,
249                Precision::Int64,
250                Precision::Bf16,
251                Precision::Fp32,
252            ]
253            .into_iter()
254            .collect(),
255            mem: MemHierarchy {
256                sram_kib: 1024,
257                hbm_gib: 64,
258                ddr_gib: 1536,
259            },
260            fusion_window: 64,
261            silicon: None,
262            metapipeline_depth: 1,
263            banks: 1,
264            counter_nest: 1,
265        }
266    }
267
268    /// An ALU-only target with exactly the given op set — no functional units, no heap. For refusal tests and
269    /// minimal wearable-class arrays.
270    pub fn with_alu_ops(
271        name: &'static str,
272        rows: usize,
273        cols: usize,
274        width: u32,
275        ops: &[AluOp],
276    ) -> FabricTarget {
277        FabricTarget {
278            name,
279            rows,
280            cols,
281            width,
282            alu_ops: ops.iter().copied().collect(),
283            func_units: BTreeSet::new(),
284            mem_depth: 0,
285            agu: false,
286            simd: None,
287            dataflows: [Dataflow::Reduction].into_iter().collect(),
288            precisions: [Precision::Int64].into_iter().collect(),
289            mem: MemHierarchy::NONE,
290            fusion_window: 1,
291            silicon: None,
292            metapipeline_depth: 1,
293            banks: 1,
294            counter_nest: 1,
295        }
296    }
297
298    /// TINY — a pure combinational scalar array: full ALU, no functional units, no state plane. The
299    /// smallest useful knob (branchless arithmetic kernels only).
300    pub fn tiny() -> FabricTarget {
301        FabricTarget {
302            name: "tiny",
303            rows: 8,
304            cols: 8,
305            width: 64,
306            alu_ops: ALL_ALU_OPS.iter().copied().collect(),
307            func_units: BTreeSet::new(),
308            mem_depth: 0,
309            agu: false,
310            simd: None,
311            dataflows: [Dataflow::Reduction].into_iter().collect(),
312            precisions: [Precision::Int64].into_iter().collect(),
313            mem: MemHierarchy::NONE,
314            fusion_window: 1,
315            silicon: None,
316            metapipeline_depth: 1,
317            banks: 1,
318            counter_nest: 1,
319        }
320    }
321
322    /// WEARABLE — the realistic edge target: full ALU + every functional unit (division + the float lane) +
323    /// a heap + a decoupled AGU. The HiVec-class knob these levers were built for.
324    pub fn wearable() -> FabricTarget {
325        FabricTarget {
326            name: "wearable",
327            rows: 16,
328            cols: 16,
329            width: 64,
330            alu_ops: ALL_ALU_OPS.iter().copied().collect(),
331            func_units: ALL_FUNC_UNITS.iter().copied().collect(),
332            mem_depth: 1 << 16,
333            agu: true,
334            simd: Some(LaneTy::I32x2),
335            dataflows: [Dataflow::Reduction].into_iter().collect(),
336            precisions: [Precision::Int8, Precision::Int16, Precision::Int64]
337                .into_iter()
338                .collect(),
339            mem: MemHierarchy {
340                sram_kib: 64,
341                hbm_gib: 0,
342                ddr_gib: 0,
343            },
344            fusion_window: 1,
345            silicon: None,
346            metapipeline_depth: 1,
347            banks: 1,
348            counter_nest: 1,
349        }
350    }
351
352    /// WIDE — the superset knob: everything the fabric can place, on a large array. Nothing is refused for
353    /// lack of capability (only for lack of room).
354    pub fn wide() -> FabricTarget {
355        FabricTarget {
356            name: "wide",
357            ..FabricTarget::full(32, 32, 64)
358        }
359    }
360
361    /// RDU-SHAPED — a descriptor modeling the SambaNova **SN40L** RDU, from published specs. The PCU is
362    /// configurable as a weight-stationary / output-stationary systolic GEMM OR a SIMD-elementwise pipeline
363    /// (both dataflows + reduction), fed by a three-tier SRAM/HBM/DDR hierarchy (PMU scratchpad + package
364    /// memory) with deep operator fusion. Our certified lowering places onto THIS shape — the pitch target.
365    ///
366    /// Public SN40L specs (each pinned by `sn40l_target_lock`):
367    /// - **1040 PCUs + 1040 PMUs**, **TSMC 5 nm**, **10.2 bf16 PFLOPS** roofline → [`SiliconSpec`].
368    /// - Three-tier memory: **520 MiB** on-chip SRAM, **64 GiB** co-packaged HBM, **1.5 TiB** DDR.
369    /// - Datatypes BF16 / FP32 / INT32 / INT8; we advertise only the precisions our fabric places AND witnesses
370    ///   (Int8 + Bf16). FP32/INT32 are chip-supported but witness-pending — NOT gated as available, to keep the
371    ///   precision ratchet honest.
372    /// Sources: SambaNova Hot Chips 2024 (hc2024.hotchips.org) · arXiv 2405.07518 · ServeTheHome · sambanova.ai.
373    ///
374    /// `(rows, cols)` models the placement envelope of ONE datapath, not the 1040-PCU mesh (that is
375    /// `silicon.compute_tiles`); the per-PCU systolic dimension is not public.
376    ///
377    /// Derived from [`RduChipFacts::sn40l`](crate::spatial::rdu_chip::RduChipFacts::sn40l): the published
378    /// numbers above live there with their citations, and the per-PCU pipeline depth / PMU banking /
379    /// counter chain — not public — come from the facts' unit model, labeled modeled and cited to the open
380    /// RDU (Plasticine, ISCA 2017) they are inherited from.
381    pub fn rdu_shaped() -> FabricTarget {
382        crate::spatial::rdu_chip::RduChipFacts::sn40l().fabric_target()
383    }
384
385    /// SN50-SHAPED — the SambaNova **SN50** RDU, derived from
386    /// [`RduChipFacts::sn50`](crate::spatial::rdu_chip::RduChipFacts::sn50): the same PCU capability
387    /// envelope as the SN40L (systolic + SIMD dataflows, the three memory tiers, a decoupled AGU, deep
388    /// fusion), with the SN50's published memory — **432 MB** on-chip SRAM, **64 GB** HBM2E, DDR5 up to
389    /// **512 GB** — and a **1.6 bf16 PFLOPS** per-RDU roofline at **TSMC 5 nm**. The PCU/PMU counts are not
390    /// public and are inherited from the SN40L (modeled, so labeled on the mesh descriptor). Only the
391    /// witnessed precisions (int8 + bf16) are advertised; the chip's FP8 and FP32 are witness-pending and
392    /// refused by name until their witness ships.
393    pub fn sn50_shaped() -> FabricTarget {
394        crate::spatial::rdu_chip::RduChipFacts::sn50().fabric_target()
395    }
396
397    /// PLASTICINE — the open RDU: Prabhakar et al., *Plasticine: A Reconfigurable Architecture for Parallel
398    /// Patterns*, ISCA 2017 (DOI 10.1145/3079856.3080256). A 16×8 checkerboard of **64 PCUs + 64 PMUs** at
399    /// **28 nm** (112.8 mm², 49 W, 1 GHz, 12.3 fp32 TFLOPS, 16 MB scratchpad). A PCU is a **6-stage × 16-lane
400    /// SIMD pipeline of 32-bit FUs** (integer + fp32) with a cross-lane reduction tree and a counter chain; a
401    /// PMU is a **16-bank × 16 KB = 256 KB** scratchpad with an address-calculation datapath (strided / FIFO /
402    /// line-buffer / duplication banking, N-buffering). Interconnect: statically configured scalar / vector /
403    /// control networks. Off-chip: 4 DDR3-1600 channels, 51.2 GB/s (bandwidth on the mesh descriptor,
404    /// [`super::plasticine::PlasticineTarget`]; DRAM capacity is unpublished, so no `ddr_gib` is claimed).
405    ///
406    /// Capability envelope, honestly: the PCU realizes `Reduction` (SIMD lanes + tree) — it is NOT a systolic
407    /// array, so weight-/output-stationary modes are refused; the FUs are 32-bit, so the only advertised
408    /// precision is the witnessed **fp32** (no int8/bf16 MAC exists). `(rows, cols)` = one PCU datapath
409    /// envelope (stages × lanes); the mesh lives on `silicon` + the mesh descriptor. Peak bf16 is 0: the
410    /// chip has no bf16 datapath.
411    pub fn plasticine_shaped() -> FabricTarget {
412        FabricTarget {
413            name: "plasticine-isca17",
414            rows: 6,
415            cols: 16,
416            width: 32,
417            alu_ops: ALL_ALU_OPS.iter().copied().collect(),
418            func_units: ALL_FUNC_UNITS.iter().copied().collect(),
419            mem_depth: 256 * 1024 / 4, // one PMU: 256 KB of 32-bit words
420            agu: true,                 // the PMU's address-calculation datapath
421            simd: Some(LaneTy::I32x2), // 16-lane 32-bit SIMD (our packed lane type models a 32-bit lane)
422            dataflows: [Dataflow::Reduction].into_iter().collect(),
423            precisions: [Precision::Fp32].into_iter().collect(),
424            mem: MemHierarchy {
425                sram_kib: 16 * 1024,
426                hbm_gib: 0,
427                ddr_gib: 0,
428            },
429            fusion_window: 6, // up to six chained FU stages fuse inside one PCU
430            silicon: Some(SiliconSpec {
431                compute_tiles: 64,
432                mem_tiles: 64,
433                node_nm: 28,
434                peak_bf16_pflops: 0.0,
435            }),
436            metapipeline_depth: 6,
437            banks: 16,
438            counter_nest: 1, // the counter chain's depth is not tabulated; one counter is certain
439        }
440    }
441
442    /// Whether this target provides a required capability.
443    pub fn provides(&self, cap: Capability) -> bool {
444        match cap {
445            Capability::Alu(op) => self.alu_ops.contains(&op),
446            Capability::Func(u) => self.func_units.contains(&u),
447            Capability::Select => true,
448            Capability::Memory => self.mem_depth > 0,
449            Capability::Simd(ty) => self.simd == Some(ty),
450            Capability::Dataflow(d) => self.dataflows.contains(&d),
451            Capability::Precision(p) => self.precisions.contains(&p),
452            Capability::MemTier(t) => match t {
453                MemTier::Sram => self.mem.sram_kib > 0,
454                MemTier::Hbm => self.mem.hbm_gib > 0,
455                MemTier::Ddr => self.mem.ddr_gib > 0,
456            },
457            Capability::Fusion(n) => self.fusion_window >= n,
458            Capability::Metapipeline(n) => self.metapipeline_depth >= n,
459            Capability::Banks(n) => self.banks >= n,
460            Capability::CounterNest(n) => self.counter_nest >= n,
461        }
462    }
463
464    /// The capability half of [`place_with_target`]: every cell's required capability is provided, or
465    /// the FIRST missing one is named. A program larger than one datapath envelope (a whole decision
466    /// program spanning many PCUs) is gated here cell by cell; its geometry is the mesh's question.
467    pub fn require_cells(&self, cells: &[LoopCell]) -> Result<(), Unplaceable> {
468        for cell in cells {
469            let cap = required_capability(cell);
470            if !self.provides(cap) {
471                return Err(Unplaceable::MissingCapability {
472                    needed: cap,
473                    target: self.name,
474                });
475            }
476        }
477        Ok(())
478    }
479
480    /// Gate a requested `(dataflow, precision)` GEMM against this target — the check Phase-1+ lowerings call
481    /// before placing a systolic/precision datapath. Returns a typed [`Unplaceable::MissingCapability`] naming
482    /// the FIRST dial the target lacks (dataflow before precision), or `Ok(())` if both are provided.
483    pub fn require_gemm(
484        &self,
485        dataflow: Dataflow,
486        precision: Precision,
487    ) -> Result<(), Unplaceable> {
488        for cap in [
489            Capability::Dataflow(dataflow),
490            Capability::Precision(precision),
491        ] {
492            if !self.provides(cap) {
493                return Err(Unplaceable::MissingCapability {
494                    needed: cap,
495                    target: self.name,
496                });
497            }
498        }
499        Ok(())
500    }
501}
502
503/// The capability a program cell demands of a target. Exhaustive over `LoopCell`, so a new cell kind forces a
504/// deliberate capability decision here rather than silently placing on an incapable target.
505fn required_capability(cell: &LoopCell) -> Capability {
506    match cell {
507        LoopCell::Arith { op, .. } => Capability::Alu(*op),
508        LoopCell::Func { unit, .. } => Capability::Func(*unit),
509        LoopCell::VectorArith { ty, .. } => Capability::Simd(*ty),
510        LoopCell::Select { .. } => Capability::Select,
511        LoopCell::MemRead { .. }
512        | LoopCell::HeapRead { .. }
513        | LoopCell::HeapWrite { .. }
514        | LoopCell::HeapAlloc { .. } => Capability::Memory,
515    }
516}
517
518/// Place a dataflow body onto a DESCRIBED target: gate every cell against the target's capability set, place
519/// with the certified `route_loop_body`, gate the placed shape against the described array, then pad onto it.
520/// Returns the placed+padded [`Grid`], or a typed [`Unplaceable`] — a correct grid or a named refusal.
521pub fn place_with_target(
522    cells: &[LoopCell],
523    n_in: usize,
524    next: &[Src],
525    target: &FabricTarget,
526) -> Result<Grid, Unplaceable> {
527    target.require_cells(cells)?;
528    let g = route_loop_body(cells, n_in, next);
529    if g.rows > target.rows || g.cols > target.cols {
530        return Err(Unplaceable::TooSmall {
531            needed_rows: g.rows,
532            needed_cols: g.cols,
533            target_rows: target.rows,
534            target_cols: target.cols,
535            target: target.name,
536        });
537    }
538    Ok(pad_grid(&g, target.rows, target.cols))
539}
540
541/// Compile a whole program corpus onto ONE described array: if-convert each `(source, function)` to a decision
542/// island and place it THROUGH `target`, so every island shares the described shape. The resulting
543/// [`ConfigTable`] is certifiable by [`super::fabric_config::certify_config_table`] — `config == k ⇒ the
544/// described array ≡ island k`, for ALL inputs — the ADL's retarget-a-corpus-onto-a-chosen-shape payoff.
545/// Returns a typed [`Unplaceable`] if any datapath needs a capability the target lacks or does not fit.
546pub fn compile_to_config_table_on_target(
547    sources: &[(&str, &str)],
548    target: &FabricTarget,
549    width: u32,
550    cfg_width: u32,
551) -> Result<ConfigTable, Unplaceable> {
552    assert!(
553        !sources.is_empty(),
554        "a config table needs at least one datapath"
555    );
556    assert!(
557        (1usize << cfg_width) >= sources.len(),
558        "cfg_width {cfg_width} cannot index {} datapaths",
559        sources.len()
560    );
561    let mut grids = Vec::with_capacity(sources.len());
562    for (src, name) in sources {
563        let df = decision_fabric_from_source(src, name);
564        grids.push(place_with_target(&df.cells, df.n_in, &[df.result], target)?);
565    }
566    Ok(ConfigTable {
567        grids,
568        rows: target.rows,
569        cols: target.cols,
570        width,
571        cfg_width,
572    })
573}