1use crate::env;
18use std::sync::OnceLock;
19
20#[derive(Debug, Clone, Copy, PartialEq)]
22pub enum EnvKind {
23 Bool,
24 BoolOr {
26 default: bool,
27 },
28 U64,
29 String,
30 Enum(&'static [&'static str]),
31 Path,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum EnvStability {
37 Public,
39 Bisect,
41 Internal,
43 Deprecated { replace_with: &'static str },
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum EnvLayer {
50 Compile,
51 Device,
52 Runtime,
53 Backend(&'static str),
54 Tooling,
55}
56
57#[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#[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
87pub const REGISTRY: &[EnvVarEntry] = &include!("env_registry_data.inc.rs");
89
90pub fn public_entries() -> impl Iterator<Item = &'static EnvVarEntry> {
92 REGISTRY
93 .iter()
94 .filter(|e| matches!(e.stability, EnvStability::Public))
95}
96
97pub 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 REGISTRY
105 .iter()
106 .find(|e| matches!(e.stability, EnvStability::Deprecated { .. }) && e.name == name)
107 })
108}
109
110pub fn is_registered(name: &str) -> bool {
112 lookup(name).is_some()
113}
114
115pub 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 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
149pub fn var(name: &str) -> Option<String> {
151 if let Some(e) = lookup(name) {
152 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 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
176pub fn flag(name: &str) -> bool {
178 match var(name) {
179 Some(v) => truthy(&v),
180 None => false,
181 }
182}
183
184pub 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
199pub 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
217pub 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
231pub 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
252pub 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
332pub 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
340pub 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}