metrale_circuit/pipeline/
check.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-10-02: The check a plan must pass: for every node of every group, the pipeline the
4//! group's kernel family declares for it ([`super::declare`]) equals the pipeline the plan
5//! requires of it ([`super::require`]), inputs, every step and outputs alike. A fused group
6//! composes its nodes' pipelines: each member's outputs are handed to the next member in the
7//! format the kernel declares, which must be the format the plan stores or the rule states it
8//! holds, so a fused edge cannot change format silently.
9//!
10//! Owner: metrale-circuit (pipeline).
11//! Invariants:
12//! - Every node of the plan's section gets a required pipeline or the check fails.
13//! - A group whose kernels no family lists, or whose family's declaration does not resolve for a
14//!   node, fails like a differing one ([`PipelineError::Undeclared`], [`PipelineError::Declared`]
15//!   inside a [`PipelineError::Mismatch`]); only a hardware planner's placeholder group (no
16//!   kernel of the class covers the op) is exempt, since nothing runs it.
17//! - The family of a node is the one, among those listing a kernel of its group and
18//!   implementing its op (with its role and weight format), that names the role explicitly,
19//!   else the first in manifest order.
20
21use std::collections::BTreeMap;
22
23use super::require::{Need, required};
24use super::{Mismatch, NodePipeline, PipelineError, Stated, differences};
25use crate::fuser::{FusionPlan, Group};
26use crate::hardware::plan::NOVEL_EMITTER;
27use crate::ir::{Circuit, Node, NodeIdx, OpKind};
28use crate::rules::Rule;
29use crate::venn::Subject;
30use crate::venn::classify::point_of;
31use crate::venn::families::{Families, Family};
32
33/// 2026-10-02: The required pipeline of every node a plan runs, and the steps its rules state.
34#[derive(Debug, Clone, Default, PartialEq, Eq)]
35pub struct PlanPipelines {
36    /// 2026-10-02: By node index; `None` outside the plan's section.
37    pub nodes: Vec<Option<NodePipeline>>,
38    /// 2026-10-02: What a rule states, by node.
39    pub stated: BTreeMap<NodeIdx, Stated>,
40}
41
42impl PlanPipelines {
43    /// 2026-10-02: Node `n`'s pipeline as a plan line shows it: `in -> [...] -> out`, a step a
44    /// rule states marked ` (rule)`.
45    pub fn line(&self, n: NodeIdx) -> Option<String> {
46        let p = self.nodes.get(n)?.as_ref()?;
47        Some(p.render(self.stated.get(&n).unwrap_or(&Stated::default())))
48    }
49
50    /// 2026-10-02: Node `n`'s pipeline as facts ([`NodePipeline::facts`]).
51    pub fn facts(&self, n: NodeIdx) -> Option<Vec<String>> {
52        let p = self.nodes.get(n)?.as_ref()?;
53        Some(p.facts(self.stated.get(&n).unwrap_or(&Stated::default())))
54    }
55}
56
57/// 2026-10-02: The family that declares node `node`'s pipeline in group `g`.
58pub fn family_of<'f>(families: &'f Families, g: &Group, node: &Node) -> Option<&'f Family> {
59    let listed = |f: &Family| {
60        if g.kernels.is_empty() {
61            f.emitters.contains(&g.emitter)
62        } else {
63            g.kernels.iter().any(|k| f.kernels.contains(k))
64        }
65    };
66    let rank = |f: &Family| -> Option<u8> {
67        f.ops
68            .iter()
69            .filter(|s| s.names(&node.op))
70            .filter(|s| s.weight.is_empty() || node.weight.is_some_and(|w| s.weight.contains(&w)))
71            .filter_map(|s| match node.op {
72                OpKind::Linear(r) if s.roles.contains(&r) => Some(0),
73                OpKind::Linear(_) if s.roles.is_empty() => Some(1),
74                OpKind::Linear(_) => None,
75                _ => Some(1),
76            })
77            .min()
78    };
79    families
80        .families
81        .iter()
82        .filter(|f| listed(f))
83        .filter_map(|f| rank(f).map(|r| (r, f)))
84        .min_by_key(|(r, _)| *r)
85        .map(|(_, f)| f)
86}
87
88/// 2026-10-02: Check `plan` of `c`: every node's declared pipeline against its requirement
89/// under `settings`, the plan's groups' rules being among `rules`.
90pub fn check_plan(
91    c: &Circuit,
92    plan: &FusionPlan,
93    rules: &[Rule],
94    families: &Families,
95    settings: &BTreeMap<String, String>,
96) -> Result<PlanPipelines, PipelineError> {
97    let need = Need::of_plan(c, plan, rules, settings);
98    let subject = Subject {
99        recipe: "",
100        circuit: c,
101        settings,
102        plan: None,
103    };
104    let mut out = PlanPipelines {
105        nodes: vec![None; c.nodes.len()],
106        stated: BTreeMap::new(),
107    };
108    let mut mismatches = Vec::new();
109    for g in &plan.groups {
110        for &n in &g.nodes {
111            let node = &c.nodes[n];
112            let req = required(&need, n)?;
113            let stated = Stated {
114                steps: need.stated_steps(n).collect(),
115                inputs: node
116                    .inputs
117                    .iter()
118                    .enumerate()
119                    .filter(|(_, e)| need.held_input(n, **e))
120                    .map(|(i, _)| i)
121                    .collect(),
122                outputs: node
123                    .outputs
124                    .iter()
125                    .enumerate()
126                    .filter(|(_, e)| need.holds_in_group(**e))
127                    .map(|(i, _)| i)
128                    .collect(),
129            };
130            if stated != Stated::default() {
131                out.stated.insert(n, stated);
132            }
133            if g.emitter != NOVEL_EMITTER {
134                // 2026-10-02: A kernel whose pipeline is undeclared or unresolvable cannot be
135                // shown to run the requirement: it is a mismatch of its rule, like a differing one.
136                let found = match declared(families, g, n, &subject) {
137                    Ok((_, decl)) if decl == req => None,
138                    Ok((fam, decl)) => Some((fam.to_string(), differences(&req, &decl))),
139                    Err(PipelineError::Declared { family, detail, .. }) => {
140                        Some((family, vec![detail]))
141                    }
142                    Err(PipelineError::Undeclared { kernels, .. }) => Some((
143                        "-".to_string(),
144                        vec![format!(
145                            "no kernel family declares a pipeline for {kernels}"
146                        )],
147                    )),
148                    Err(e) => Some(("-".to_string(), vec![e.to_string()])),
149                };
150                if let Some((family, diffs)) = found {
151                    mismatches.push(Mismatch {
152                        node: node.id.clone(),
153                        rule: g.rule.clone(),
154                        family,
155                        diffs,
156                    });
157                }
158            }
159            out.nodes[n] = Some(req);
160        }
161    }
162    if mismatches.is_empty() {
163        Ok(out)
164    } else {
165        Err(PipelineError::Mismatch(mismatches))
166    }
167}
168
169fn declared<'f>(
170    families: &'f Families,
171    g: &Group,
172    n: NodeIdx,
173    subject: &Subject<'_>,
174) -> Result<(&'f str, NodePipeline), PipelineError> {
175    let node = &subject.circuit.nodes[n];
176    let kernels = || {
177        if g.kernels.is_empty() {
178            format!("({} emitter)", g.emitter)
179        } else {
180            g.kernels
181                .iter()
182                .map(|k| k.to_string())
183                .collect::<Vec<_>>()
184                .join(" + ")
185        }
186    };
187    let fam = family_of(families, g, node).ok_or_else(|| PipelineError::Undeclared {
188        node: node.id.clone(),
189        op: node.op.name(),
190        kernels: kernels(),
191    })?;
192    let fail = |detail: String| PipelineError::Declared {
193        node: node.id.clone(),
194        op: node.op.name(),
195        family: fam.id.clone(),
196        detail,
197    };
198    let (point, _) = point_of(fam, subject, node).map_err(|e| fail(e.to_string()))?;
199    let p = super::declare::declared_for(
200        &fam.pipeline,
201        &fam.point_pipelines(),
202        &g.kernels,
203        &super::declare::Site::of(subject.circuit, n),
204        &point,
205    )
206    .map_err(fail)?;
207    Ok((fam.id.as_str(), p))
208}
209
210#[cfg(test)]
211#[path = "check_tests.rs"]
212mod check_tests;