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