Skip to main content

patch_rexx/
env.rs

1//! REXX variable environments — scoping, stem variables, PROCEDURE EXPOSE.
2//!
3//! REXX uses dynamic scoping by default (all variables in one pool),
4//! with PROCEDURE creating new isolated scopes and EXPOSE selectively
5//! bringing variables into scope.
6
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10use crate::value::RexxValue;
11
12/// Information about the most recently trapped condition, read by `CONDITION()` BIF.
13#[derive(Debug, Clone, Default)]
14pub struct ConditionInfoData {
15    pub condition: String,
16    pub description: String,
17    pub instruction: String,
18    pub status: String,
19}
20
21/// A variable environment (scope).
22#[derive(Debug, Clone)]
23pub struct Environment {
24    /// Stack of variable scopes. Last is the current (innermost) scope.
25    scopes: Vec<Scope>,
26    /// Most recent condition trap info (for `CONDITION()` BIF).
27    pub condition_info: Option<ConditionInfoData>,
28    /// Current default ADDRESS environment (initially "SYSTEM").
29    address_default: String,
30    /// Previous ADDRESS environment (initially "SYSTEM").
31    address_previous: String,
32    /// Path to the currently executing source file (for external function resolution).
33    source_path: Option<PathBuf>,
34}
35
36/// A single variable scope.
37#[derive(Debug, Clone)]
38struct Scope {
39    /// Simple variables: name (uppercased) → value.
40    vars: HashMap<String, RexxValue>,
41    /// Stem variables: "STEM." → { tail → value }.
42    stems: HashMap<String, StemVar>,
43    /// Names exposed from the parent scope (written back on pop).
44    exposed: Vec<String>,
45}
46
47/// A stem variable with default and compound entries.
48#[derive(Debug, Clone)]
49struct StemVar {
50    /// Default value when a specific tail hasn't been set.
51    /// If None, compound references return their own name.
52    default: Option<RexxValue>,
53    /// Explicit compound entries: resolved tail → value.
54    entries: HashMap<String, RexxValue>,
55}
56
57impl Environment {
58    pub fn new() -> Self {
59        Self {
60            scopes: vec![Scope::new()],
61            condition_info: None,
62            address_default: "SYSTEM".to_string(),
63            address_previous: "SYSTEM".to_string(),
64            source_path: None,
65        }
66    }
67
68    /// Get a simple variable's value. In REXX, an unset variable
69    /// returns its own name in uppercase.
70    ///
71    /// REXX scoping: PROCEDURE creates an opaque wall. Only the
72    /// current (topmost) scope is searched. EXPOSE copies specific
73    /// variables into the new scope — that's the only way through.
74    pub fn get(&self, name: &str) -> RexxValue {
75        let upper = name.to_uppercase();
76        let scope = self.scopes.last().expect("environment has no scopes");
77        if let Some(val) = scope.vars.get(&upper) {
78            return val.clone();
79        }
80        // REXX default: unset variable returns its own name
81        RexxValue::new(upper)
82    }
83
84    /// Set a simple variable.
85    pub fn set(&mut self, name: &str, value: RexxValue) {
86        let upper = name.to_uppercase();
87        self.current_scope_mut().vars.insert(upper, value);
88    }
89
90    /// Get a compound variable: stem.tail
91    /// The tail components are resolved (each variable in the tail
92    /// is looked up) and concatenated with dots.
93    pub fn get_compound(&self, stem: &str, resolved_tail: &str) -> RexxValue {
94        let stem_upper = format!("{}.", stem.to_uppercase());
95        let tail_upper = resolved_tail.to_uppercase();
96
97        let scope = self.scopes.last().expect("environment has no scopes");
98        if let Some(stem_var) = scope.stems.get(&stem_upper) {
99            if let Some(val) = stem_var.entries.get(&tail_upper) {
100                return val.clone();
101            }
102            if let Some(ref default) = stem_var.default {
103                return default.clone();
104            }
105        }
106        // Default: return the compound name itself
107        RexxValue::new(format!("{stem_upper}{tail_upper}"))
108    }
109
110    /// Set a compound variable.
111    pub fn set_compound(&mut self, stem: &str, resolved_tail: &str, value: RexxValue) {
112        let stem_upper = format!("{}.", stem.to_uppercase());
113        let tail_upper = resolved_tail.to_uppercase();
114
115        let scope = self.current_scope_mut();
116        let stem_var = scope.stems.entry(stem_upper).or_insert_with(StemVar::new);
117        stem_var.entries.insert(tail_upper, value);
118    }
119
120    /// Set the default value for a stem (e.g., `stem. = 0`).
121    pub fn set_stem_default(&mut self, stem: &str, value: RexxValue) {
122        let stem_upper = format!("{}.", stem.to_uppercase());
123        let scope = self.current_scope_mut();
124        let stem_var = scope.stems.entry(stem_upper).or_insert_with(StemVar::new);
125        stem_var.default = Some(value);
126    }
127
128    /// DROP a variable — restore it to its uninitialized state.
129    pub fn drop(&mut self, name: &str) {
130        let upper = name.to_uppercase();
131        self.current_scope_mut().vars.remove(&upper);
132    }
133
134    /// PROCEDURE — push a new empty scope.
135    pub fn push_procedure(&mut self) {
136        self.scopes.push(Scope::new());
137    }
138
139    /// PROCEDURE EXPOSE — push a new scope that shares specified variables.
140    pub fn push_procedure_expose(&mut self, names: &[String]) {
141        let mut new_scope = Scope::new();
142
143        let caller = self.scopes.last().expect("environment has no scopes");
144        for name in names {
145            let upper = name.to_uppercase();
146            if upper.ends_with('.') {
147                // Expose a stem — copy from caller's scope only
148                if let Some(stem_var) = caller.stems.get(&upper) {
149                    new_scope.stems.insert(upper.clone(), stem_var.clone());
150                }
151            } else {
152                // Expose a simple variable — copy from caller's scope only
153                if let Some(val) = caller.vars.get(&upper) {
154                    new_scope.vars.insert(upper.clone(), val.clone());
155                }
156            }
157        }
158
159        new_scope.exposed = names.iter().map(|n| n.to_uppercase()).collect();
160        self.scopes.push(new_scope);
161    }
162
163    /// Pop the current scope (on RETURN from a PROCEDURE).
164    /// Writes back any exposed variables to the parent scope.
165    pub fn pop_procedure(&mut self) {
166        debug_assert!(
167            self.scopes.len() > 1,
168            "pop_procedure called with no nested scope"
169        );
170        if self.scopes.len() > 1 {
171            let popped = self.scopes.pop().unwrap();
172            let parent = self.scopes.last_mut().unwrap();
173            for name in &popped.exposed {
174                if name.ends_with('.') {
175                    if let Some(stem_var) = popped.stems.get(name) {
176                        parent.stems.insert(name.clone(), stem_var.clone());
177                    }
178                } else if let Some(val) = popped.vars.get(name) {
179                    parent.vars.insert(name.clone(), val.clone());
180                } else {
181                    // Variable was dropped in the inner scope — drop in parent too
182                    parent.vars.remove(name);
183                }
184            }
185        }
186    }
187
188    /// Return the current default ADDRESS environment name.
189    pub fn address(&self) -> &str {
190        &self.address_default
191    }
192
193    /// Set a new default ADDRESS environment, saving the old as previous.
194    pub fn set_address(&mut self, env: &str) {
195        self.address_previous = std::mem::replace(&mut self.address_default, env.to_string());
196    }
197
198    /// Swap default ↔ previous ADDRESS environment.
199    pub fn swap_address(&mut self) {
200        std::mem::swap(&mut self.address_default, &mut self.address_previous);
201    }
202
203    /// Set the source file path (for external function resolution and PARSE SOURCE).
204    pub fn set_source_path(&mut self, path: PathBuf) {
205        self.source_path = Some(path);
206    }
207
208    /// Clear the source file path (restores REPL/no-file state).
209    pub fn clear_source_path(&mut self) {
210        self.source_path = None;
211    }
212
213    /// Get the source file path.
214    pub fn source_path(&self) -> Option<&Path> {
215        self.source_path.as_deref()
216    }
217
218    /// Get the directory containing the source file.
219    pub fn source_dir(&self) -> Option<&Path> {
220        self.source_path.as_deref().and_then(Path::parent)
221    }
222
223    /// Set condition info (called when a trap fires).
224    pub fn set_condition_info(&mut self, info: ConditionInfoData) {
225        self.condition_info = Some(info);
226    }
227
228    /// Check if a simple variable has been explicitly set (for SIGNAL ON NOVALUE).
229    pub fn is_set(&self, name: &str) -> bool {
230        let upper = name.to_uppercase();
231        self.scopes
232            .last()
233            .is_some_and(|s| s.vars.contains_key(&upper))
234    }
235
236    /// Check if a compound variable has been explicitly set (for SIGNAL ON NOVALUE).
237    /// Returns true if the specific tail has a value OR the stem has a default.
238    pub fn is_compound_set(&self, stem: &str, resolved_tail: &str) -> bool {
239        let stem_upper = format!("{}.", stem.to_uppercase());
240        let tail_upper = resolved_tail.to_uppercase();
241        let scope = self.scopes.last().expect("environment has no scopes");
242        if let Some(stem_var) = scope.stems.get(&stem_upper) {
243            stem_var.entries.contains_key(&tail_upper) || stem_var.default.is_some()
244        } else {
245            false
246        }
247    }
248
249    fn current_scope_mut(&mut self) -> &mut Scope {
250        self.scopes.last_mut().expect("environment has no scopes")
251    }
252}
253
254impl Default for Environment {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260/// A restricted view of [`Environment`] that exposes only variable operations.
261///
262/// Passed to [`CommandHandlerWithEnv`](crate::eval) closures so that handlers
263/// can read and write REXX variables without access to ADDRESS routing,
264/// PROCEDURE scoping, or other evaluator-internal state.
265pub struct EnvVars<'a>(&'a mut Environment);
266
267impl<'a> EnvVars<'a> {
268    pub(crate) fn new(env: &'a mut Environment) -> Self {
269        Self(env)
270    }
271
272    /// Get a simple variable's value.
273    pub fn get(&self, name: &str) -> RexxValue {
274        self.0.get(name)
275    }
276
277    /// Set a simple variable.
278    pub fn set(&mut self, name: &str, value: RexxValue) {
279        self.0.set(name, value);
280    }
281
282    /// Get a compound variable: `stem.tail`.
283    pub fn get_compound(&self, stem: &str, resolved_tail: &str) -> RexxValue {
284        self.0.get_compound(stem, resolved_tail)
285    }
286
287    /// Set a compound variable.
288    pub fn set_compound(&mut self, stem: &str, resolved_tail: &str, value: RexxValue) {
289        self.0.set_compound(stem, resolved_tail, value);
290    }
291
292    /// Set the default value for a stem (e.g., `stem. = 0`).
293    pub fn set_stem_default(&mut self, stem: &str, value: RexxValue) {
294        self.0.set_stem_default(stem, value);
295    }
296
297    /// Drop (unset) a variable.
298    pub fn drop(&mut self, name: &str) {
299        self.0.drop(name);
300    }
301
302    /// Check whether a simple variable is set.
303    pub fn is_set(&self, name: &str) -> bool {
304        self.0.is_set(name)
305    }
306
307    /// Check whether a compound variable is set.
308    pub fn is_compound_set(&self, stem: &str, resolved_tail: &str) -> bool {
309        self.0.is_compound_set(stem, resolved_tail)
310    }
311}
312
313impl Scope {
314    fn new() -> Self {
315        Self {
316            vars: HashMap::new(),
317            stems: HashMap::new(),
318            exposed: Vec::new(),
319        }
320    }
321}
322
323impl StemVar {
324    fn new() -> Self {
325        Self {
326            default: None,
327            entries: HashMap::new(),
328        }
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn unset_variable_returns_name() {
338        let env = Environment::new();
339        assert_eq!(env.get("foo").as_str(), "FOO");
340    }
341
342    #[test]
343    fn set_and_get() {
344        let mut env = Environment::new();
345        env.set("name", RexxValue::new("Alice"));
346        assert_eq!(env.get("name").as_str(), "Alice");
347    }
348
349    #[test]
350    fn case_insensitive() {
351        let mut env = Environment::new();
352        env.set("Name", RexxValue::new("Bob"));
353        assert_eq!(env.get("NAME").as_str(), "Bob");
354        assert_eq!(env.get("name").as_str(), "Bob");
355    }
356
357    #[test]
358    fn stem_variables() {
359        let mut env = Environment::new();
360        env.set_compound("arr", "1", RexxValue::new("first"));
361        env.set_compound("arr", "2", RexxValue::new("second"));
362        assert_eq!(env.get_compound("arr", "1").as_str(), "first");
363        assert_eq!(env.get_compound("arr", "2").as_str(), "second");
364        // Unset compound returns its name
365        assert_eq!(env.get_compound("arr", "3").as_str(), "ARR.3");
366    }
367
368    #[test]
369    fn stem_default() {
370        let mut env = Environment::new();
371        env.set_stem_default("count", RexxValue::new("0"));
372        assert_eq!(env.get_compound("count", "anything").as_str(), "0");
373        env.set_compound("count", "special", RexxValue::new("99"));
374        assert_eq!(env.get_compound("count", "special").as_str(), "99");
375        assert_eq!(env.get_compound("count", "other").as_str(), "0");
376    }
377
378    #[test]
379    fn procedure_scope() {
380        let mut env = Environment::new();
381        env.set("x", RexxValue::new("outer"));
382        env.push_procedure();
383        // x is not visible in the new scope
384        assert_eq!(env.get("x").as_str(), "X");
385        env.set("x", RexxValue::new("inner"));
386        assert_eq!(env.get("x").as_str(), "inner");
387        env.pop_procedure();
388        assert_eq!(env.get("x").as_str(), "outer");
389    }
390
391    #[test]
392    fn procedure_expose() {
393        let mut env = Environment::new();
394        env.set("x", RexxValue::new("shared"));
395        env.set("y", RexxValue::new("hidden"));
396        env.push_procedure_expose(&["x".into()]);
397        assert_eq!(env.get("x").as_str(), "shared");
398        assert_eq!(env.get("y").as_str(), "Y"); // not exposed
399        env.pop_procedure();
400    }
401
402    #[test]
403    fn drop_variable() {
404        let mut env = Environment::new();
405        env.set("x", RexxValue::new("42"));
406        assert!(env.is_set("x"));
407        env.drop("x");
408        assert!(!env.is_set("x"));
409        assert_eq!(env.get("x").as_str(), "X");
410    }
411}