metrale_circuit/hardware/
class.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-30: A kernel class (`kernels/<class>/`) as the planner sees it: its inheritance chain
4//! and build defines from `HARDWARE.toml`, its fusion rules (`FUSIONS.toml`, inherited and
5//! overridden along the chain) and its kernel families (`KERNEL_FAMILIES.toml`, with points
6//! rediscovered from the class's own resolved sources and evidence kept per class).
7//!
8//! Owner: metrale-circuit (hardware).
9//! Invariants:
10//! - Rules: a class's `common/FUSIONS.toml` either stands alone or names `inherits = "<class>"`,
11//!   in which case its rules replace the parent's rules of the same id, add new ones, and
12//!   `remove` drops parent rules by id. A class without the file takes its HARDWARE.toml
13//!   parent's rules; a class with neither has no rules ([`ClassRules::None`]), which the planner
14//!   reports, never fills in.
15//! - Families: evidence is a measurement on one class. Families inherited from another class's
16//!   manifest keep their points (re-resolved against this class's sources) and lose their
17//!   evidence, so a report on this class says "unmeasured", never "optimized".
18
19use std::collections::{BTreeMap, BTreeSet};
20
21use super::HwError;
22use super::sources::ClassSources;
23use crate::rules::Rule;
24use crate::runtime::{RuleSet, RuntimeRoute, check_ids, overlay, parse_rule_set};
25use crate::venn::discover::{Found, KernelSources, discover};
26use crate::venn::families::{Discover, Families, How, Point, parse_families};
27use crate::venn::repo::Repo;
28
29/// 2026-09-30: What one `HARDWARE.toml` says about the class.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct ClassInfo {
32    /// 2026-09-30: Directory name.
33    pub name: String,
34    /// 2026-09-30: `[hardware] arch`.
35    pub arch: String,
36    /// 2026-09-30: `[hardware] inherits`.
37    pub inherits: Option<String>,
38    /// 2026-09-30: Macros `[build] extra_nvcc_flags` defines (`-DNAME`).
39    pub defines: BTreeSet<String>,
40    /// 2026-09-30: `[defaults]`, values as text.
41    pub defaults: BTreeMap<String, String>,
42    /// 2026-10-02: `[tensor_core_policy]` (`super::tc_policy`); `None` when the class states none.
43    pub tensor_core: Option<super::tc_policy::TcPolicy>,
44}
45
46fn toml_err(rel: &str) -> impl Fn(toml::de::Error) -> HwError + '_ {
47    move |e| HwError::Class(format!("{rel}: {e}"))
48}
49
50/// 2026-09-30: Parse `kernels/<name>/HARDWARE.toml`.
51pub fn parse_class(name: &str, text: &str) -> Result<ClassInfo, HwError> {
52    let rel = format!("kernels/{name}/HARDWARE.toml");
53    let t: toml::Table = toml::from_str(text).map_err(toml_err(&rel))?;
54    let hw = t
55        .get("hardware")
56        .and_then(|v| v.as_table())
57        .ok_or_else(|| HwError::Class(format!("{rel}: no [hardware]")))?;
58    let arch = hw
59        .get("arch")
60        .and_then(|v| v.as_str())
61        .ok_or_else(|| HwError::Class(format!("{rel}: no [hardware] arch")))?
62        .to_string();
63    let inherits = hw
64        .get("inherits")
65        .and_then(|v| v.as_str())
66        .map(str::to_string);
67    let defines = t
68        .get("build")
69        .and_then(|b| b.get("extra_nvcc_flags"))
70        .and_then(|f| f.as_array())
71        .into_iter()
72        .flatten()
73        .filter_map(|f| f.as_str()?.strip_prefix("-D"))
74        .map(|d| d.split('=').next().unwrap_or(d).to_string())
75        .collect();
76    let defaults = t
77        .get("defaults")
78        .and_then(|d| d.as_table())
79        .into_iter()
80        .flatten()
81        .map(|(k, v)| {
82            let text = v.as_str().map_or_else(|| v.to_string(), str::to_string);
83            (k.clone(), text)
84        })
85        .collect();
86    let tensor_core =
87        super::tc_policy::parse_policy(&t).map_err(|e| HwError::Class(format!("{rel}: {e}")))?;
88    Ok(ClassInfo {
89        name: name.to_string(),
90        arch,
91        inherits,
92        defines,
93        defaults,
94        tensor_core,
95    })
96}
97
98/// 2026-09-30: The class and its ancestors, own class first.
99pub fn chain(repo: &dyn Repo, class: &str) -> Result<Vec<ClassInfo>, HwError> {
100    let mut out: Vec<ClassInfo> = Vec::new();
101    let mut next = Some(class.to_string());
102    while let Some(name) = next {
103        if out.iter().any(|c| c.name == name) {
104            return Err(HwError::Class(format!(
105                "inheritance cycle through `{name}`"
106            )));
107        }
108        let text = repo
109            .read(&format!("kernels/{name}/HARDWARE.toml"))
110            .map_err(|e| HwError::Class(format!("class `{name}`: {e}")))?;
111        let info = parse_class(&name, &text)?;
112        next = info.inherits.clone();
113        out.push(info);
114    }
115    Ok(out)
116}
117
118/// 2026-09-30: The classes a plan on `class` reads rules and families from: its HARDWARE.toml
119/// chain, continued through the `inherits` of the last class's FUSIONS.toml (a class that does
120/// not inherit gb10's sources may still declare that it plans with gb10's rules).
121pub fn planning_chain(repo: &dyn Repo, class: &str) -> Result<Vec<ClassInfo>, HwError> {
122    let mut out = chain(repo, class)?;
123    loop {
124        let last = out.last().map(|c| c.name.clone()).unwrap_or_default();
125        let Ok(text) = repo.read(&fusions_rel(&last)) else {
126            return Ok(out);
127        };
128        let t: toml::Table = toml::from_str(&text).map_err(toml_err(&fusions_rel(&last)))?;
129        let Some(next) = t.get("inherits").and_then(|v| v.as_str()) else {
130            return Ok(out);
131        };
132        if out.iter().any(|c| c.name == next) {
133            return Ok(out);
134        }
135        out.extend(chain(repo, next)?);
136    }
137}
138
139/// 2026-09-30: A class's resolved rule set.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum ClassRules {
142    /// 2026-09-30: The rules, and the files they were resolved from, base first.
143    Rules {
144        /// 2026-09-30: Rules after every override.
145        rules: Vec<Rule>,
146        /// 2026-09-30: `kernels/<class>/common/FUSIONS.toml` paths read, base first.
147        files: Vec<String>,
148        /// 2026-09-30: Runtime routes after every override ([`crate::runtime`]).
149        runtime: Vec<RuntimeRoute>,
150    },
151    /// 2026-09-30: Neither the class nor any ancestor declares a FUSIONS.toml.
152    None,
153}
154
155fn fusions_rel(class: &str) -> String {
156    format!("kernels/{class}/common/FUSIONS.toml")
157}
158
159/// 2026-09-30: The rules `class` plans with.
160pub fn class_rules(repo: &dyn Repo, class: &str) -> Result<ClassRules, HwError> {
161    rules_of(repo, class, &mut Vec::new())
162}
163
164fn rules_of(repo: &dyn Repo, class: &str, seen: &mut Vec<String>) -> Result<ClassRules, HwError> {
165    if seen.iter().any(|c| c == class) {
166        return Err(HwError::Class(format!(
167            "FUSIONS inheritance cycle through `{class}`"
168        )));
169    }
170    seen.push(class.to_string());
171    let rel = fusions_rel(class);
172    let Ok(text) = repo.read(&rel) else {
173        let info = chain(repo, class)?;
174        return match info[0].inherits.as_deref() {
175            Some(parent) => rules_of(repo, parent, seen),
176            None => Ok(ClassRules::None),
177        };
178    };
179    let mut table: toml::Table = toml::from_str(&text).map_err(toml_err(&rel))?;
180    let inherits = table.remove("inherits");
181    let remove = table.remove("remove");
182    table
183        .entry("rule")
184        .or_insert_with(|| toml::Value::Array(Vec::new()));
185    let own_text = toml::to_string(&table).map_err(|e| HwError::Class(format!("{rel}: {e}")))?;
186    let RuleSet {
187        rules: own,
188        runtime: own_runtime,
189    } = parse_rule_set(&own_text).map_err(|e| HwError::Class(format!("{rel}: {e}")))?;
190    let Some(parent) = inherits else {
191        if remove.is_some() {
192            return Err(HwError::Class(format!(
193                "{rel}: `remove` needs `inherits`: a standalone rule set has nothing to remove"
194            )));
195        }
196        return Ok(ClassRules::Rules {
197            rules: own,
198            files: vec![rel],
199            runtime: own_runtime,
200        });
201    };
202    let parent = parent
203        .as_str()
204        .ok_or_else(|| HwError::Class(format!("{rel}: `inherits` is not a class name")))?;
205    let ClassRules::Rules {
206        rules: mut base,
207        mut files,
208        mut runtime,
209    } = rules_of(repo, parent, seen)?
210    else {
211        return Err(HwError::Class(format!(
212            "{rel}: inherits `{parent}`, which has no rules"
213        )));
214    };
215    let drop: Vec<String> = remove
216        .as_ref()
217        .and_then(|v| v.as_array())
218        .into_iter()
219        .flatten()
220        .map(|v| v.as_str().map(str::to_string))
221        .collect::<Option<_>>()
222        .ok_or_else(|| HwError::Class(format!("{rel}: `remove` is not a list of rule ids")))?;
223    for id in &drop {
224        let before = base.len();
225        base.retain(|r| &r.id != id);
226        if base.len() == before {
227            return Err(HwError::Class(format!(
228                "{rel}: removes `{id}`, which `{parent}` does not define"
229            )));
230        }
231    }
232    for r in own {
233        match base.iter_mut().find(|b| b.id == r.id) {
234            Some(slot) => *slot = r,
235            None => base.push(r),
236        }
237    }
238    overlay(&mut runtime, own_runtime);
239    check_ids(&base, &runtime).map_err(|e| HwError::Class(format!("{rel}: {e}")))?;
240    files.push(rel);
241    Ok(ClassRules::Rules {
242        rules: base,
243        files,
244        runtime,
245    })
246}
247
248/// 2026-09-30: The kernel families `class` is classified against: the nearest
249/// `KERNEL_FAMILIES.toml` along the chain as the base (evidence dropped unless it is the class's
250/// own), the class's own manifest overriding families by id, and every point re-resolved against
251/// the class's sources ([`class_points`]).
252pub fn class_families(
253    repo: &dyn Repo,
254    chain: &[ClassInfo],
255    sources: &ClassSources,
256) -> Result<Families, HwError> {
257    let rel = |c: &str| format!("kernels/{c}/common/KERNEL_FAMILIES.toml");
258    let mut found: Vec<(String, Families)> = Vec::new();
259    for c in chain {
260        if let Ok(text) = repo.read(&rel(&c.name)) {
261            let f = parse_families(&text).map_err(|e| HwError::Class(e.to_string()))?;
262            found.push((c.name.clone(), f));
263        }
264    }
265    let own = chain
266        .first()
267        .map(|c| c.name.clone())
268        .ok_or_else(|| HwError::Class("empty class chain".into()))?;
269    let (base_class, mut out) = found.pop().ok_or_else(|| {
270        HwError::Class(format!(
271            "no KERNEL_FAMILIES.toml along `{own}`'s planning chain: nothing to classify against"
272        ))
273    })?;
274    if base_class != own {
275        for f in &mut out.families {
276            f.evidence.clear();
277        }
278    }
279    while let Some((class, overlay)) = found.pop() {
280        for f in overlay.families {
281            let mut f = f;
282            if class != own {
283                f.evidence.clear();
284            }
285            match out.families.iter_mut().find(|g| g.id == f.id) {
286                Some(slot) => *slot = f,
287                None => out.families.push(f),
288            }
289        }
290        out.legacy.extend(overlay.legacy);
291    }
292    let manifest = std::mem::replace(&mut out.hardware, own);
293    let found = class_discovered(&out, &manifest, chain, sources);
294    for f in &mut out.families {
295        f.points = class_points(&f.points, chain, sources);
296        for d in found.iter().filter(|d| d.family == f.id) {
297            if !f.points.iter().any(|p| p.files.contains(&d.file)) {
298                f.points.push(Point {
299                    values: d.values.clone(),
300                    how: How::Copy,
301                    files: vec![d.file.clone()],
302                    // 2026-10-02: A discovered copy runs on its family's unit and pipeline.
303                    compute: None,
304                    pipeline: Default::default(),
305                });
306            }
307        }
308    }
309    Ok(out)
310}
311
312/// 2026-09-30: The points the manifest's `discover` rules find in the class's OWN trees: each
313/// rule's `kernels/<manifest>/...` path re-rooted at every class of the chain that is not the
314/// manifest's, matched against what the class compiles. A class's own copy of a kernel (a
315/// per-class file the manifest never listed) becomes a point of its family on that class.
316pub fn class_discovered(
317    fams: &Families,
318    manifest: &str,
319    chain: &[ClassInfo],
320    sources: &ClassSources,
321) -> BTreeSet<Found> {
322    let texts: BTreeMap<String, String> = sources
323        .modules
324        .values()
325        .map(|m| (m.path.clone(), m.text.clone()))
326        .collect();
327    let from = format!("kernels/{manifest}/");
328    let mut out = BTreeSet::new();
329    for tier in chain.iter().filter(|c| c.name != manifest) {
330        let to = format!("kernels/{}/", tier.name);
331        let reroot = |p: &str| p.strip_prefix(&from).map(|rest| format!("{to}{rest}"));
332        let mut moved = fams.clone();
333        for f in &mut moved.families {
334            f.discover = f
335                .discover
336                .iter()
337                .filter_map(|d| match d {
338                    Discover::File { glob, values } => Some(Discover::File {
339                        glob: reroot(glob)?,
340                        values: values.clone(),
341                    }),
342                    Discover::Macro {
343                        file,
344                        name,
345                        args,
346                        map,
347                    } => Some(Discover::Macro {
348                        file: reroot(file).filter(|f| texts.contains_key(f))?,
349                        name: name.clone(),
350                        args: args.clone(),
351                        map: map.clone(),
352                    }),
353                })
354                .collect();
355        }
356        let src = KernelSources {
357            paths: sources.files.clone(),
358            texts: texts.clone(),
359        };
360        out.extend(discover(&moved, &src).0);
361    }
362    out
363}
364
365/// 2026-09-30: The points of a family that `sources` realises: the files of a point that the
366/// class compiles as they are, or through its own copy of the same file name (a shadow),
367/// rewritten to what the class compiles. Host-side files (`crates/...`) are shared by every
368/// class. A point is kept with the files the class compiles (a point realised by several
369/// per-model copies exists where one of them is compiled) and dropped when none is.
370pub fn class_points(points: &[Point], chain: &[ClassInfo], sources: &ClassSources) -> Vec<Point> {
371    let by_name: BTreeMap<&str, &str> = sources
372        .files
373        .iter()
374        .map(|p| (file_name(p), p.as_str()))
375        .collect();
376    let compiled: BTreeSet<&str> = sources.files.iter().map(String::as_str).collect();
377    let in_chain = |p: &str| {
378        chain
379            .iter()
380            .any(|c| p.starts_with(&format!("kernels/{}/", c.name)))
381    };
382    points
383        .iter()
384        .filter_map(|p| {
385            let files: Vec<String> = p
386                .files
387                .iter()
388                .filter_map(|f| {
389                    if !f.starts_with("kernels/") || compiled.contains(f.as_str()) {
390                        return Some(f.clone());
391                    }
392                    let own = by_name.get(file_name(f)).copied()?;
393                    in_chain(own).then(|| own.to_string())
394                })
395                .collect();
396            if files.is_empty() {
397                return None;
398            }
399            Some(Point {
400                values: p.values.clone(),
401                how: p.how,
402                files,
403                compute: p.compute.clone(),
404                pipeline: p.pipeline.clone(),
405            })
406        })
407        .collect()
408}
409
410fn file_name(p: &str) -> &str {
411    p.rsplit('/').next().unwrap_or(p)
412}