metrale_config/
levers.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: Every `METRALE_*` environment variable the repository reads, declared once.
4//!
5//! `met` refuses to start with an undeclared `METRALE_*` name in its environment ([`check`],
6//! called first in `serve_main`). The bench crate's `serve_env` takes the harness set from the
7//! table and validates a recipe's `env:` block against it, and `met dump-serve-options`
8//! publishes it. `levers_tests` checks the table against the sources: every `"METRALE_*"`
9//! literal outside test files and every `getenv("METRALE_*")` in C/CUDA must be declared, each
10//! lever's `reader` file must name it, and a `Runtime` lever needs a production reader. The
11//! server's `manifest_tests` check the `flag` column against clap.
12//!
13//! Owner: config.
14//! Invariants: none beyond the types.
15
16macro_rules! lever {
17    ($env:literal, $path:literal, $ty:ident, $default:literal, None, $class:ident, $reader:literal, $doc:literal) => {
18        $crate::levers::LeverSpec {
19            env: $env,
20            path: $path,
21            ty: $crate::levers::Ty::$ty,
22            default: $default,
23            flag: None,
24            class: $crate::levers::Class::$class,
25            reader: $reader,
26            doc: $doc,
27        }
28    };
29    ($env:literal, $path:literal, $ty:ident, $default:literal, $flag:literal, $class:ident, $reader:literal, $doc:literal) => {
30        $crate::levers::LeverSpec {
31            env: $env,
32            path: $path,
33            ty: $crate::levers::Ty::$ty,
34            default: $default,
35            flag: Some($flag),
36            class: $crate::levers::Class::$class,
37            reader: $reader,
38            doc: $doc,
39        }
40    };
41}
42
43mod table_a;
44mod table_b;
45
46/// 2026-09-26: Who reads a lever. `levers_tests` checks each lever's `reader` against its class.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum Class {
49    /// 2026-09-26: Read by a `met` process at run time (serve or bench). A recipe may declare
50    /// it in `env:`.
51    Runtime,
52    /// 2026-09-26: Places, logs or builds for the harness. A recipe may not declare it.
53    Harness,
54    /// 2026-09-26: Read when the binary is built (a build script or `option_env!`).
55    Build,
56    /// 2026-09-26: Read only by a test or a dev example/benchmark.
57    Dev,
58    /// 2026-09-26: Read only by an operator script or the site build, outside `crates/`.
59    Tool,
60}
61
62impl Class {
63    #[must_use]
64    pub fn as_str(self) -> &'static str {
65        match self {
66            Self::Runtime => "runtime",
67            Self::Harness => "harness",
68            Self::Build => "build",
69            Self::Dev => "dev",
70            Self::Tool => "tool",
71        }
72    }
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum Ty {
77    /// 2026-09-26: On or off; `default` names the state when the variable is unset.
78    Switch,
79    Int,
80    Float,
81    Text,
82    Path,
83}
84
85impl Ty {
86    #[must_use]
87    pub fn as_str(self) -> &'static str {
88        match self {
89            Self::Switch => "switch",
90            Self::Int => "int",
91            Self::Float => "float",
92            Self::Text => "text",
93            Self::Path => "path",
94        }
95    }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub struct LeverSpec {
100    pub env: &'static str,
101    /// 2026-09-26: `group.name`, ending in the variable's suffix in lower case.
102    pub path: &'static str,
103    pub ty: Ty,
104    /// 2026-09-26: The value in force when the variable is unset: `on`/`off` for a switch (of
105    /// the effect the name describes), a number, `unset`, `built-in`, or `target-declared`
106    /// (the `[defaults]` table of `kernels/<hw>/HARDWARE.toml`).
107    pub default: &'static str,
108    /// 2026-09-26: The `met serve` flag that sets the same thing.
109    pub flag: Option<&'static str>,
110    pub class: Class,
111    /// 2026-09-26: The repository file that reads it; `levers_tests` checks that it names it.
112    pub reader: &'static str,
113    pub doc: &'static str,
114}
115
116pub const PREFIX: &str = "METRALE_";
117
118/// 2026-09-26: Every lever, in name order (`levers_tests` checks the order).
119pub fn all() -> impl Iterator<Item = &'static LeverSpec> {
120    table_a::LEVERS.iter().chain(table_b::LEVERS)
121}
122
123#[must_use]
124pub fn lookup(env: &str) -> Option<&'static LeverSpec> {
125    all().find(|l| l.env == env)
126}
127
128#[must_use]
129pub fn is_harness(env: &str) -> bool {
130    lookup(env).is_some_and(|l| l.class == Class::Harness)
131}
132
133/// 2026-09-26: The `METRALE_*` names among `names` that no lever declares, sorted and
134/// de-duplicated.
135pub fn undeclared<I, S>(names: I) -> Vec<String>
136where
137    I: IntoIterator<Item = S>,
138    S: AsRef<str>,
139{
140    let mut out: Vec<String> = names
141        .into_iter()
142        .map(|n| n.as_ref().to_string())
143        .filter(|n| n.starts_with(PREFIX) && lookup(n).is_none())
144        .collect();
145    out.sort();
146    out.dedup();
147    out
148}
149
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct UndeclaredLevers(pub Vec<String>);
152
153impl std::fmt::Display for UndeclaredLevers {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        write!(
156            f,
157            "undeclared METRALE_* environment variable{}: {}. Nothing reads {}: it is a \
158             typo or a lever that no longer exists, and running on as if it applied would \
159             measure something other than what was asked for. Unset {}. (`met \
160             dump-serve-options` lists every declared lever.)",
161            if self.0.len() == 1 { "" } else { "s" },
162            self.0.join(", "),
163            if self.0.len() == 1 { "it" } else { "them" },
164            if self.0.len() == 1 { "it" } else { "them" },
165        )
166    }
167}
168
169impl std::error::Error for UndeclaredLevers {}
170
171/// 2026-09-26: Refuse an environment carrying an undeclared `METRALE_*` name.
172///
173/// # Errors
174/// [`UndeclaredLevers`] naming every such variable.
175pub fn check<I, S>(names: I) -> Result<(), UndeclaredLevers>
176where
177    I: IntoIterator<Item = S>,
178    S: AsRef<str>,
179{
180    let bad = undeclared(names);
181    if bad.is_empty() {
182        Ok(())
183    } else {
184        Err(UndeclaredLevers(bad))
185    }
186}
187
188/// 2026-09-26: `std::env::var(name).ok()`, for a declared lever.
189///
190/// Debug builds assert that a `METRALE_*` name is declared, so a reader of a new name fails
191/// its own tests until the table carries it. Names outside the prefix are not checked.
192pub fn var(name: &str) -> Option<String> {
193    debug_assert!(declared_or_foreign(name), "{name} is not a declared lever");
194    std::env::var(name).ok()
195}
196
197/// 2026-09-26: `std::env::var_os(name)`, for a declared lever (see [`var`]).
198pub fn var_os(name: &str) -> Option<std::ffi::OsString> {
199    debug_assert!(declared_or_foreign(name), "{name} is not a declared lever");
200    std::env::var_os(name)
201}
202
203fn declared_or_foreign(name: &str) -> bool {
204    !name.starts_with(PREFIX) || lookup(name).is_some()
205}
206
207#[cfg(test)]
208#[path = "levers_tests.rs"]
209mod tests;