metrale_circuit/
rules_parse.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-30: The FUSIONS.toml file format and its parse into [`super::Rule`]s, split out of
4//! `rules.rs` unchanged apart from the `[[runtime]]` table ([`crate::runtime`]).
5//!
6//! Owner: metrale-circuit.
7//! Invariants: those of `rules.rs`.
8
9use std::collections::{BTreeMap, BTreeSet};
10
11use serde::Deserialize;
12
13use super::{KernelId, Mode, Numerics, PatternOp, Repeat, Rule, RuleError};
14use crate::format::Format;
15use crate::ir::{LayerKind, LinearRole, OpKind};
16
17#[derive(Deserialize)]
18#[serde(deny_unknown_fields)]
19struct RulesFile {
20    schema: u32,
21    rule: Vec<RuleFile>,
22    #[serde(default)]
23    runtime: Vec<crate::runtime::RouteFile>,
24}
25
26#[derive(Deserialize)]
27#[serde(deny_unknown_fields)]
28struct RuleFile {
29    id: String,
30    pattern: Vec<PatternFile>,
31    kernels: Vec<KernelFile>,
32    repeat: String,
33    copies: Option<String>,
34    emitter: String,
35    rows: [u64; 2],
36    modes: Vec<String>,
37    #[serde(default)]
38    requires: Vec<String>,
39    #[serde(default)]
40    when: BTreeMap<String, String>,
41    numerics: String,
42    microtest: Option<String>,
43    lever: Option<String>,
44    priority: i64,
45    cite: String,
46    #[serde(default)]
47    run: Vec<RunFile>,
48}
49
50/// 2026-09-30: `[[rule.run]]`: a per-run selector (`crate::runs::RunSelect`).
51#[derive(Deserialize)]
52#[serde(deny_unknown_fields)]
53struct RunFile {
54    k: [u64; 2],
55    n: [u64; 2],
56    contiguous: Option<bool>,
57    carried: Option<bool>,
58    launches: Vec<LaunchFile>,
59    copies: Option<String>,
60}
61
62#[derive(Deserialize)]
63#[serde(deny_unknown_fields)]
64struct LaunchFile {
65    module: String,
66    func: String,
67    times: String,
68}
69
70#[derive(Deserialize)]
71#[serde(deny_unknown_fields)]
72struct KernelFile {
73    module: String,
74    func: String,
75}
76
77#[derive(Deserialize)]
78#[serde(deny_unknown_fields)]
79struct PatternFile {
80    op: String,
81    role: Option<String>,
82    #[serde(default)]
83    roles: Vec<String>,
84    layer_kind: Option<String>,
85    format: Option<String>,
86    local: Option<String>,
87    weight: Option<String>,
88    input: Option<String>,
89    writes: Option<String>,
90    #[serde(default)]
91    keep: bool,
92    #[serde(default)]
93    stored: bool,
94    #[serde(default)]
95    sibling: bool,
96    holds: Option<String>,
97    #[serde(default)]
98    steps: BTreeMap<String, String>,
99}
100
101/// 2026-09-28: Parse FUSIONS.toml text into rules, in file order. 2026-09-30: A file that
102/// declares `[[runtime]]` routes is refused: a caller that plans reads it with
103/// [`crate::runtime::parse_rule_set`], so no route is dropped unseen.
104pub fn parse_rules(text: &str) -> Result<Vec<Rule>, RuleError> {
105    let (rules, routes) = parse_file(text)?;
106    if let Some(r) = routes.first() {
107        return Err(RuleError::Runtime {
108            route: r.id.clone(),
109            detail: "a rule set with runtime routes is read with `parse_rule_set`".into(),
110        });
111    }
112    Ok(rules)
113}
114
115/// 2026-09-30: The rules, and the `[[runtime]]` entries unchecked.
116pub(crate) fn parse_file(
117    text: &str,
118) -> Result<(Vec<Rule>, Vec<crate::runtime::RouteFile>), RuleError> {
119    let file: RulesFile = toml::from_str(text).map_err(|e| RuleError::Parse(e.to_string()))?;
120    if file.schema != 1 {
121        return Err(RuleError::Parse(format!(
122            "schema {} (this build reads 1)",
123            file.schema
124        )));
125    }
126    let mut seen = BTreeSet::new();
127    let mut out = Vec::with_capacity(file.rule.len());
128    for r in file.rule {
129        if !seen.insert(r.id.clone()) {
130            return Err(RuleError::DuplicateId(r.id));
131        }
132        out.push(rule(r)?);
133    }
134    Ok((out, file.runtime))
135}
136
137fn rule(r: RuleFile) -> Result<Rule, RuleError> {
138    let shape = |detail: &str| RuleError::Shape {
139        rule: r.id.clone(),
140        detail: detail.to_string(),
141    };
142    if r.pattern.is_empty() {
143        return Err(shape("empty pattern"));
144    }
145    if r.modes.is_empty() {
146        return Err(shape("empty modes"));
147    }
148    if r.rows[0] == 0 || r.rows[0] > r.rows[1] {
149        return Err(shape("rows must be [lo, hi] with 1 <= lo <= hi"));
150    }
151    if r.cite.trim().is_empty() {
152        return Err(shape("empty cite"));
153    }
154    let mut modes = BTreeSet::new();
155    for m in &r.modes {
156        modes.insert(Mode::parse(m).ok_or_else(|| shape(&format!("unknown mode `{m}`")))?);
157    }
158    let repeat = Repeat::parse(&r.repeat).ok_or_else(|| {
159        shape(&format!(
160            "unknown repeat `{}` (once | per_row | per_row_but_last | per_run | chunk<n>)",
161            r.repeat
162        ))
163    })?;
164    let kernels: Vec<KernelId> = r
165        .kernels
166        .iter()
167        .map(|k| KernelId {
168            module: k.module.clone(),
169            func: k.func.clone(),
170        })
171        .collect();
172    let runs = runs(&r, &kernels)?;
173    if (repeat == Repeat::PerRun) == runs.is_empty() {
174        return Err(shape(
175            "`per_run` needs `[[rule.run]]` selectors, and only a `per_run` rule may have them",
176        ));
177    }
178    if repeat == Repeat::PerRun
179        && (r.copies.is_some() || modes.iter().any(|m| *m != Mode::VerifyBatch))
180    {
181        return Err(shape(
182            "a `per_run` rule serves `verify_batch` only and states copies per run",
183        ));
184    }
185    let copies = r
186        .copies
187        .as_deref()
188        .map(|c| {
189            Repeat::parse(c).ok_or_else(|| {
190                shape(&format!(
191                    "unknown copies `{c}` (once | per_row | per_row_but_last | chunk<n>)"
192                ))
193            })
194        })
195        .transpose()?;
196    if r.pattern[0].sibling {
197        return Err(shape("the first pattern element cannot be a sibling"));
198    }
199    let numerics = numerics(&r)?;
200    let mut pattern = Vec::with_capacity(r.pattern.len());
201    for p in &r.pattern {
202        pattern.push(pattern_op(&r.id, p)?);
203    }
204    Ok(Rule {
205        pattern,
206        kernels,
207        repeat,
208        copies,
209        emitter: r.emitter,
210        rows: (r.rows[0], r.rows[1]),
211        modes,
212        requires: r.requires.into_iter().collect(),
213        when: r.when,
214        numerics,
215        priority: r.priority,
216        cite: r.cite,
217        runs,
218        id: r.id,
219    })
220}
221
222/// 2026-09-30: The rule's `[[rule.run]]` selectors; every launch must name one of its kernels.
223fn runs(r: &RuleFile, kernels: &[KernelId]) -> Result<Vec<crate::runs::RunSelect>, RuleError> {
224    use crate::runs::{RunSelect, Times};
225    let bad = |detail: String| RuleError::Shape {
226        rule: r.id.clone(),
227        detail,
228    };
229    let times = |s: &str| {
230        Times::parse(s).ok_or_else(|| {
231            bad(format!(
232                "unknown times `{s}` (once | per_seq | per_row | per_seq_row_but_last)"
233            ))
234        })
235    };
236    let mut out = Vec::with_capacity(r.run.len());
237    for f in &r.run {
238        if f.k[0] == 0 || f.k[0] > f.k[1] || f.n[0] == 0 || f.n[0] > f.n[1] {
239            return Err(bad(format!("run selector k {:?} n {:?}", f.k, f.n)));
240        }
241        if f.launches.is_empty() {
242            return Err(bad("a run selector with no launches".into()));
243        }
244        let mut launches = Vec::with_capacity(f.launches.len());
245        for l in &f.launches {
246            let id = KernelId {
247                module: l.module.clone(),
248                func: l.func.clone(),
249            };
250            if !kernels.contains(&id) {
251                return Err(bad(format!(
252                    "run selector launches {id}, which `kernels` does not list"
253                )));
254            }
255            launches.push((id, times(&l.times)?));
256        }
257        out.push(RunSelect {
258            k: (f.k[0], f.k[1]),
259            n: (f.n[0], f.n[1]),
260            contiguous: f.contiguous,
261            carried: f.carried,
262            launches,
263            copies: f.copies.as_deref().map(times).transpose()?,
264        });
265    }
266    Ok(out)
267}
268
269fn numerics(r: &RuleFile) -> Result<Numerics, RuleError> {
270    let stray = |what: &str| RuleError::Numerics {
271        rule: r.id.clone(),
272        detail: format!("`{what}` is not allowed on a `{}` rule", r.numerics),
273    };
274    match r.numerics.as_str() {
275        "bit_identical" => {
276            if r.lever.is_some() {
277                return Err(stray("lever"));
278            }
279            match &r.microtest {
280                Some(m) if !m.trim().is_empty() => Ok(Numerics::BitIdentical {
281                    microtest: m.clone(),
282                }),
283                _ => Err(RuleError::MissingMicrotest(r.id.clone())),
284            }
285        }
286        "reference" => {
287            if r.lever.is_some() {
288                return Err(stray("lever"));
289            }
290            if r.microtest.is_some() {
291                return Err(stray("microtest"));
292            }
293            Ok(Numerics::Reference)
294        }
295        "differs" => {
296            if r.microtest.is_some() {
297                return Err(stray("microtest"));
298            }
299            match &r.lever {
300                Some(l) if !l.trim().is_empty() => Ok(Numerics::Differs { lever: l.clone() }),
301                _ => Err(RuleError::MissingLever(r.id.clone())),
302            }
303        }
304        other => Err(RuleError::Numerics {
305            rule: r.id.clone(),
306            detail: format!("unknown numerics class `{other}`"),
307        }),
308    }
309}
310
311fn pattern_op(rule: &str, p: &PatternFile) -> Result<PatternOp, RuleError> {
312    let err = |detail: String| RuleError::Op {
313        rule: rule.to_string(),
314        detail,
315    };
316    let fmt = |s: &Option<String>| -> Result<Option<Format>, RuleError> {
317        s.as_deref()
318            .map(Format::parse)
319            .transpose()
320            .map_err(|e| err(e.to_string()))
321    };
322    let mut roles = BTreeSet::new();
323    for r in &p.roles {
324        roles
325            .insert(LinearRole::parse(r).ok_or_else(|| err(format!("unknown linear role `{r}`")))?);
326    }
327    let role = match (p.role.as_deref(), p.roles.first()) {
328        (Some(_), Some(_)) => return Err(err("give `role` or `roles`, not both".into())),
329        (Some(r), None) => Some(r),
330        (None, first) => first.map(String::as_str),
331    };
332    let op = OpKind::parse(&p.op, role, fmt(&p.format)?).map_err(|e| err(e.to_string()))?;
333    let layer_kind = p
334        .layer_kind
335        .as_deref()
336        .map(|k| LayerKind::parse(k).ok_or_else(|| err(format!("unknown layer kind `{k}`"))))
337        .transpose()?;
338    // 2026-10-02: A stated step is one of the op's pipeline steps, with a value of its shape.
339    let mut steps = BTreeMap::new();
340    for (name, text) in &p.steps {
341        let kind = crate::pipeline::vocab::parse_step(name)
342            .filter(|k| crate::pipeline::vocab::steps_of(&op).contains(k))
343            .ok_or_else(|| {
344                err(format!(
345                    "`{name}` is not a pipeline step of `{}`",
346                    op.name()
347                ))
348            })?;
349        let value = crate::pipeline::vocab::parse_value(kind, text)
350            .map_err(|e| err(format!("step `{name}`: {e}")))?;
351        steps.insert(kind, value);
352    }
353    Ok(PatternOp {
354        op,
355        roles,
356        layer_kind,
357        local: p.local.clone(),
358        weight: fmt(&p.weight)?,
359        input: fmt(&p.input)?,
360        writes: fmt(&p.writes)?,
361        keep: p.keep,
362        stored: p.stored,
363        sibling: p.sibling,
364        holds: fmt(&p.holds)?,
365        steps,
366    })
367}