Skip to main content

logicaffeine_compile/codegen_sva/
plasticine.rs

1//! The **Plasticine** mesh descriptor — the open RDU (Prabhakar et al., ISCA 2017) and its SARA/Capstan-era
2//! successor configuration — as a physically-described target that wraps the certified [`FabricTarget`] seam.
3//!
4//! [`FabricTarget::plasticine_shaped`] models ONE PCU datapath (the placement envelope the certified placer
5//! fills); this descriptor adds what the papers publish about the MESH — how many PCUs/PMUs in what array, the
6//! PCU's lanes / stages / pipeline registers, the PMU's banks / capacity / stages, the DRAM channels and
7//! bandwidth, clock, area, power, fp32 roofline — every value cited at its constructor, and `Deref`s to the
8//! wrapped `FabricTarget` so every existing `&FabricTarget` API (placement, certification, capability gates)
9//! accepts it unchanged. Fields a paper does not publish are `None`, never guessed.
10//!
11//! P0 of the Plasticine lane: descriptor + backend. The metapipeline II model, PMU banking model, mesh
12//! placement and the Spatial/PIR emitter follow (P1–P4); until the emitter lands the backend hands back the
13//! certified neutral placement and says so.
14
15use std::ops::Deref;
16
17use super::backend::{serialize_grid, BackendArtifact, TargetBackend};
18use super::fabric_grid::Grid;
19use super::fabric_target::FabricTarget;
20use super::fabric_target::Precision;
21use crate::spatial::rdu_chip::{Fact, Provenance, RduChip, HBM_STACKS};
22use crate::spatial::roofs::{
23    ComputeRoof, Engine, MemLevel, MemRoof, Rate, Roofs, Scope, StreamMix, Transfer,
24};
25
26const ISCA17: &str = "Prabhakar et al., Plasticine, ISCA 2017 (DOI 10.1145/3079856.3080256)";
27const SARA: &str = "Zhang, Scaling a Reconfigurable Dataflow Accelerator (SARA), Stanford 2020";
28const TOOLCHAIN_LEDGER: &str = "benchmarks/silicon/plasticine_toolchain.json";
29
30/// The lithographic reticle limit, 26 mm × 33 mm — what "a max-reticle die" measures (modeled area basis).
31const RETICLE_LIMIT_MM2: f64 = 858.0;
32
33/// Off-chip memory of a Plasticine configuration: channel count, kind, and published peak bandwidth.
34#[derive(Clone, Debug, PartialEq, serde::Serialize)]
35pub struct DramSpec {
36    pub channels: usize,
37    pub kind: &'static str,
38    /// Published peak bandwidth in GB/s.
39    pub gbps: f64,
40}
41
42/// A published Plasticine mesh configuration.
43#[derive(Clone, Debug, PartialEq, serde::Serialize)]
44pub struct PlasticineTarget {
45    /// The certified per-PCU datapath descriptor (capabilities, precision, placement envelope).
46    pub base: FabricTarget,
47    /// Checkerboard array dimensions (PCUs and PMUs alternate 1:1).
48    pub pcu_rows: usize,
49    pub pcu_cols: usize,
50    /// SIMD lanes per PCU.
51    pub pcu_lanes: usize,
52    /// FU pipeline stages per PCU.
53    pub pcu_stages: usize,
54    /// Pipeline registers per stage.
55    pub pcu_regs_per_stage: usize,
56    /// Scalar / vector / control ports per PCU (in, out).
57    pub pcu_scalar_io: (usize, usize),
58    pub pcu_vector_io: (usize, usize),
59    /// PMU vector ports (in, out) — how many vector nets a scratchpad can sink / source at once.
60    pub pmu_vector_io: (usize, usize),
61    pub pcu_control_io: Option<(usize, usize)>,
62    /// Counter-chain depth per PCU, when the paper tabulates it.
63    pub pcu_counters: Option<usize>,
64    /// Scratchpad banks per PMU, PMU capacity in KiB, PMU address-datapath stages.
65    pub pmu_banks: usize,
66    pub pmu_kib: usize,
67    pub pmu_stages: usize,
68    pub dram: DramSpec,
69    pub clock_ghz: f64,
70    pub area_mm2: f64,
71    pub power_w: Option<f64>,
72    /// Published peak fp32 throughput (the chip's native format; it has no bf16 datapath).
73    pub peak_fp32_tflops: f64,
74    /// Where every number above comes from.
75    pub cite: &'static str,
76    /// What vouches for the three rates above — the DRAM bandwidth, the clock and the fp32 peak — read by
77    /// [`Self::roofs`]. The numbers live in their fields; this holds only their provenance.
78    #[serde(skip)]
79    rates: RateFacts,
80}
81
82/// The provenance of a configuration's DRAM bandwidth (with the access mix it holds for), clock and fp32
83/// peak, and the sources their citations name.
84#[derive(Clone, Copy, Debug, PartialEq)]
85struct RateFacts {
86    dram: (Provenance, StreamMix, &'static str),
87    clock: (Provenance, &'static str),
88    peak_fp32: (Provenance, &'static str),
89    sources: &'static [&'static str],
90}
91
92impl Deref for PlasticineTarget {
93    type Target = FabricTarget;
94    fn deref(&self) -> &FabricTarget {
95        &self.base
96    }
97}
98
99impl PlasticineTarget {
100    /// ISCA 2017 Plasticine (Table 3 + §3–4): 16×8 checkerboard (64 PCU + 64 PMU), PCU 16 lanes × 6 stages ×
101    /// 6 registers, 6/5 scalar and 3/3 vector ports; PMU 16 banks × 16 KB = 256 KB, 4 stages, 3 vector in /
102    /// 1 vector out; 4 × DDR3-1600 =
103    /// 51.2 GB/s; 1 GHz; 112.8 mm² at 28 nm; 49 W; 12.3 fp32 TFLOPS.
104    pub fn isca17() -> PlasticineTarget {
105        PlasticineTarget {
106            base: FabricTarget::plasticine_shaped(),
107            pcu_rows: 16,
108            pcu_cols: 8,
109            pcu_lanes: 16,
110            pcu_stages: 6,
111            pcu_regs_per_stage: 6,
112            pcu_scalar_io: (6, 5),
113            pcu_vector_io: (3, 3),
114            pmu_vector_io: (3, 1),
115            pcu_control_io: None,
116            pcu_counters: None,
117            pmu_banks: 16,
118            pmu_kib: 256,
119            pmu_stages: 4,
120            dram: DramSpec { channels: 4, kind: "DDR3-1600", gbps: 51.2 },
121            clock_ghz: 1.0,
122            area_mm2: 112.8,
123            power_w: Some(49.0),
124            peak_fp32_tflops: 12.3,
125            cite: "Prabhakar et al., Plasticine, ISCA 2017 (DOI 10.1145/3079856.3080256), Tables 3/5, §3, §5",
126            rates: RateFacts {
127                dram: (Provenance::Published, StreamMix::Any, "Prabhakar et al., Plasticine, ISCA 2017 (DOI 10.1145/3079856.3080256), Tables 3/5, §3, §5: four DDR3-1600 channels, 51.2 GB/s"),
128                clock: (Provenance::Published, "Prabhakar et al., Plasticine, ISCA 2017 (DOI 10.1145/3079856.3080256), Tables 3/5, §3, §5: 1 GHz"),
129                peak_fp32: (Provenance::Published, "Prabhakar et al., Plasticine, ISCA 2017 (DOI 10.1145/3079856.3080256), Tables 3/5, §3, §5: 12.3 fp32 TFLOPS"),
130                sources: &[ISCA17],
131            },
132        }
133    }
134
135    /// The SARA / Capstan-era configuration (Zhang, *Scaling a Reconfigurable Dataflow Accelerator*, Stanford
136    /// 2020, Table 3.3 + §4; Rucker et al., Capstan, MICRO 2021, Table 7): 20×20 checkerboard, PCU 16 lanes ×
137    /// 6 stages × 8 registers, 6/6 vector, 6/6 scalar, 16/8 control ports; PMU 16 banks × 256 KB, 10 stages;
138    /// HBM2 at 1 TB/s; 352 mm² at 28 nm. Peak fp32 scales with the PCU count (200/64 × 12.3).
139    pub fn sara_20x20() -> PlasticineTarget {
140        PlasticineTarget {
141            base: FabricTarget { name: "plasticine-sara-20x20", ..FabricTarget::plasticine_shaped() },
142            pcu_rows: 20,
143            pcu_cols: 20,
144            pcu_lanes: 16,
145            pcu_stages: 6,
146            pcu_regs_per_stage: 8,
147            pcu_scalar_io: (6, 6),
148            pcu_vector_io: (6, 6),
149            pmu_vector_io: (6, 6),
150            pcu_control_io: Some((16, 8)),
151            pcu_counters: None,
152            pmu_banks: 16,
153            pmu_kib: 256,
154            pmu_stages: 10,
155            dram: DramSpec { channels: 8, kind: "HBM2", gbps: 1000.0 },
156            clock_ghz: 1.0,
157            area_mm2: 352.0,
158            power_w: None,
159            peak_fp32_tflops: 12.3 * 200.0 / 64.0,
160            cite: "Zhang, Scaling a Reconfigurable Dataflow Accelerator (SARA), Stanford 2020, Table 3.3, §4.2; Capstan, MICRO 2021, Table 7",
161            rates: SARA_RATES,
162        }
163    }
164
165    /// The mesh the public toolchain simulates by default: PIR's `--row=20 --col=20` checkerboard (the SARA
166    /// unit shapes) in front of Tungsten's ramulator memory — `DDR4-2133`, 4 channels, `DDR4_8Gb_x16`
167    /// (`DDR4-config.cfg` in every generated app). Its effective bandwidth is what Tungsten itself reports on
168    /// a memory-bound run (`Average DRAM Read Bandwidth: 86.57 GB/s` on Spatial's `DotProduct_0`, recorded in
169    /// `benchmarks/silicon/plasticine_toolchain.json`), used as the roofline of the rows calibrated against it.
170    pub fn tungsten_20x20_ddr4() -> PlasticineTarget {
171        PlasticineTarget {
172            rates: RateFacts {
173                dram: (
174                    Provenance::Measured,
175                    StreamMix::Read,
176                    "benchmarks/silicon/plasticine_toolchain.json: Tungsten (ramulator, DDR4-2133, four channels) reports \
177                     dram_read_gbps 86.5733 on Spatial's DotProduct_0, a read stream; 86.6 at the descriptor's precision",
178                ),
179                ..SARA_RATES
180            },
181            base: FabricTarget { name: "plasticine-tungsten-20x20-ddr4", ..FabricTarget::plasticine_shaped() },
182            dram: DramSpec { channels: 4, kind: "DDR4-2133 x16, ramulator (Tungsten default config); effective 86.6 GB/s as Tungsten reports on DotProduct_0", gbps: 86.6 },
183            cite: "PIR default mesh (--row=20 --col=20) + Tungsten DDR4-config.cfg (standard DDR4, channels 4, ranks 4, DDR4_2133, DDR4_8Gb_x16); bandwidth = Tungsten's own report on DotProduct_0",
184            ..PlasticineTarget::sara_20x20()
185        }
186    }
187
188    /// The SambaNova RDU — SN40L or SN50 — as a mesh descriptor, derived from
189    /// [`RduChipFacts`](crate::spatial::rdu_chip::RduChipFacts): the per-PCU
190    /// envelope is the facts' fabric target (`rdu_shaped()` / `sn50_shaped()`, every published number and
191    /// its citation there); the checkerboard lays the facts' PCU count out on the unit model's column
192    /// aspect (40 × 26 PCUs interleaved 1:1 with PMUs = 52 × 40 units); lanes, stages, banks, address
193    /// stages and the counter chain read the unit model; the per-PMU capacity is the published SRAM spread
194    /// over the PMU count; the DRAM is the published HBM at a MODELED rate; the clock, the area, the port
195    /// counts and the register depth are modeled (cited on [`Self::rdu_provenance`]); power is not public
196    /// (`None`); fp32 is not published, so the published bf16 peak stands as the roofline and says so. The
197    /// mesh descriptor is therefore modeled through and through — the published facts live on the envelope.
198    pub fn rdu(chip: RduChip) -> PlasticineTarget {
199        let facts = chip.facts();
200        let um = &facts.unit_model;
201        let mesh = facts.mesh();
202        assert_eq!(
203            facts.pcu_count.value, facts.pmu_count.value,
204            "the 1:1 checkerboard carries one PMU per PCU; a PMU count that differs is the descriptor refinement an SDK number brings"
205        );
206        let (kind, cite) = match chip {
207            RduChip::Sn40l => (
208                "HBM (generation not public)",
209                "sambanova.ai / Hot Chips 2024 (hc2024.hotchips.org) / arXiv:2405.07518 for the envelope's published numbers; the mesh descriptor is modeled: the SN40L's 1040 PCUs and 1040 PMUs on the unit model's 40-column aspect, the PCU/PMU shapes inherited from Plasticine (Prabhakar et al., ISCA 2017), 520 MiB spread over 1040 PMUs, the co-packaged HBM at an HBM2E-class modeled rate (its generation and bandwidth are not public), a modeled 1 GHz clock, two reticle-limit dies as the area; fp32 peak not published — the 640 TFLOPS bf16 peak stands as the roofline",
210            ),
211            RduChip::Sn50 => (
212                "HBM2E",
213                "sambanova.ai/products/rdu-ai-chips / Hot Chips 2026 / ServeTheHome for the envelope's published numbers; the mesh descriptor is modeled: the PCU/PMU counts inherited from the SN40L (not public for the SN50) on the unit model's 40-column aspect, the PCU/PMU shapes inherited from Plasticine (Prabhakar et al., ISCA 2017), 432 MiB spread over 1040 PMUs, the published 64 GB HBM2E at a modeled JEDEC-rate bandwidth (not public), a modeled 1 GHz clock, two max-reticle dies as the area; fp32 peak not published — the 1600 TFLOPS bf16 peak stands as the roofline",
214            ),
215        };
216        PlasticineTarget {
217            base: facts.fabric_target(),
218            pcu_rows: 2 * mesh.y,
219            pcu_cols: mesh.x,
220            pcu_lanes: um.pcu_lanes.value,
221            pcu_stages: um.pcu_stages.value,
222            pcu_regs_per_stage: 6,
223            pcu_scalar_io: (6, 5),
224            pcu_vector_io: (3, 3),
225            pmu_vector_io: (3, 1),
226            pcu_control_io: None,
227            pcu_counters: Some(um.counter_nest.value),
228            pmu_banks: um.pmu.value.banks,
229            pmu_kib: facts.sram_mib.value * 1024 / facts.pmu_count.value.max(1),
230            pmu_stages: um.pmu.value.stages,
231            dram: DramSpec {
232                channels: HBM_STACKS,
233                kind,
234                gbps: facts.hbm_gbps().value,
235            },
236            clock_ghz: facts.clock_ghz().value,
237            area_mm2: facts.dies.value as f64 * RETICLE_LIMIT_MM2,
238            power_w: None,
239            peak_fp32_tflops: facts.peak_bf16_tflops.value as f64,
240            cite,
241            rates: RateFacts {
242                dram: (
243                    facts.hbm_gbps().provenance,
244                    StreamMix::Any,
245                    facts.hbm_gbps().cite,
246                ),
247                clock: (facts.clock_ghz().provenance, facts.clock_ghz().cite),
248                peak_fp32: (Provenance::Modeled, rdu_roofline_cite(chip)),
249                sources: facts.cites,
250            },
251        }
252    }
253
254    /// The SN40L mesh descriptor ([`Self::rdu`] on `RduChip::Sn40l`).
255    pub fn sn40l_rdu() -> PlasticineTarget {
256        PlasticineTarget::rdu(RduChip::Sn40l)
257    }
258
259    /// The SN50 mesh descriptor ([`Self::rdu`] on `RduChip::Sn50`).
260    pub fn sn50_rdu() -> PlasticineTarget {
261        PlasticineTarget::rdu(RduChip::Sn50)
262    }
263
264    /// Every numeric field of the RDU mesh descriptor with its provenance, by serialized path (the wrapped
265    /// envelope's numbers are the facts' own and are ledgered there): the value the descriptor carries, and
266    /// whether it is published or modeled, with the source or the gap it fills. Read by `rdu_loop_lane_lock`
267    /// through reflection over the serialized descriptor, so a number without a row cannot land.
268    pub fn rdu_provenance(chip: RduChip) -> Vec<(&'static str, Fact<f64>)> {
269        let t = PlasticineTarget::rdu(chip);
270        let facts = chip.facts();
271        let counts: &'static str = match chip {
272            RduChip::Sn40l => "not public as a mesh aspect; the published 1040 PCUs and 1040 PMUs (Hot Chips 2024 / arXiv:2405.07518) laid out on the unit model's modeled 40-column aspect, PMUs interleaved 1:1",
273            RduChip::Sn50 => "not public; the SN50 describes a sea of PCUs and PMUs without a count — the SN40L's 1040 + 1040 inherited, laid out on the unit model's modeled 40-column aspect, PMUs interleaved 1:1",
274        };
275        let pmu_kib: &'static str = match chip {
276            RduChip::Sn40l => "modeled: the published 520 MiB on-chip SRAM (arXiv:2405.07518) spread over the published 1040 PMUs — the per-PMU capacity is not tabulated",
277            RduChip::Sn50 => "modeled: the published 432 MB on-chip SRAM (sambanova.ai/products/rdu-ai-chips) spread over the modeled 1040 PMUs — the per-PMU capacity is not public",
278        };
279        let hbm = facts.hbm_gbps().cite;
280        let roofline = rdu_roofline_cite(chip);
281        let area: &'static str = match chip {
282            RduChip::Sn40l => "not public; modeled from the published two dies (Hot Chips 2024) at the 26 mm × 33 mm reticle limit (858 mm² each)",
283            RduChip::Sn50 => "not public; modeled from the published two max-reticle logic dies (Hot Chips 2026) at the 26 mm × 33 mm reticle limit (858 mm² each)",
284        };
285        let um = &facts.unit_model;
286        let m = |v: f64, cite: &'static str| Fact::modeled(v, cite);
287        vec![
288            ("pcu_rows", m(t.pcu_rows as f64, counts)),
289            ("pcu_cols", m(t.pcu_cols as f64, counts)),
290            ("pcu_lanes", m(t.pcu_lanes as f64, um.pcu_lanes.cite)),
291            ("pcu_stages", m(t.pcu_stages as f64, um.pcu_stages.cite)),
292            ("pcu_regs_per_stage", m(t.pcu_regs_per_stage as f64, "not public; inherited from Plasticine (Prabhakar et al., ISCA 2017) Table 3 — six pipeline registers per stage")),
293            ("pcu_scalar_io.0", m(t.pcu_scalar_io.0 as f64, "not public; inherited from Plasticine (Prabhakar et al., ISCA 2017) Table 3 — 6 scalar inputs per PCU")),
294            ("pcu_scalar_io.1", m(t.pcu_scalar_io.1 as f64, "not public; inherited from Plasticine (Prabhakar et al., ISCA 2017) Table 3 — 5 scalar outputs per PCU")),
295            ("pcu_vector_io.0", m(t.pcu_vector_io.0 as f64, "not public; inherited from Plasticine (Prabhakar et al., ISCA 2017) Table 3 — 3 vector inputs per PCU")),
296            ("pcu_vector_io.1", m(t.pcu_vector_io.1 as f64, "not public; inherited from Plasticine (Prabhakar et al., ISCA 2017) Table 3 — 3 vector outputs per PCU")),
297            ("pmu_vector_io.0", m(t.pmu_vector_io.0 as f64, "not public; inherited from Plasticine (Prabhakar et al., ISCA 2017) Table 3 — 3 vector inputs per PMU")),
298            ("pmu_vector_io.1", m(t.pmu_vector_io.1 as f64, "not public; inherited from Plasticine (Prabhakar et al., ISCA 2017) Table 3 — 1 vector output per PMU")),
299            ("pcu_counters", m(t.pcu_counters.unwrap_or(0) as f64, um.counter_nest.cite)),
300            ("pmu_banks", m(t.pmu_banks as f64, um.pmu.cite)),
301            ("pmu_kib", m(t.pmu_kib as f64, pmu_kib)),
302            ("pmu_stages", m(t.pmu_stages as f64, um.pmu.cite)),
303            ("dram.channels", m(t.dram.channels as f64, hbm)),
304            ("dram.gbps", m(t.dram.gbps, hbm)),
305            ("clock_ghz", m(t.clock_ghz, facts.clock_ghz().cite)),
306            ("area_mm2", m(t.area_mm2, area)),
307            ("peak_fp32_tflops", m(t.peak_fp32_tflops, roofline)),
308        ]
309    }
310
311    /// The configuration's roofs: its DRAM bandwidth, clock and fp32 peak, each with what vouches for it.
312    pub fn roofs(&self) -> Roofs {
313        let r = &self.rates;
314        Roofs {
315            clock_ghz: Fact {
316                value: self.clock_ghz,
317                provenance: r.clock.0,
318                cite: r.clock.1,
319            },
320            mem: vec![MemRoof {
321                level: MemLevel::Dram,
322                mix: r.dram.1,
323                scope: Scope::Chip,
324                unit: Transfer::Bytes,
325                rate: Fact {
326                    value: Rate::PerSecond(self.dram.gbps * 1e9),
327                    provenance: r.dram.0,
328                    cite: r.dram.2,
329                },
330            }],
331            compute: vec![ComputeRoof {
332                engine: Engine::Vector,
333                precision: Precision::Fp32,
334                scope: Scope::Chip,
335                ops: Fact {
336                    value: Rate::PerSecond(self.peak_fp32_tflops * 1e12),
337                    provenance: r.peak_fp32.0,
338                    cite: r.peak_fp32.1,
339                },
340            }],
341            sources: r.sources,
342        }
343    }
344
345    /// PCUs in the mesh (a 1:1 checkerboard holds as many PMUs).
346    pub fn pcus(&self) -> usize {
347        self.pcu_rows * self.pcu_cols / 2
348    }
349
350    /// PMUs in the mesh.
351    pub fn pmus(&self) -> usize {
352        self.pcu_rows * self.pcu_cols - self.pcus()
353    }
354
355    /// Total scratchpad in KiB (PMU count × PMU capacity).
356    pub fn scratchpad_kib(&self) -> usize {
357        self.pmus() * self.pmu_kib
358    }
359}
360
361/// The SARA-era configuration's rates: the published HBM2 bandwidth, Plasticine's clock (the SARA tables
362/// give none), and Plasticine's fp32 peak scaled to the PCU count.
363const SARA_RATES: RateFacts = RateFacts {
364    dram: (Provenance::Published, StreamMix::Any, "Zhang, Scaling a Reconfigurable Dataflow Accelerator (SARA), Stanford 2020, Table 3.3, §4.2: HBM2 at 1 TB/s"),
365    clock: (Provenance::Modeled, "inherited from Plasticine (Prabhakar et al., ISCA 2017 §5): 1 GHz; the SARA configuration tabulates no clock"),
366    peak_fp32: (
367        Provenance::Derived,
368        "Prabhakar et al., Plasticine, ISCA 2017 (DOI 10.1145/3079856.3080256), Tables 3/5, §3, §5: 12.3 fp32 TFLOPS on 64 PCUs, scaled to \
369         the 200 PCUs of the 20 x 20 checkerboard: 12.3 x 200 / 64",
370    ),
371    sources: &[ISCA17, SARA, TOOLCHAIN_LEDGER],
372};
373
374/// What the RDU mesh descriptor's fp32 roofline stands on: no fp32 peak is published, so the bf16 one is.
375fn rdu_roofline_cite(chip: RduChip) -> &'static str {
376    match chip {
377        RduChip::Sn40l => "fp32 peak not published; the published 640 TFLOPS bf16 peak (Hot Chips 2024) used as the roofline — modeled by that substitution",
378        RduChip::Sn50 => "fp32 peak not published; the published 1600 TFLOPS bf16 peak (sambanova.ai/products/rdu-ai-chips) used as the roofline — modeled by that substitution",
379    }
380}
381
382/// The Plasticine backend: certifies a placement TODAY (backend-agnostic SAT proof). Counted loops reach the
383/// open toolchain (Spatial → PIR → plastiroute → Tungsten, mirrored at gitlab.com/dataplane-ai) as Spatial
384/// source through [`super::spatial_emit`] and `largo emit plasticine`; a placed straight-line [`Grid`] has no
385/// Spatial/PIR form yet, so its artifact is the ready-to-bridge neutral placement with the reason stated.
386pub struct PlasticineBackend {
387    pub target: PlasticineTarget,
388}
389
390impl PlasticineBackend {
391    pub fn isca17() -> Self {
392        PlasticineBackend {
393            target: PlasticineTarget::isca17(),
394        }
395    }
396    pub fn sara_20x20() -> Self {
397        PlasticineBackend {
398            target: PlasticineTarget::sara_20x20(),
399        }
400    }
401    const PENDING_REASON: &'static str =
402        "Spatial source is emitted per counted loop by codegen_sva::spatial_emit (P4, `largo emit plasticine`); a placed straight-line Grid has no Spatial/PIR form yet — this is its certified neutral placement, not a Plasticine program";
403}
404
405impl TargetBackend for PlasticineBackend {
406    fn name(&self) -> &str {
407        self.target.name
408    }
409    fn target(&self) -> &FabricTarget {
410        &self.target.base
411    }
412    fn emit_grid(&self, _name: &str, g: &Grid) -> BackendArtifact {
413        BackendArtifact::SdkGated {
414            neutral_placement: serialize_grid(g),
415            reason: Self::PENDING_REASON,
416        }
417    }
418    fn emit_config_grid(&self, _name: &str, grids: &[&Grid], _cfg_width: u32) -> BackendArtifact {
419        let placement = grids
420            .iter()
421            .map(|g| serialize_grid(g))
422            .collect::<Vec<_>>()
423            .join("---\n");
424        BackendArtifact::SdkGated {
425            neutral_placement: placement,
426            reason: Self::PENDING_REASON,
427        }
428    }
429}