metrale_closure/
layout_manifest.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The manifest files [`super::layout`] reads, and the errors it
4//! reports.
5//!
6//! Three manifests steer resolution:
7//!
8//! * `kernels/<hw>/HARDWARE.toml`: `[hardware] vendor` (default `nvidia`)
9//!   picks the source extension, `[hardware] inherits = "<hw>"` layers this
10//!   tree over another.
11//! * `kernels/<hw>/<model>/MODEL.toml`: `[model] kernel_source = "<model>"`
12//!   redirects the per-quant kernel directories to another model's.
13//! * `KERNEL.toml` in a leaf or `common/` directory: `[sources] use = [...]`
14//!   adds files from elsewhere in `kernels/` to that directory's layer, and
15//!   `[shadow] <stem> = "<reason>"` declares which of the directory's entries
16//!   replace a same-name entry in a lower layer.
17//!
18//! Other keys in those files belong to the build script and are not
19//! interpreted here, except that `[hardware] arch` is carried verbatim, a
20//! `[kernels] overrides` table in `HARDWARE.toml` is refused, and so is a
21//! `[sources]` key other than `use`.
22//!
23//! Owner: metrale-closure (kernel layout).
24//! Invariants:
25//! - A `Hardware` that [`hardware`] returns has a known source extension, and
26//!   its parent, if any, exists, is not itself, inherits nothing and has the
27//!   same extension.
28//! - A `[shadow]` reason that [`kernel_manifest`] returns is not blank.
29
30use std::collections::BTreeMap;
31use std::path::{Path, PathBuf};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum LayoutError {
35    /// 2026-09-26: `kernels/<hw>` has no `HARDWARE.toml`.
36    UnknownHardware(String),
37    /// 2026-09-26: A manifest did not parse, or holds a key of the wrong
38    /// shape.
39    Manifest { path: PathBuf, message: String },
40    /// 2026-09-26: `[hardware] vendor` names no known source extension.
41    UnknownVendor { hardware: String, vendor: String },
42    /// 2026-09-26: `[hardware] inherits` names itself, a missing tree, a tree
43    /// that itself inherits (chains are refused, as `kernel_source` chains
44    /// are), or a tree with another source extension.
45    Inherits { hardware: String, message: String },
46    /// 2026-09-26: `[model] kernel_source` names no model directory, or one
47    /// that redirects.
48    Redirect { model_dir: PathBuf, message: String },
49    /// 2026-09-26: The model directory the target names does not exist.
50    UnknownModel(PathBuf),
51    /// 2026-09-26: No layer directory exists at all for the target.
52    NoKernelDirectory(String),
53    /// 2026-09-26: A symlink inside a consulted directory, or a
54    /// `[sources] use` path that is one. Files are shared with
55    /// `[sources] use`, not symlinks.
56    Symlink(PathBuf),
57    /// 2026-09-26: A `[sources] use` entry that resolves to nothing usable.
58    Use {
59        manifest: PathBuf,
60        entry: String,
61        message: String,
62    },
63    /// 2026-09-26: Two entries in one layer share a name (a `use` naming a
64    /// file the directory already holds, or two `use`s of one name).
65    UseCollides {
66        manifest: PathBuf,
67        name: String,
68        existing: PathBuf,
69    },
70    /// 2026-09-26: One name resolves in two layers and the winner's
71    /// `KERNEL.toml` does not declare it in `[shadow]`.
72    UndeclaredShadow {
73        name: String,
74        winner: PathBuf,
75        loser: PathBuf,
76        manifest: PathBuf,
77    },
78    /// 2026-09-26: A `[shadow]` key whose entry shadows nothing.
79    DeadShadow { manifest: PathBuf, stem: String },
80    /// 2026-09-26: A winner that is the very file it would shadow, reached
81    /// twice.
82    SelfShadow { name: String, source: PathBuf },
83    /// 2026-09-26: `HARDWARE.toml [kernels] overrides`, which is refused: a
84    /// replacement is declared in `[shadow]` of the directory's `KERNEL.toml`.
85    RetiredKey { path: PathBuf, key: String },
86}
87
88impl std::fmt::Display for LayoutError {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        match self {
91            Self::UnknownHardware(hw) => write!(f, "kernels/{hw}: no HARDWARE.toml"),
92            Self::Manifest { path, message } => write!(f, "{}: {message}", path.display()),
93            Self::UnknownVendor { hardware, vendor } => write!(
94                f,
95                "kernels/{hardware}/HARDWARE.toml: vendor {vendor:?} has no kernel source extension"
96            ),
97            Self::Inherits { hardware, message } => {
98                write!(
99                    f,
100                    "kernels/{hardware}/HARDWARE.toml: [hardware] inherits: {message}"
101                )
102            }
103            Self::Redirect { model_dir, message } => {
104                write!(
105                    f,
106                    "{}/MODEL.toml: [model] kernel_source: {message}",
107                    model_dir.display()
108                )
109            }
110            Self::UnknownModel(dir) => write!(f, "{}: no such model directory", dir.display()),
111            Self::NoKernelDirectory(t) => write!(f, "{t}: no kernel directory in any layer"),
112            Self::Symlink(p) => write!(
113                f,
114                "{}: is a symlink — symlinks are not a sharing mechanism under kernels/; \
115                 declare the file in [sources] use of the KERNEL.toml beside it",
116                p.display()
117            ),
118            Self::Use {
119                manifest,
120                entry,
121                message,
122            } => {
123                write!(
124                    f,
125                    "{}: [sources] use {entry:?}: {message}",
126                    manifest.display()
127                )
128            }
129            Self::UseCollides {
130                manifest,
131                name,
132                existing,
133            } => write!(
134                f,
135                "{}: [sources] use brings {name} but this layer already holds {}",
136                manifest.display(),
137                existing.display()
138            ),
139            Self::UndeclaredShadow {
140                name,
141                winner,
142                loser,
143                manifest,
144            } => write!(
145                f,
146                "{} shadows {} and {} does not declare it: add `[shadow] {} = \"<reason>\"`",
147                winner.display(),
148                loser.display(),
149                manifest.display(),
150                stem_of(name)
151            ),
152            Self::DeadShadow { manifest, stem } => write!(
153                f,
154                "{}: [shadow] {stem} is declared but this directory's {stem} shadows nothing",
155                manifest.display()
156            ),
157            Self::SelfShadow { name, source } => write!(
158                f,
159                "{name} resolves to {} in two layers — a use of a file the target already reaches",
160                source.display()
161            ),
162            Self::RetiredKey { path, key } => write!(
163                f,
164                "{}: `{key}` is retired; an overlay's own files need no inventory, and one that \
165                 replaces a parent's file declares it in [shadow] of its KERNEL.toml",
166                path.display()
167            ),
168        }
169    }
170}
171
172impl std::error::Error for LayoutError {}
173
174/// 2026-09-26: The stem of a kernel-source file name: `w4a16_gemm.cu` ->
175/// `w4a16_gemm`.
176pub fn stem_of(name: &str) -> &str {
177    name.rsplit_once('.').map_or(name, |(stem, _)| stem)
178}
179
180/// 2026-09-26: Kernel-source extension for a `[hardware] vendor`, or `None`
181/// for an unknown vendor, which [`hardware`] refuses.
182pub fn source_ext(vendor: &str) -> Option<&'static str> {
183    match vendor {
184        "nvidia" | "cuda" | "amd" | "rocm" | "scale" | "hip" => Some("cu"),
185        "apple" | "metal" => Some("metal"),
186        _ => None,
187    }
188}
189
190/// 2026-09-26: Header extensions that are entries of every layer beside the
191/// sources, so a source's headers are staged with it.
192pub const HEADER_EXTS: &[&str] = &["cuh", "h"];
193
194/// 2026-09-26: `kernels/<hw>/HARDWARE.toml`, the part resolution reads.
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct Hardware {
197    pub name: String,
198    pub vendor: String,
199    /// 2026-09-26: `[hardware] arch`, verbatim, when present.
200    pub arch: Option<String>,
201    pub inherits: Option<String>,
202    pub source_ext: &'static str,
203}
204
205fn read_toml(path: &Path) -> Result<toml::Value, LayoutError> {
206    let text = std::fs::read_to_string(path).map_err(|e| LayoutError::Manifest {
207        path: path.to_path_buf(),
208        message: e.to_string(),
209    })?;
210    toml::from_str(&text).map_err(|e| LayoutError::Manifest {
211        path: path.to_path_buf(),
212        message: format!("bad TOML: {e}"),
213    })
214}
215
216fn string_at(value: &toml::Value, path: &Path, key: &str) -> Result<Option<String>, LayoutError> {
217    let (table, field) = key.split_once('.').unwrap_or(("", key));
218    let holder = if table.is_empty() {
219        Some(value)
220    } else {
221        value.get(table)
222    };
223    match holder.and_then(|t| t.get(field)) {
224        None => Ok(None),
225        Some(v) => v
226            .as_str()
227            .map(|s| Some(s.to_string()))
228            .ok_or_else(|| LayoutError::Manifest {
229                path: path.to_path_buf(),
230                message: format!("{key} must be a string"),
231            }),
232    }
233}
234
235/// 2026-09-26: Read one `HARDWARE.toml` without validating `inherits`;
236/// [`hardware`] does.
237pub(crate) fn hardware_raw(kernels: &Path, hw: &str) -> Result<Hardware, LayoutError> {
238    let path = kernels.join(hw).join("HARDWARE.toml");
239    if !path.is_file() {
240        return Err(LayoutError::UnknownHardware(hw.to_string()));
241    }
242    let value = read_toml(&path)?;
243    if value
244        .get("kernels")
245        .and_then(|k| k.get("overrides"))
246        .is_some()
247    {
248        return Err(LayoutError::RetiredKey {
249            path,
250            key: "[kernels] overrides".into(),
251        });
252    }
253    let vendor = string_at(&value, &path, "hardware.vendor")?.unwrap_or_else(|| "nvidia".into());
254    let source_ext = source_ext(&vendor).ok_or_else(|| LayoutError::UnknownVendor {
255        hardware: hw.to_string(),
256        vendor: vendor.clone(),
257    })?;
258    Ok(Hardware {
259        name: hw.to_string(),
260        vendor,
261        arch: string_at(&value, &path, "hardware.arch")?,
262        inherits: string_at(&value, &path, "hardware.inherits")?,
263        source_ext,
264    })
265}
266
267/// 2026-09-26: `kernels/<hw>/HARDWARE.toml`, with `inherits` checked: the
268/// parent must exist, must not be the tree itself, must not inherit in turn,
269/// and must compile the same source extension.
270pub fn hardware(kernels: &Path, hw: &str) -> Result<Hardware, LayoutError> {
271    let own = hardware_raw(kernels, hw)?;
272    if let Some(parent) = &own.inherits {
273        let fail = |message: String| LayoutError::Inherits {
274            hardware: hw.to_string(),
275            message,
276        };
277        if parent == hw {
278            return Err(fail("a tree cannot inherit itself".into()));
279        }
280        let parent_hw =
281            hardware_raw(kernels, parent).map_err(|e| fail(format!("parent {parent:?}: {e}")))?;
282        if let Some(grand) = parent_hw.inherits {
283            return Err(fail(format!(
284                "{parent:?} itself inherits {grand:?} — chains are refused; inherit the tree that owns the sources"
285            )));
286        }
287        if parent_hw.source_ext != own.source_ext {
288            return Err(fail(format!(
289                "{parent:?} compiles .{} sources, this tree .{}",
290                parent_hw.source_ext, own.source_ext
291            )));
292        }
293    }
294    Ok(own)
295}
296
297/// 2026-09-26: `[model] kernel_source` of `<model_dir>/MODEL.toml`,
298/// validated: the referent is a sibling model directory holding a MODEL.toml
299/// that does not redirect. Returns the source model directory (the model's
300/// own when there is no redirect). A model directory with no MODEL.toml owns
301/// its own sources.
302pub fn kernel_source_dir(model_dir: &Path) -> Result<PathBuf, LayoutError> {
303    let Some(src) = kernel_source(model_dir)? else {
304        return Ok(model_dir.to_path_buf());
305    };
306    let fail = |message: String| LayoutError::Redirect {
307        model_dir: model_dir.to_path_buf(),
308        message,
309    };
310    let hw_dir = model_dir
311        .parent()
312        .ok_or_else(|| fail("model dir has no parent".into()))?;
313    let src_dir = hw_dir.join(&src);
314    if !src_dir.is_dir() || !src_dir.join("MODEL.toml").is_file() {
315        return Err(fail(format!(
316            "{src:?} does not name a kernel target directory under {}",
317            hw_dir.display()
318        )));
319    }
320    if kernel_source(&src_dir)?.is_some() {
321        return Err(fail(format!(
322            "{src:?} itself redirects — chains are not allowed; point at the target that owns the sources"
323        )));
324    }
325    Ok(src_dir)
326}
327
328fn kernel_source(model_dir: &Path) -> Result<Option<String>, LayoutError> {
329    let path = model_dir.join("MODEL.toml");
330    if !path.is_file() {
331        return Ok(None);
332    }
333    let value = read_toml(&path)?;
334    let src = string_at(&value, &path, "model.kernel_source")?;
335    if let Some(s) = &src
336        && s.trim().is_empty()
337    {
338        return Err(LayoutError::Manifest {
339            path,
340            message: "[model] kernel_source must name a kernel target directory".into(),
341        });
342    }
343    Ok(src)
344}
345
346/// 2026-09-26: The resolution-relevant part of one `KERNEL.toml`.
347#[derive(Debug, Clone, PartialEq, Eq, Default)]
348pub struct KernelManifest {
349    pub path: PathBuf,
350    /// 2026-09-26: `[sources] use`, verbatim, in file order.
351    pub uses: Vec<String>,
352    /// 2026-09-26: `[shadow] <stem> = "<reason>"`.
353    pub shadow: BTreeMap<String, String>,
354}
355
356/// 2026-09-26: Parse `<dir>/KERNEL.toml` if it exists.
357pub fn kernel_manifest(dir: &Path) -> Result<Option<KernelManifest>, LayoutError> {
358    let path = dir.join("KERNEL.toml");
359    if !path.is_file() {
360        return Ok(None);
361    }
362    let value = read_toml(&path)?;
363    let fail = |message: String| LayoutError::Manifest {
364        path: path.clone(),
365        message,
366    };
367    let mut uses = Vec::new();
368    if let Some(sources) = value.get("sources") {
369        let table = sources
370            .as_table()
371            .ok_or_else(|| fail("[sources] must be a table".into()))?;
372        for key in table.keys() {
373            if key != "use" {
374                return Err(fail(format!("[sources] has no key `{key}`; only `use`")));
375            }
376        }
377        if let Some(list) = table.get("use") {
378            let arr = list
379                .as_array()
380                .ok_or_else(|| fail("[sources] use must be an array of paths".into()))?;
381            for v in arr {
382                let s = v
383                    .as_str()
384                    .ok_or_else(|| fail("[sources] use entries must be strings".into()))?;
385                uses.push(s.to_string());
386            }
387        }
388    }
389    let mut shadow = BTreeMap::new();
390    if let Some(decl) = value.get("shadow") {
391        let table = decl
392            .as_table()
393            .ok_or_else(|| fail("[shadow] must be a table of `stem = \"reason\"`".into()))?;
394        for (stem, reason) in table {
395            let reason = reason
396                .as_str()
397                .ok_or_else(|| fail(format!("[shadow] {stem} must be a reason string")))?;
398            if reason.trim().is_empty() {
399                return Err(fail(format!("[shadow] {stem} needs a stated reason")));
400            }
401            shadow.insert(stem.clone(), reason.to_string());
402        }
403    }
404    Ok(Some(KernelManifest { path, uses, shadow }))
405}