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}