Skip to main content

kaish_kernel/interpreter/
scope.rs

1//! Variable scope management for kaish.
2//!
3//! Scopes provide variable bindings with:
4//! - Nested scope frames (push/pop for loops, tool calls)
5//! - The special `$?` variable tracking the last command result
6//! - Path resolution for nested access (`${VAR.field[0]}`)
7
8use std::collections::{HashMap, HashSet};
9
10use crate::ast::{Value, VarPath, VarSegment};
11
12use super::result::ExecResult;
13
14/// Variable scope with nested frames and last-result tracking.
15///
16/// Variables are looked up from innermost to outermost frame.
17/// The `?` variable always refers to the last command result.
18#[derive(Debug, Clone)]
19pub struct Scope {
20    /// Stack of variable frames. Last element is the innermost scope.
21    frames: Vec<HashMap<String, Value>>,
22    /// Variables marked for export to child processes.
23    exported: HashSet<String>,
24    /// The result of the last command execution.
25    last_result: ExecResult,
26    /// Script or tool name ($0).
27    script_name: String,
28    /// Positional arguments ($1-$9, $@, $#).
29    positional: Vec<String>,
30    /// Error exit mode (set -e): exit on any command failure.
31    error_exit: bool,
32    /// Current process ID ($$), captured at scope creation.
33    pid: u32,
34}
35
36impl Scope {
37    /// Create a new scope with one empty frame.
38    pub fn new() -> Self {
39        Self {
40            frames: vec![HashMap::new()],
41            exported: HashSet::new(),
42            last_result: ExecResult::default(),
43            script_name: String::new(),
44            positional: Vec::new(),
45            error_exit: false,
46            pid: std::process::id(),
47        }
48    }
49
50    /// Get the process ID ($$).
51    pub fn pid(&self) -> u32 {
52        self.pid
53    }
54
55    /// Push a new scope frame (for entering a loop, tool call, etc.)
56    pub fn push_frame(&mut self) {
57        self.frames.push(HashMap::new());
58    }
59
60    /// Pop the innermost scope frame.
61    ///
62    /// Panics if attempting to pop the last frame.
63    pub fn pop_frame(&mut self) {
64        if self.frames.len() > 1 {
65            self.frames.pop();
66        } else {
67            panic!("cannot pop the root scope frame");
68        }
69    }
70
71    /// Set a variable in the current (innermost) frame.
72    ///
73    /// Use this for `local` variable declarations.
74    pub fn set(&mut self, name: impl Into<String>, value: Value) {
75        if let Some(frame) = self.frames.last_mut() {
76            frame.insert(name.into(), value);
77        }
78    }
79
80    /// Set a variable with global semantics (shell default).
81    ///
82    /// If the variable exists in any frame, update it there.
83    /// Otherwise, create it in the outermost (root) frame.
84    /// Use this for non-local variable assignments.
85    pub fn set_global(&mut self, name: impl Into<String>, value: Value) {
86        let name = name.into();
87
88        // Search from innermost to outermost to find existing variable
89        for frame in self.frames.iter_mut().rev() {
90            if let std::collections::hash_map::Entry::Occupied(mut e) = frame.entry(name.clone()) {
91                e.insert(value);
92                return;
93            }
94        }
95
96        // Variable doesn't exist - create in root frame (index 0)
97        if let Some(frame) = self.frames.first_mut() {
98            frame.insert(name, value);
99        }
100    }
101
102    /// Get a variable by name, searching from innermost to outermost frame.
103    pub fn get(&self, name: &str) -> Option<&Value> {
104        for frame in self.frames.iter().rev() {
105            if let Some(value) = frame.get(name) {
106                return Some(value);
107            }
108        }
109        None
110    }
111
112    /// Remove a variable, searching from innermost to outermost frame.
113    ///
114    /// Returns the removed value if found, None otherwise.
115    pub fn remove(&mut self, name: &str) -> Option<Value> {
116        for frame in self.frames.iter_mut().rev() {
117            if let Some(value) = frame.remove(name) {
118                return Some(value);
119            }
120        }
121        None
122    }
123
124    /// Set the last command result (accessible via `$?`).
125    pub fn set_last_result(&mut self, result: ExecResult) {
126        self.last_result = result;
127    }
128
129    /// Get the last command result.
130    pub fn last_result(&self) -> &ExecResult {
131        &self.last_result
132    }
133
134    /// Set the positional parameters ($0, $1-$9, $@, $#).
135    ///
136    /// The script_name becomes $0, and args become $1, $2, etc.
137    pub fn set_positional(&mut self, script_name: impl Into<String>, args: Vec<String>) {
138        self.script_name = script_name.into();
139        self.positional = args;
140    }
141
142    /// Save current positional parameters for later restoration.
143    ///
144    /// Returns (script_name, args) tuple that can be passed to set_positional.
145    pub fn save_positional(&self) -> (String, Vec<String>) {
146        (self.script_name.clone(), self.positional.clone())
147    }
148
149    /// Get a positional parameter by index ($0-$9).
150    ///
151    /// $0 returns the script name, $1-$9 return arguments.
152    pub fn get_positional(&self, n: usize) -> Option<&str> {
153        if n == 0 {
154            if self.script_name.is_empty() {
155                None
156            } else {
157                Some(&self.script_name)
158            }
159        } else {
160            self.positional.get(n - 1).map(|s| s.as_str())
161        }
162    }
163
164    /// Get all positional arguments as a slice ($@).
165    pub fn all_args(&self) -> &[String] {
166        &self.positional
167    }
168
169    /// Get the count of positional arguments ($#).
170    pub fn arg_count(&self) -> usize {
171        self.positional.len()
172    }
173
174    /// Check if error-exit mode is enabled (set -e).
175    pub fn error_exit_enabled(&self) -> bool {
176        self.error_exit
177    }
178
179    /// Set error-exit mode (set -e / set +e).
180    pub fn set_error_exit(&mut self, enabled: bool) {
181        self.error_exit = enabled;
182    }
183
184    /// Mark a variable as exported (visible to child processes).
185    ///
186    /// The variable doesn't need to exist yet; it will be exported when set.
187    pub fn export(&mut self, name: impl Into<String>) {
188        self.exported.insert(name.into());
189    }
190
191    /// Check if a variable is marked for export.
192    pub fn is_exported(&self, name: &str) -> bool {
193        self.exported.contains(name)
194    }
195
196    /// Set a variable and mark it as exported.
197    pub fn set_exported(&mut self, name: impl Into<String>, value: Value) {
198        let name = name.into();
199        self.set(&name, value);
200        self.export(name);
201    }
202
203    /// Unmark a variable from export.
204    pub fn unexport(&mut self, name: &str) {
205        self.exported.remove(name);
206    }
207
208    /// Get all exported variables with their values.
209    ///
210    /// Only returns variables that exist and are marked for export.
211    pub fn exported_vars(&self) -> Vec<(String, Value)> {
212        let mut result = Vec::new();
213        for name in &self.exported {
214            if let Some(value) = self.get(name) {
215                result.push((name.clone(), value.clone()));
216            }
217        }
218        result.sort_by(|(a, _), (b, _)| a.cmp(b));
219        result
220    }
221
222    /// Get all exported variable names.
223    pub fn exported_names(&self) -> Vec<&str> {
224        let mut names: Vec<&str> = self.exported.iter().map(|s| s.as_str()).collect();
225        names.sort();
226        names
227    }
228
229    /// Resolve a variable path like `${VAR}` or `${?.field}`.
230    ///
231    /// Returns None if the path cannot be resolved.
232    /// Field access is only supported for the special `$?` variable.
233    pub fn resolve_path(&self, path: &VarPath) -> Option<Value> {
234        if path.segments.is_empty() {
235            return None;
236        }
237
238        // Get the root variable name
239        let VarSegment::Field(root_name) = &path.segments[0];
240
241        // Special case: $? (last result)
242        if root_name == "?" {
243            return self.resolve_result_path(&path.segments[1..]);
244        }
245
246        // For regular variables, only simple access is supported
247        if path.segments.len() > 1 {
248            return None; // No nested field access for regular variables
249        }
250
251        self.get(root_name).cloned()
252    }
253
254    /// Resolve path segments on the last result ($?).
255    ///
256    /// `$?` alone returns the exit code as an integer (0-255).
257    /// `${?.code}`, `${?.ok}`, `${?.out}`, `${?.err}` access specific fields.
258    fn resolve_result_path(&self, segments: &[VarSegment]) -> Option<Value> {
259        if segments.is_empty() {
260            // $? alone returns just the exit code as an integer (bash-compatible)
261            return Some(Value::Int(self.last_result.code));
262        }
263
264        // Allow ${?.code}, ${?.ok}, etc.
265        let VarSegment::Field(field_name) = &segments[0];
266
267        // Only single-level field access on $?
268        if segments.len() > 1 {
269            return None;
270        }
271
272        // Get the field value from the result
273        self.last_result.get_field(field_name)
274    }
275
276    /// Check if a variable exists in any frame.
277    pub fn contains(&self, name: &str) -> bool {
278        self.get(name).is_some()
279    }
280
281    /// Get all variable names in scope (for debugging/introspection).
282    pub fn all_names(&self) -> Vec<&str> {
283        let mut names: Vec<&str> = self
284            .frames
285            .iter()
286            .flat_map(|f| f.keys().map(|s| s.as_str()))
287            .collect();
288        names.sort();
289        names.dedup();
290        names
291    }
292
293    /// Get all variables as (name, value) pairs.
294    ///
295    /// Variables are deduplicated, with inner frames shadowing outer ones.
296    pub fn all(&self) -> Vec<(String, Value)> {
297        let mut result = std::collections::HashMap::new();
298        // Iterate outer to inner so inner frames override
299        for frame in &self.frames {
300            for (name, value) in frame {
301                result.insert(name.clone(), value.clone());
302            }
303        }
304        let mut pairs: Vec<_> = result.into_iter().collect();
305        pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
306        pairs
307    }
308}
309
310impl Default for Scope {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn new_scope_has_one_frame() {
322        let scope = Scope::new();
323        assert_eq!(scope.frames.len(), 1);
324    }
325
326    #[test]
327    fn set_and_get_variable() {
328        let mut scope = Scope::new();
329        scope.set("X", Value::Int(42));
330        assert_eq!(scope.get("X"), Some(&Value::Int(42)));
331    }
332
333    #[test]
334    fn get_nonexistent_returns_none() {
335        let scope = Scope::new();
336        assert_eq!(scope.get("MISSING"), None);
337    }
338
339    #[test]
340    fn inner_frame_shadows_outer() {
341        let mut scope = Scope::new();
342        scope.set("X", Value::Int(1));
343        scope.push_frame();
344        scope.set("X", Value::Int(2));
345        assert_eq!(scope.get("X"), Some(&Value::Int(2)));
346        scope.pop_frame();
347        assert_eq!(scope.get("X"), Some(&Value::Int(1)));
348    }
349
350    #[test]
351    fn inner_frame_can_see_outer_vars() {
352        let mut scope = Scope::new();
353        scope.set("OUTER", Value::String("visible".into()));
354        scope.push_frame();
355        assert_eq!(scope.get("OUTER"), Some(&Value::String("visible".into())));
356    }
357
358    #[test]
359    fn resolve_simple_path() {
360        let mut scope = Scope::new();
361        scope.set("NAME", Value::String("Alice".into()));
362
363        let path = VarPath::simple("NAME");
364        assert_eq!(
365            scope.resolve_path(&path),
366            Some(Value::String("Alice".into()))
367        );
368    }
369
370    #[test]
371    fn resolve_last_result_ok() {
372        let mut scope = Scope::new();
373        scope.set_last_result(ExecResult::success("output"));
374
375        let path = VarPath {
376            segments: vec![
377                VarSegment::Field("?".into()),
378                VarSegment::Field("ok".into()),
379            ],
380        };
381        assert_eq!(scope.resolve_path(&path), Some(Value::Bool(true)));
382    }
383
384    #[test]
385    fn resolve_last_result_code() {
386        let mut scope = Scope::new();
387        scope.set_last_result(ExecResult::failure(127, "not found"));
388
389        let path = VarPath {
390            segments: vec![
391                VarSegment::Field("?".into()),
392                VarSegment::Field("code".into()),
393            ],
394        };
395        assert_eq!(scope.resolve_path(&path), Some(Value::Int(127)));
396    }
397
398    #[test]
399    fn resolve_last_result_data_field() {
400        let mut scope = Scope::new();
401        scope.set_last_result(ExecResult::success(r#"{"count": 5}"#));
402
403        // ${?.data} - only single-level field access is supported on $?
404        let path = VarPath {
405            segments: vec![
406                VarSegment::Field("?".into()),
407                VarSegment::Field("data".into()),
408            ],
409        };
410        // data is now a Value::Json for structured data
411        let result = scope.resolve_path(&path);
412        assert!(result.is_some());
413        if let Some(Value::Json(json)) = result {
414            assert_eq!(json.get("count"), Some(&serde_json::json!(5)));
415        } else {
416            panic!("expected Value::Json, got {:?}", result);
417        }
418    }
419
420    #[test]
421    fn resolve_invalid_path_returns_none() {
422        let mut scope = Scope::new();
423        scope.set("X", Value::Int(42));
424
425        // Cannot do field access on an int
426        let path = VarPath {
427            segments: vec![
428                VarSegment::Field("X".into()),
429                VarSegment::Field("invalid".into()),
430            ],
431        };
432        assert_eq!(scope.resolve_path(&path), None);
433    }
434
435    #[test]
436    fn contains_finds_variable() {
437        let mut scope = Scope::new();
438        scope.set("EXISTS", Value::Bool(true));
439        assert!(scope.contains("EXISTS"));
440        assert!(!scope.contains("MISSING"));
441    }
442
443    #[test]
444    fn all_names_lists_variables() {
445        let mut scope = Scope::new();
446        scope.set("A", Value::Int(1));
447        scope.set("B", Value::Int(2));
448        scope.push_frame();
449        scope.set("C", Value::Int(3));
450
451        let names = scope.all_names();
452        assert!(names.contains(&"A"));
453        assert!(names.contains(&"B"));
454        assert!(names.contains(&"C"));
455    }
456
457    #[test]
458    #[should_panic(expected = "cannot pop the root scope frame")]
459    fn pop_root_frame_panics() {
460        let mut scope = Scope::new();
461        scope.pop_frame();
462    }
463
464    #[test]
465    fn positional_params_basic() {
466        let mut scope = Scope::new();
467        scope.set_positional("my_tool", vec!["arg1".into(), "arg2".into(), "arg3".into()]);
468
469        // $0 is the script/tool name
470        assert_eq!(scope.get_positional(0), Some("my_tool"));
471        // $1, $2, $3 are the arguments
472        assert_eq!(scope.get_positional(1), Some("arg1"));
473        assert_eq!(scope.get_positional(2), Some("arg2"));
474        assert_eq!(scope.get_positional(3), Some("arg3"));
475        // $4 doesn't exist
476        assert_eq!(scope.get_positional(4), None);
477    }
478
479    #[test]
480    fn positional_params_empty() {
481        let scope = Scope::new();
482        // No positional params set
483        assert_eq!(scope.get_positional(0), None);
484        assert_eq!(scope.get_positional(1), None);
485        assert_eq!(scope.arg_count(), 0);
486        assert!(scope.all_args().is_empty());
487    }
488
489    #[test]
490    fn all_args_returns_slice() {
491        let mut scope = Scope::new();
492        scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
493
494        let args = scope.all_args();
495        assert_eq!(args, &["a", "b", "c"]);
496    }
497
498    #[test]
499    fn arg_count_returns_count() {
500        let mut scope = Scope::new();
501        scope.set_positional("test", vec!["one".into(), "two".into()]);
502
503        assert_eq!(scope.arg_count(), 2);
504    }
505
506    #[test]
507    fn export_marks_variable() {
508        let mut scope = Scope::new();
509        scope.set("X", Value::Int(42));
510
511        assert!(!scope.is_exported("X"));
512        scope.export("X");
513        assert!(scope.is_exported("X"));
514    }
515
516    #[test]
517    fn set_exported_sets_and_exports() {
518        let mut scope = Scope::new();
519        scope.set_exported("PATH", Value::String("/usr/bin".into()));
520
521        assert!(scope.is_exported("PATH"));
522        assert_eq!(scope.get("PATH"), Some(&Value::String("/usr/bin".into())));
523    }
524
525    #[test]
526    fn unexport_removes_export_marker() {
527        let mut scope = Scope::new();
528        scope.set_exported("VAR", Value::Int(1));
529        assert!(scope.is_exported("VAR"));
530
531        scope.unexport("VAR");
532        assert!(!scope.is_exported("VAR"));
533        // Variable still exists, just not exported
534        assert!(scope.get("VAR").is_some());
535    }
536
537    #[test]
538    fn exported_vars_returns_only_exported_with_values() {
539        let mut scope = Scope::new();
540        scope.set_exported("A", Value::Int(1));
541        scope.set_exported("B", Value::Int(2));
542        scope.set("C", Value::Int(3)); // Not exported
543        scope.export("D"); // Exported but no value
544
545        let exported = scope.exported_vars();
546        assert_eq!(exported.len(), 2);
547        assert_eq!(exported[0], ("A".to_string(), Value::Int(1)));
548        assert_eq!(exported[1], ("B".to_string(), Value::Int(2)));
549    }
550
551    #[test]
552    fn exported_names_returns_sorted_names() {
553        let mut scope = Scope::new();
554        scope.export("Z");
555        scope.export("A");
556        scope.export("M");
557
558        let names = scope.exported_names();
559        assert_eq!(names, vec!["A", "M", "Z"]);
560    }
561}