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;