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