1use std::collections::BTreeMap;
13
14use serde::Deserialize;
15
16use crate::dims::DimError;
17use crate::format::FormatError;
18use crate::ir::OpParseError;
19
20#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22pub enum CircuitError {
23 #[error("circuit TOML: {0}")]
25 Parse(String),
26 #[error("block `{block}` node `{node}`: {source}")]
28 Op {
29 block: String,
31 node: String,
33 source: OpParseError,
35 },
36 #[error("block `{block}` edge `{edge}`: {source}")]
38 Format {
39 block: String,
41 edge: String,
43 source: FormatError,
45 },
46 #[error("block `{block}` edge `{edge}`: `{format}` is a weight layout, not an edge format")]
48 WeightFormatOnEdge {
49 block: String,
51 edge: String,
53 format: String,
55 },
56 #[error("block `{block}` edge `{edge}`: {detail}")]
58 Shape {
59 block: String,
61 edge: String,
63 detail: String,
65 },
66 #[error("block `{block}` state `{state}`: {detail}")]
68 State {
69 block: String,
71 state: String,
73 detail: String,
75 },
76 #[error("block `{block}`: {source}")]
78 Dim {
79 block: String,
81 source: DimError,
83 },
84 #[error("block `{block}` node `{node}` reads `{edge}`, which no earlier node produces")]
86 DanglingInput {
87 block: String,
89 node: String,
91 edge: String,
93 },
94 #[error("edge `{0}` is produced but never read, and is not a declared output")]
96 DanglingOutput(String),
97 #[error("block `{block}`: `{name}` is defined twice")]
99 Duplicate {
100 block: String,
102 name: String,
104 },
105 #[error(
107 "node `{node}` ({op}) does not accept `{edge}` as {format}{}",
108 expected.as_ref().map(|e| format!(" (expects {e})")).unwrap_or_default()
109 )]
110 FormatMismatch {
111 node: String,
113 op: String,
115 edge: String,
117 format: String,
119 expected: Option<String>,
121 },
122 #[error("node `{node}`: bound modules resolve to different formats ({detail})")]
124 MixedPrecision {
125 node: String,
127 detail: String,
129 },
130 #[error("node `{node}`: {detail}")]
133 Binding {
134 node: String,
136 detail: String,
138 },
139 #[error("layout: {0}")]
142 Layout(String),
143 #[error("include `{name}`: {detail}")]
146 Include {
147 name: String,
149 detail: String,
151 },
152 #[error("arch shape: {0}")]
155 ShapeMismatch(String),
156}
157
158#[derive(Deserialize)]
159#[serde(deny_unknown_fields)]
160pub(crate) struct CircuitFile {
161 pub schema: u32,
162 pub arch: String,
163 pub description: String,
164 pub layer_module: String,
165 pub include: Vec<String>,
166 pub dims: Vec<String>,
167 pub layout: LayoutFile,
168 pub prologue: Vec<String>,
169 pub epilogue: Vec<String>,
170 pub draft: Vec<String>,
171 pub draft_module: Option<String>,
172 pub draft_when: Option<String>,
175 #[serde(default)]
179 pub draft_variant: BTreeMap<String, Vec<String>>,
180 pub block: BTreeMap<String, BlockFile>,
181 #[serde(skip)]
184 pub local_blocks: std::collections::BTreeSet<String>,
185}
186
187#[derive(Deserialize)]
188#[serde(deny_unknown_fields)]
189pub(crate) struct LayoutFile {
190 pub kind: String,
191 pub period: Option<usize>,
192 pub blocks: BTreeMap<String, Vec<String>>,
193 #[serde(default)]
196 pub when: BTreeMap<String, BTreeMap<String, Vec<String>>>,
197}
198
199#[derive(Deserialize)]
200#[serde(deny_unknown_fields)]
201pub(crate) struct BlockFile {
202 pub stream_in: Option<String>,
203 pub stream_out: Option<String>,
204 #[serde(default)]
205 pub outputs: Vec<OutputFile>,
206 pub node: Vec<NodeFile>,
207 #[serde(default)]
209 pub state: Vec<StateFile>,
210}
211
212#[derive(Deserialize)]
215#[serde(untagged)]
216pub(crate) enum OutputFile {
217 Bare(String),
218 Bound { edge: String, buffer: String },
219}
220
221impl OutputFile {
222 pub fn edge(&self) -> &str {
224 match self {
225 Self::Bare(e) | Self::Bound { edge: e, .. } => e,
226 }
227 }
228}
229
230#[derive(Deserialize)]
237#[serde(deny_unknown_fields)]
238pub(crate) struct StateFile {
239 pub id: String,
240 pub kind: String,
241 pub lifetime: String,
243 pub format: String,
244 pub shape: Option<String>,
245 pub verify: Option<String>,
246 pub of: Option<String>,
247}
248
249#[derive(Deserialize)]
250#[serde(deny_unknown_fields)]
251pub(crate) struct NodeFile {
252 pub id: String,
253 pub op: String,
254 #[serde(default)]
256 pub state: BTreeMap<String, String>,
257 pub role: Option<String>,
258 pub format: Option<String>,
259 #[serde(default, rename = "in")]
260 pub inputs: Vec<String>,
261 pub out: Vec<EdgeFile>,
262 #[serde(default)]
263 pub binding: Vec<String>,
264 #[serde(default)]
265 pub params: BTreeMap<String, String>,
266 pub when: Option<String>,
270 #[serde(default)]
272 pub params_when: BTreeMap<String, BTreeMap<String, String>>,
273 #[serde(default)]
277 pub unquantized: bool,
278}
279
280#[derive(Deserialize)]
281#[serde(deny_unknown_fields)]
282pub(crate) struct EdgeFile {
283 pub edge: String,
284 pub format: String,
285 pub shape: String,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
290pub enum LayoutRule {
291 Interval {
294 period: usize,
296 },
297 List,
299}
300
301#[derive(Deserialize)]
304#[serde(deny_unknown_fields)]
305struct BlocksFile {
306 schema: u32,
307 #[allow(dead_code)]
308 description: String,
309 block: BTreeMap<String, BlockFile>,
310}
311
312pub fn includes_of(text: &str) -> Result<Vec<String>, CircuitError> {
315 Ok(parse_one(text)?.include)
316}
317
318fn parse_one(text: &str) -> Result<CircuitFile, CircuitError> {
319 let file: CircuitFile = toml::from_str(text).map_err(|e| CircuitError::Parse(e.to_string()))?;
320 if file.schema != 1 {
321 return Err(CircuitError::Parse(format!(
322 "schema {} (this build reads 1)",
323 file.schema
324 )));
325 }
326 Ok(file)
327}
328
329pub(crate) fn parse_file(
333 text: &str,
334 includes: &[(&str, &str)],
335) -> Result<CircuitFile, CircuitError> {
336 let mut file = parse_one(text)?;
337 file.local_blocks = file.block.keys().cloned().collect();
338 for name in file.include.clone() {
339 let err = |detail: String| CircuitError::Include {
340 name: name.clone(),
341 detail,
342 };
343 let lib_text = includes
344 .iter()
345 .find(|(n, _)| *n == name)
346 .map(|(_, t)| *t)
347 .ok_or_else(|| err("not supplied".into()))?;
348 let lib: BlocksFile = toml::from_str(lib_text).map_err(|e| err(e.to_string()))?;
349 if lib.schema != 1 {
350 return Err(err(format!("schema {} (this build reads 1)", lib.schema)));
351 }
352 for (block, tpl) in lib.block {
353 if file.block.contains_key(&block) {
354 return Err(err(format!("block `{block}` is defined twice")));
355 }
356 file.block.insert(block, tpl);
357 }
358 }
359 Ok(file)
360}
361
362pub(crate) fn layout_rule(l: &LayoutFile) -> Result<LayoutRule, CircuitError> {
363 match (l.kind.as_str(), l.period) {
364 ("interval", Some(p)) if p > 0 => Ok(LayoutRule::Interval { period: p }),
365 ("interval", _) => Err(CircuitError::Layout(
366 "`interval` needs a `period` of at least 1".into(),
367 )),
368 ("list", None) => Ok(LayoutRule::List),
369 ("list", Some(_)) => Err(CircuitError::Layout("`list` takes no `period`".into())),
370 (other, _) => Err(CircuitError::Layout(format!(
371 "unknown layout kind `{other}` (interval | list)"
372 ))),
373 }
374}
375
376pub(crate) fn when_holds(
379 expr: &str,
380 declared: &[String],
381 dims: &BTreeMap<String, u64>,
382) -> Result<bool, CircuitError> {
383 let (negate, name) = match expr.trim().strip_prefix('!') {
384 Some(n) => (true, n.trim()),
385 None => (false, expr.trim()),
386 };
387 if !declared.iter().any(|d| d == name) {
388 return Err(CircuitError::ShapeMismatch(format!(
389 "switch `{expr}` names `{name}`, which is not in the circuit's `dims` list"
390 )));
391 }
392 let v = dims.get(name).copied().ok_or_else(|| {
393 CircuitError::ShapeMismatch(format!("switch `{expr}`: the arch shape lacks `{name}`"))
394 })?;
395 Ok((v != 0) != negate)
396}
397
398pub(crate) fn split_shape(s: &str) -> Option<(&str, &str)> {
400 let (rows, dim) = s.split_once(" x ")?;
401 Some((rows.trim(), dim.trim()))
402}