Skip to main content

rlx_ir/
env.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//! Unified `RLX_*` configuration — readable from **code overrides** or process env.
6//!
7//! Code overrides (via [`set`], [`RlxEnv::apply`], or [`RuntimeOverrides::install`])
8//! take precedence over `std::env` for the same key.
9//!
10//! ```rust
11//! use rlx_ir::env::{self, RlxEnv};
12//!
13//! // Single knob
14//! env::set("RLX_VERBOSE", "1");
15//! assert!(env::flag("RLX_VERBOSE"));
16//!
17//! // Bulk
18//! RlxEnv::new()
19//!     .set("RLX_DISABLE_MPSGRAPH", "1")
20//!     .set("RLX_MPSGRAPH_MIN_FLOPS", "100000")
21//!     .apply();
22//! ```
23
24use std::collections::HashMap;
25use std::ffi::OsString;
26use std::str::FromStr;
27use std::sync::{OnceLock, RwLock};
28
29static OVERRIDES: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
30
31fn map() -> &'static RwLock<HashMap<String, String>> {
32    OVERRIDES.get_or_init(|| RwLock::new(HashMap::new()))
33}
34
35/// Normalize to `RLX_*` form.
36pub fn normalize_key(key: &str) -> String {
37    if key.starts_with("RLX_") {
38        key.to_string()
39    } else {
40        format!("RLX_{key}")
41    }
42}
43
44/// Set a code-side override. Pass `"0"` / `"false"` to force a flag off even
45/// when the process environment has it enabled.
46pub fn set(key: impl AsRef<str>, value: impl Into<String>) {
47    let key = normalize_key(key.as_ref());
48    if let Ok(mut g) = map().write() {
49        g.insert(key, value.into());
50    }
51}
52
53/// Remove a code override for `key`; subsequent reads fall back to process env.
54pub fn unset(key: impl AsRef<str>) {
55    let key = normalize_key(key.as_ref());
56    if let Ok(mut g) = map().write() {
57        g.remove(&key);
58    }
59}
60
61/// Drop every code override.
62pub fn clear_overrides() {
63    if let Ok(mut g) = map().write() {
64        g.clear();
65    }
66}
67
68/// Read configuration: code override first, then `std::env::var`.
69pub fn var(key: &str) -> Option<String> {
70    let key = normalize_key(key);
71    if let Ok(g) = map().read() {
72        if let Some(v) = g.get(&key) {
73            return Some(v.clone());
74        }
75    }
76    std::env::var(&key).ok()
77}
78
79/// Like [`var`] but returns an `OsString` (mirrors `std::env::var_os`).
80pub fn var_os(key: &str) -> Option<OsString> {
81    var(key).map(Into::into)
82}
83
84/// True when the variable is set to a truthy value (`1`, `true`, `yes`, `on`, …).
85/// False when unset or set to `0` / `false` / `off` / `no` / empty.
86pub fn flag(key: &str) -> bool {
87    match var(key) {
88        Some(v) => truthy(&v),
89        None => false,
90    }
91}
92
93/// Like [`flag`], but returns `default` when the key is unset. Use for
94/// failsafe defaults that must stay on unless explicitly disabled (`=0`).
95pub fn flag_or(key: &str, default: bool) -> bool {
96    match var(key) {
97        Some(v) => truthy(&v),
98        None => default,
99    }
100}
101
102/// True when neither a code override nor process env provides the key.
103pub fn is_unset(key: &str) -> bool {
104    var(key).is_none()
105}
106
107/// Parse an integer/bool/string knob, falling back to `default`.
108pub fn parse_or<T: FromStr>(key: &str, default: T) -> T {
109    var(key).and_then(|s| s.parse().ok()).unwrap_or(default)
110}
111
112fn truthy(v: &str) -> bool {
113    let s = v.trim();
114    if s.is_empty() {
115        return false;
116    }
117    match s.to_ascii_lowercase().as_str() {
118        "0" | "false" | "off" | "no" => false,
119        "1" | "true" | "yes" | "on" => true,
120        _ if s.chars().all(|c| c.is_ascii_digit()) => s != "0",
121        _ => true, // any other non-empty value counts as enabled
122    }
123}
124
125/// Bulk builder for code-side `RLX_*` overrides.
126#[derive(Debug, Clone, Default)]
127pub struct RlxEnv {
128    pairs: Vec<(String, String)>,
129}
130
131impl RlxEnv {
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    pub fn set(mut self, key: impl AsRef<str>, value: impl Into<String>) -> Self {
137        self.pairs.push((normalize_key(key.as_ref()), value.into()));
138        self
139    }
140
141    pub fn flag(mut self, key: impl AsRef<str>, on: bool) -> Self {
142        self.pairs.push((
143            normalize_key(key.as_ref()),
144            if on { "1" } else { "0" }.into(),
145        ));
146        self
147    }
148
149    /// Apply all pairs to the global override map.
150    pub fn apply(self) {
151        for (k, v) in self.pairs {
152            set(&k, v);
153        }
154    }
155}
156
157/// RAII guard: installs overrides on construction, restores previous values on drop.
158pub struct RuntimeOverrides {
159    saved: Vec<(String, Option<String>)>,
160}
161
162impl RuntimeOverrides {
163    /// Install `pairs` for the lifetime of the returned guard.
164    pub fn install(pairs: impl IntoIterator<Item = (impl AsRef<str>, impl Into<String>)>) -> Self {
165        let mut saved = Vec::new();
166        for (key, value) in pairs {
167            let key = normalize_key(key.as_ref());
168            let prev = map().read().ok().and_then(|g| g.get(&key).cloned());
169            saved.push((key.clone(), prev));
170            set(&key, value);
171        }
172        Self { saved }
173    }
174}
175
176impl Drop for RuntimeOverrides {
177    fn drop(&mut self) {
178        for (key, prev) in self.saved.drain(..) {
179            match prev {
180                Some(v) => set(&key, v),
181                None => unset(&key),
182            }
183        }
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use std::sync::Mutex;
191
192    /// Env tests share the process-global override map.
193    static ENV_TEST_LOCK: Mutex<()> = Mutex::new(());
194
195    fn with_clean_overrides(f: impl FnOnce()) {
196        let _guard = ENV_TEST_LOCK.lock().expect("env test lock poisoned");
197        clear_overrides();
198        f();
199        clear_overrides();
200    }
201
202    #[test]
203    fn code_override_wins_over_process_env() {
204        with_clean_overrides(|| {
205            let _g = RuntimeOverrides::install([("VERBOSE", "2")]);
206            assert_eq!(var("RLX_VERBOSE"), Some("2".into()));
207            assert!(flag("RLX_VERBOSE"));
208        });
209    }
210
211    #[test]
212    fn flag_parses_falsy_override() {
213        with_clean_overrides(|| {
214            set("RLX_DISABLE_MPSGRAPH", "0");
215            assert!(!flag("RLX_DISABLE_MPSGRAPH"));
216        });
217    }
218
219    #[test]
220    fn rlx_env_bulk_apply() {
221        with_clean_overrides(|| {
222            RlxEnv::new()
223                .set("MPSGRAPH_MIN_FLOPS", "42")
224                .flag("USE_ICB", true)
225                .apply();
226            assert_eq!(parse_or("RLX_MPSGRAPH_MIN_FLOPS", 0u64), 42);
227            assert!(flag("RLX_USE_ICB"));
228        });
229    }
230}