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