Skip to main content

logicaffeine_compile/spatial/lower/
mod.rs

1//! The lowering seam: every backend is a pure function of a VERIFIED program and its target, and every
2//! construct it emits is the image of an IR object (the census bijection).
3
4pub mod cuda;
5pub mod fabric;
6pub mod fabric_board;
7pub mod rdu;
8pub mod rdu_lanes;
9pub mod rdu_sdk;
10pub mod sambaflow;
11pub mod spatial_lang;
12pub mod tensix;
13
14use serde::{Deserialize, Serialize};
15
16use super::certificate::ChoreographyCertificate;
17use super::eval::ChoreoFault;
18use super::ir::*;
19use super::target::SpatialTarget;
20
21/// What the IR contains, counted — the number every artifact must reproduce.
22#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
23pub struct Census {
24    /// Distinct buffer templates (one circular buffer / ring / FIFO instance kind each).
25    pub buffer_templates: usize,
26    pub fifos: usize,
27    pub semaphores: usize,
28    pub barrier_semaphores: usize,
29    pub mcast_groups: usize,
30    pub kernels: usize,
31    pub sites: usize,
32}
33
34impl Census {
35    /// The census of an emitted Verilog artifact. A buffer TEMPLATE and an engine CLASS are module
36    /// definitions (printed once); a FIFO, a multicast bus and a site are instances (one per IR object); a
37    /// semaphore is one IR object however many sites hold a copy, so its instances are counted by id.
38    pub fn of_verilog(rtl: &str) -> Census {
39        let count = |prefix: &str| {
40            rtl.lines()
41                .filter(|l| l.trim_start().starts_with(prefix))
42                .count()
43        };
44        // A semaphore instance is `sem_prim sem_<id>_…`, or `barrier_prim sem_<id>_…` for a section rendezvous.
45        let sem_ids: std::collections::BTreeSet<&str> = rtl
46            .lines()
47            .filter_map(|l| {
48                let l = l.trim_start();
49                l.strip_prefix("sem_prim sem_")
50                    .or_else(|| l.strip_prefix("barrier_prim sem_"))
51            })
52            .filter_map(|r| r.split('_').next())
53            .collect();
54        Census {
55            buffer_templates: count("module fifo_tpl_"),
56            fifos: count("fifo_tpl_"),
57            semaphores: sem_ids.len(),
58            barrier_semaphores: count("barrier_prim "),
59            mcast_groups: count("mcast_bus_prim_"),
60            kernels: count("module engine_"),
61            sites: count("site_"),
62        }
63    }
64
65    pub fn of(p: &SpatialProgram) -> Census {
66        let templates: std::collections::BTreeSet<_> =
67            p.buffers.iter().map(|b| b.origin.unwrap_or(b.id)).collect();
68        Census {
69            buffer_templates: templates.len(),
70            // An intermediate spill is a bounded buffer with the same flow control as any other — every
71            // backend materialises one as a FIFO, so counting only `Fifo` here undercounts the hardware and
72            // makes the bijection fail against a lowering that is doing the right thing.
73            fifos: p
74                .buffers
75                .iter()
76                .filter(|b| matches!(b.kind, BufferKind::Fifo { .. } | BufferKind::Interm { .. }))
77                .count(),
78            semaphores: p.semaphores.len(),
79            barrier_semaphores: p
80                .semaphores
81                .iter()
82                .filter(|s| matches!(s.role, SemRole::Barrier { .. }))
83                .count(),
84            mcast_groups: p.placement.as_ref().map(|pl| pl.groups.len()).unwrap_or(0),
85            kernels: p.summary().engine_program_classes,
86            sites: p.sites().len(),
87        }
88    }
89}
90
91#[derive(Clone, Debug, PartialEq)]
92pub enum SpatialLowerError {
93    NotVerified {
94        phase: Phase,
95    },
96    Unsupported(&'static str),
97    GroupSpansSites {
98        group: GroupId,
99    },
100    NotNearestNeighbour {
101        buffer: BufferId,
102    },
103    TooBig {
104        what: &'static str,
105        need: usize,
106        have: usize,
107    },
108    Precision {
109        needed: crate::codegen_sva::fabric_target::Precision,
110    },
111    /// A fabric whose declared memories or arrays need more of a part's resource than it has, refused before a
112    /// line of RTL is printed. Only raised when the target names a part ([`crate::spatial::target::SpatialTarget::part`]).
113    OverCapacity(CapacityRefusal),
114    Uncertified(ChoreoFault),
115}
116
117/// A resource a programmable part carries a bounded number of.
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum FabricResource {
120    UltraRam,
121    BlockRam18,
122    LutRam,
123    Flops,
124    Dsp,
125}
126
127impl FabricResource {
128    pub fn label(self) -> &'static str {
129        match self {
130            FabricResource::UltraRam => "UltraRAM blocks",
131            FabricResource::BlockRam18 => "18Kb block RAMs",
132            FabricResource::LutRam => "LUT-RAM LUTs",
133            FabricResource::Flops => "flip-flops",
134            FabricResource::Dsp => "DSP slices",
135        }
136    }
137}
138
139/// One resource a fabric needs more of than its part has.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct Over {
142    pub resource: FabricResource,
143    /// The fewest of the resource the declared memories and arrays can take (a lower bound).
144    pub need: u64,
145    /// What the part has.
146    pub have: u64,
147    /// The memories that need the most of it, by name and bits, largest first.
148    pub largest: Vec<(String, u64)>,
149}
150
151/// Why a fabric does not fit its part, naming every resource it overruns.
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct CapacityRefusal {
154    pub part: &'static str,
155    pub over: Vec<Over>,
156}
157
158/// The neutral (SDK-gated) artifact of the RDU lane.
159#[derive(Clone, Debug)]
160pub struct NeutralArtifact {
161    pub json: String,
162    pub pretty: String,
163    pub reason: &'static str,
164    pub certificate: ChoreographyCertificate,
165    /// The certificate was rechecked against the program (it is the one the program carried); `false` when the
166    /// backend derived it.
167    pub rechecked: bool,
168    pub census: Census,
169}
170
171#[derive(Clone, Debug)]
172pub enum SpatialArtifact {
173    Tensix(tensix::TensixArtifact),
174    Verilog(fabric::VerilogArtifact),
175    Cuda(cuda::CudaArtifact),
176    SdkGated(NeutralArtifact),
177}
178
179pub trait SpatialBackend {
180    fn name(&self) -> &str;
181    fn target(&self) -> &SpatialTarget;
182    fn lower(&self, p: &SpatialProgram) -> Result<SpatialArtifact, SpatialLowerError>;
183}
184
185/// The precondition every lowering enforces.
186pub fn require_verified(p: &SpatialProgram) -> Result<(), SpatialLowerError> {
187    if p.phase() != Phase::Verified {
188        return Err(SpatialLowerError::NotVerified { phase: p.phase() });
189    }
190    Ok(())
191}
192
193/// The precondition of a printer that emits ONE kernel shape from `work.plan`: every section of the program
194/// runs under that plan. A composition's `work.plan` is its last section's, so any section lowered under
195/// another plan would be printed as the wrong kernel.
196pub fn require_one_plan(p: &SpatialProgram) -> Result<(), SpatialLowerError> {
197    if p.sections.iter().any(|s| s.plan != p.work.plan) {
198        return Err(SpatialLowerError::Unsupported("a composition whose sections run under different plans — this printer emits one kernel from one plan"));
199    }
200    Ok(())
201}