metrale_closure/
circuits.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-28: The circuit files every target of a hardware hashes as configs: each
4//! `kernels/circuits/**/*.toml` (the op graphs, precision tables and instances) and every
5//! `kernels/<class>/common/FUSIONS.toml` the hardware's rule set is resolved from. They decide
6//! which kernel runs for which op, so a record's closure must move when they do.
7//!
8//! Owner: metrale-closure (shared by the kernels build script and the bench gate, so both
9//! hash the same list).
10//! Invariants:
11//! - The list is sorted and holds only files that exist; `kernels/circuits/plans/` holds
12//!   rendered `.txt` plans, which are derived and not listed.
13//! - 2026-09-30: [`fusions_chain`] follows the rule inheritance the planner resolves
14//!   (`metrale_circuit::hardware::class::class_rules`): a class's own FUSIONS.toml and, through
15//!   its `inherits = "<class>"`, each ancestor's; a class with no FUSIONS.toml takes its
16//!   HARDWARE.toml `[hardware] inherits` parent's rules. So editing gb10's rules moves the
17//!   closure of hopper and b300 (FUSIONS `inherits`) and of b200 (HARDWARE `inherits`, no rules
18//!   of its own). A cross-crate test in metrale-circuit holds the two resolutions equal on the
19//!   real tree.
20
21use std::path::{Path, PathBuf};
22
23use crate::layout_manifest::{LayoutError, hardware_raw};
24
25/// 2026-09-28: The circuit configs of `hardware` under `root`.
26pub fn circuit_configs(root: &Path, hardware: &str) -> Result<Vec<PathBuf>, LayoutError> {
27    let mut out = Vec::new();
28    tomls_under(&root.join("kernels").join("circuits"), &mut out);
29    out.extend(fusions_chain(root, hardware)?);
30    out.sort();
31    Ok(out)
32}
33
34/// 2026-09-30: The FUSIONS.toml files `hardware` plans with, base first; empty when neither the
35/// class nor an ancestor declares rules. A cycle, an unreadable FUSIONS.toml or an `inherits`
36/// that is not a class name is an error, not a shorter list: a list that stopped early would
37/// leave an ancestor's rules out of the hash.
38pub fn fusions_chain(root: &Path, hardware: &str) -> Result<Vec<PathBuf>, LayoutError> {
39    let kernels = root.join("kernels");
40    let mut seen: Vec<String> = Vec::new();
41    let mut files: Vec<PathBuf> = Vec::new();
42    let mut next = Some(hardware.to_string());
43    while let Some(class) = next.take() {
44        let fusions = kernels.join(&class).join("common").join("FUSIONS.toml");
45        if seen.contains(&class) {
46            return Err(LayoutError::Manifest {
47                path: fusions,
48                message: format!("FUSIONS inheritance cycle through `{class}`"),
49            });
50        }
51        seen.push(class.clone());
52        if !fusions.is_file() {
53            next = hardware_raw(&kernels, &class)?.inherits;
54            continue;
55        }
56        let text = std::fs::read_to_string(&fusions).map_err(|e| LayoutError::Manifest {
57            path: fusions.clone(),
58            message: e.to_string(),
59        })?;
60        let table: toml::Table = toml::from_str(&text).map_err(|e| LayoutError::Manifest {
61            path: fusions.clone(),
62            message: format!("bad TOML: {e}"),
63        })?;
64        next = match table.get("inherits") {
65            None => None,
66            Some(v) => Some(
67                v.as_str()
68                    .ok_or_else(|| LayoutError::Manifest {
69                        path: fusions.clone(),
70                        message: "`inherits` is not a class name".into(),
71                    })?
72                    .to_string(),
73            ),
74        };
75        files.push(fusions);
76    }
77    files.reverse();
78    Ok(files)
79}
80
81/// 2026-09-28: The directories [`circuit_configs`] reads, for `cargo:rerun-if-changed`, so a
82/// circuit file added or removed reruns the build script too.
83pub fn circuit_dirs(root: &Path) -> Vec<PathBuf> {
84    vec![root.join("kernels").join("circuits")]
85}
86
87fn tomls_under(dir: &Path, out: &mut Vec<PathBuf>) {
88    let Ok(entries) = std::fs::read_dir(dir) else {
89        return;
90    };
91    for entry in entries.flatten() {
92        let path = entry.path();
93        if path.is_dir() {
94            tomls_under(&path, out);
95        } else if path.extension().is_some_and(|e| e == "toml") {
96            out.push(path);
97        }
98    }
99}
100
101#[cfg(test)]
102#[path = "circuits_tests.rs"]
103mod circuits_tests;