Skip to main content

rlx_ir/
env_registry.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Single source of truth for `RLX_*` environment variables.
6//!
7//! Every process-env / [`crate::env`] read should eventually resolve through
8//! this registry (name, type, default, stability, aliases, layer). Prefer
9//! typed `*Config::from_env()` loaders and [`crate::env`] for I/O — this
10//! module owns **metadata** and alias resolution.
11//!
12//! - Public discoverability: [`format_catalog`] / `just env-catalog`
13//! - Full inventory docs: `docs/rlx-env-vars.md` (`just gen-rlx-env-vars`)
14//! - Deprecated aliases warn when `RLX_ENV_DEPRECATIONS=1` or `RLX_VERBOSE=1`
15//! - Prefer `CompileOptions` for compile semantics; env is CLI / bisect
16
17use crate::env;
18use std::sync::OnceLock;
19
20/// Value kind for documentation and typed loaders.
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub enum EnvKind {
23    Bool,
24    /// Unset → `default`; explicit `0`/`false` disables.
25    BoolOr {
26        default: bool,
27    },
28    U64,
29    String,
30    Enum(&'static [&'static str]),
31    Path,
32}
33
34/// How stable / user-facing a variable is.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum EnvStability {
37    /// Shown by `just env-catalog`; safe for scripts and docs.
38    Public,
39    /// Escape hatch / parity bisect; documented in the full inventory.
40    Bisect,
41    /// Bench / tooling / example-only.
42    Internal,
43    /// Still accepted; prefer `replace_with`.
44    Deprecated { replace_with: &'static str },
45}
46
47/// Which subsystem owns the option.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum EnvLayer {
50    Compile,
51    Device,
52    Runtime,
53    Backend(&'static str),
54    Tooling,
55}
56
57/// One registered `RLX_*` variable.
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub struct EnvVarEntry {
60    pub name: &'static str,
61    pub group: &'static str,
62    pub summary: &'static str,
63    pub kind: EnvKind,
64    pub stability: EnvStability,
65    pub aliases: &'static [&'static str],
66    pub layer: EnvLayer,
67}
68
69/// Backward-compatible view used by older callers / docs.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct EnvVarDoc {
72    pub name: &'static str,
73    pub group: &'static str,
74    pub summary: &'static str,
75}
76
77impl From<&EnvVarEntry> for EnvVarDoc {
78    fn from(e: &EnvVarEntry) -> Self {
79        Self {
80            name: e.name,
81            group: e.group,
82            summary: e.summary,
83        }
84    }
85}
86
87/// Full registry (Public + Bisect + Internal + Deprecated).
88pub const REGISTRY: &[EnvVarEntry] = &include!("env_registry_data.inc.rs");
89
90/// Public-only entries (curated catalog).
91pub fn public_entries() -> impl Iterator<Item = &'static EnvVarEntry> {
92    REGISTRY
93        .iter()
94        .filter(|e| matches!(e.stability, EnvStability::Public))
95}
96
97/// Look up by canonical name or alias.
98pub fn lookup(name: &str) -> Option<&'static EnvVarEntry> {
99    REGISTRY
100        .iter()
101        .find(|e| e.name == name || e.aliases.contains(&name))
102        .or_else(|| {
103            // Deprecated rows are looked up by their own `name`.
104            REGISTRY
105                .iter()
106                .find(|e| matches!(e.stability, EnvStability::Deprecated { .. }) && e.name == name)
107        })
108}
109
110/// True when `name` is a registered canonical name or alias.
111pub fn is_registered(name: &str) -> bool {
112    lookup(name).is_some()
113}
114
115/// Canonical name for `name` (identity if already canonical / unknown).
116pub fn canonical_name(name: &str) -> &str {
117    match lookup(name) {
118        Some(e) => e.name,
119        None => name,
120    }
121}
122
123fn maybe_warn_deprecated(used: &str, entry: &EnvVarEntry) {
124    if used == entry.name {
125        return;
126    }
127    if !matches!(entry.stability, EnvStability::Deprecated { .. }) && !entry.aliases.contains(&used)
128    {
129        return;
130    }
131    // Alias used, or deprecated entry itself.
132    if !env::flag("RLX_ENV_DEPRECATIONS") && !env::flag("RLX_VERBOSE") {
133        return;
134    }
135    static WARNED: OnceLock<std::sync::Mutex<std::collections::HashSet<String>>> = OnceLock::new();
136    let set = WARNED.get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()));
137    if let Ok(mut g) = set.lock() {
138        if !g.insert(used.to_string()) {
139            return;
140        }
141    }
142    let replace = match entry.stability {
143        EnvStability::Deprecated { replace_with } => replace_with,
144        _ => entry.name,
145    };
146    eprintln!("rlx: `{used}` is deprecated; use `{replace}`");
147}
148
149/// Read a string via [`env::var`], resolving aliases and deprecation notices.
150pub fn var(name: &str) -> Option<String> {
151    if let Some(e) = lookup(name) {
152        // Prefer canonical, then aliases (first set wins: canonical first).
153        if let Some(v) = env::var(e.name) {
154            if name != e.name {
155                maybe_warn_deprecated(name, e);
156            }
157            return Some(v);
158        }
159        for a in e.aliases {
160            if let Some(v) = env::var(a) {
161                maybe_warn_deprecated(a, e);
162                return Some(v);
163            }
164        }
165        // Deprecated entry looked up by old name: try replace_with already covered
166        // via lookup landing on Deprecated row — also try its replace_with.
167        if let EnvStability::Deprecated { replace_with } = e.stability {
168            maybe_warn_deprecated(name, e);
169            return env::var(replace_with);
170        }
171        return None;
172    }
173    env::var(name)
174}
175
176/// Flag read with alias resolution.
177pub fn flag(name: &str) -> bool {
178    match var(name) {
179        Some(v) => truthy(&v),
180        None => false,
181    }
182}
183
184/// Flag with default when unset (alias-aware).
185pub fn flag_or(name: &str, default: bool) -> bool {
186    match var(name) {
187        Some(v) => truthy(&v),
188        None => {
189            if let Some(e) = lookup(name)
190                && let EnvKind::BoolOr { default: d } = e.kind
191            {
192                return d;
193            }
194            default
195        }
196    }
197}
198
199/// Parse with default (alias-aware).
200pub fn parse_or<T: std::str::FromStr>(name: &str, default: T) -> T {
201    var(name).and_then(|s| s.parse().ok()).unwrap_or(default)
202}
203
204fn truthy(v: &str) -> bool {
205    let s = v.trim();
206    if s.is_empty() {
207        return false;
208    }
209    match s.to_ascii_lowercase().as_str() {
210        "0" | "false" | "off" | "no" => false,
211        "1" | "true" | "yes" | "on" => true,
212        _ if s.chars().all(|c| c.is_ascii_digit()) => s != "0",
213        _ => true,
214    }
215}
216
217/// Entries whose `group` matches, or all Public when `group` is empty.
218/// When `public_only`, Non-Public are excluded.
219pub fn entries_for_group(group: &str, public_only: bool) -> Vec<&'static EnvVarEntry> {
220    REGISTRY
221        .iter()
222        .filter(|e| {
223            if public_only && !matches!(e.stability, EnvStability::Public) {
224                return false;
225            }
226            group.is_empty() || e.group == group
227        })
228        .collect()
229}
230
231/// Pretty-print the Public catalog (optionally filtered by group).
232pub fn format_catalog(group: Option<&str>) -> String {
233    let g = group.unwrap_or("");
234    let entries = entries_for_group(g, true);
235    let mut out = String::from("# RLX environment catalog (Public)\n\n");
236    let mut cur = "";
237    for e in entries {
238        if e.group != cur {
239            cur = e.group;
240            out.push_str(&format!("## {cur}\n\n"));
241        }
242        out.push_str(&format!("- `{}` — {}\n", e.name, e.summary));
243    }
244    out.push_str(
245        "\nFull registry (Bisect / Internal / Deprecated): `docs/rlx-env-vars.md` \
246         (`just gen-rlx-env-vars`). Prefer CompileOptions when a setting changes \
247         compile semantics.\n",
248    );
249    out
250}
251
252/// Markdown dump of the full registry (for `gen-rlx-env-vars`).
253pub fn format_registry_markdown() -> String {
254    let mut out = String::from("# RLX environment variables (`RLX_*`)\n\n");
255    out.push_str(
256        "Generated from the [`env_registry`](../crates/core/rlx-ir/src/env_registry.rs) \
257         source of truth. Prefer `CompileOptions` when a setting changes compile \
258         semantics. Curated Public list: `just env-catalog`.\n\n",
259    );
260    out.push_str("## Legend\n\n");
261    out.push_str("| Mark | Meaning |\n|------|---------|\n");
262    out.push_str("| Public | Stable / documented (`just env-catalog`) |\n");
263    out.push_str("| Bisect | Escape hatch / parity |\n");
264    out.push_str("| Internal | Bench / tooling |\n");
265    out.push_str("| Deprecated | Use `replace_with` |\n\n");
266    out.push_str(&format!("**Registered names:** {}\n\n", REGISTRY.len()));
267
268    let mut by_group: std::collections::BTreeMap<&str, Vec<&EnvVarEntry>> =
269        std::collections::BTreeMap::new();
270    for e in REGISTRY {
271        by_group.entry(e.group).or_default().push(e);
272    }
273    out.push_str("## Groups\n\n");
274    for (g, es) in &by_group {
275        out.push_str(&format!("- [{g}](#{g}) — {}\n", es.len()));
276    }
277    out.push('\n');
278    for (g, es) in &by_group {
279        out.push_str(&format!("## {g}\n\n"));
280        out.push_str("| Name | Stability | Kind | Layer | Summary |\n");
281        out.push_str("|------|-----------|------|-------|----------|\n");
282        for e in es {
283            let stab = match e.stability {
284                EnvStability::Public => "Public",
285                EnvStability::Bisect => "Bisect",
286                EnvStability::Internal => "Internal",
287                EnvStability::Deprecated { replace_with } => {
288                    out.push_str(&format!(
289                        "| `{}` | Deprecated → `{replace_with}` | {:?} | {:?} | {} |\n",
290                        e.name, e.kind, e.layer, e.summary
291                    ));
292                    continue;
293                }
294            };
295            let kind = match e.kind {
296                EnvKind::Bool => "Bool".into(),
297                EnvKind::BoolOr { default } => format!("BoolOr({default})"),
298                EnvKind::U64 => "U64".into(),
299                EnvKind::String => "String".into(),
300                EnvKind::Enum(v) => format!("Enum{v:?}"),
301                EnvKind::Path => "Path".into(),
302            };
303            let layer = match e.layer {
304                EnvLayer::Compile => "Compile".into(),
305                EnvLayer::Device => "Device".into(),
306                EnvLayer::Runtime => "Runtime".into(),
307                EnvLayer::Backend(b) => format!("Backend({b})"),
308                EnvLayer::Tooling => "Tooling".into(),
309            };
310            let aliases = if e.aliases.is_empty() {
311                String::new()
312            } else {
313                format!(" aliases: {}", e.aliases.join(", "))
314            };
315            out.push_str(&format!(
316                "| `{}` | {stab} | {kind} | {layer} | {}{aliases} |\n",
317                e.name, e.summary
318            ));
319        }
320        out.push('\n');
321    }
322    out.push_str("## Maintenance\n\n");
323    out.push_str("```sh\njust gen-rlx-env-vars\n```\n\n");
324    out.push_str(
325        "Add new names to `env_registry_data.inc.rs` (or regenerate via \
326         `scripts/gen-rlx-env-vars.py --seed-reads`). Unregistered \
327         `env::flag(\"RLX_…\")` call sites fail `just check-rlx-env-vars`.\n",
328    );
329    out
330}
331
332/// Public catalog as docs (for `catalog_for_group` compat).
333pub fn catalog_for_group(group: &str) -> Vec<EnvVarDoc> {
334    public_entries()
335        .filter(|e| group.is_empty() || e.group == group)
336        .map(EnvVarDoc::from)
337        .collect()
338}
339
340/// Public [`EnvVarDoc`] slice built once (for `ENV_CATALOG` re-export).
341pub fn public_catalog_docs() -> &'static [EnvVarDoc] {
342    static DOCS: OnceLock<Vec<EnvVarDoc>> = OnceLock::new();
343    DOCS.get_or_init(|| public_entries().map(EnvVarDoc::from).collect())
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn registry_nonempty_and_prefixed() {
352        assert!(!REGISTRY.is_empty());
353        for e in REGISTRY {
354            assert!(e.name.starts_with("RLX_"), "{}", e.name);
355            assert!(!e.group.is_empty());
356            assert!(!e.summary.is_empty());
357        }
358    }
359
360    #[test]
361    fn public_catalog_has_device() {
362        let s = format_catalog(Some("device"));
363        assert!(s.contains("RLX_DEVICE"));
364        assert!(!s.contains("RLX_DISABLE_MPSGRAPH"));
365    }
366
367    #[test]
368    fn alias_resolves_metal_dequant() {
369        assert_eq!(
370            canonical_name("RLX_DISABLE_METAL_DEQUANT_GPU"),
371            "RLX_METAL_DEQUANT_GPU_DISABLE"
372        );
373        assert!(is_registered("RLX_DISABLE_METAL_DEQUANT_GPU"));
374        assert!(is_registered("RLX_METAL_DEQUANT_GPU_DISABLE"));
375    }
376
377    #[test]
378    fn lookup_cache_param() {
379        let e = lookup("RLX_CACHE_PARAM_INVARIANT").expect("registered");
380        assert!(matches!(e.stability, EnvStability::Public));
381        assert!(matches!(e.layer, EnvLayer::Compile));
382    }
383}