Skip to main content

bashkit/interpreter/
state.rs

1//! Interpreter state types
2
3/// Control flow signals from commands like break, continue, return
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum ControlFlow {
6    #[default]
7    None,
8    /// Break out of a loop (with optional level count)
9    Break(u32),
10    /// Continue to next iteration (with optional level count)
11    Continue(u32),
12    /// Return from a function (with exit code)
13    Return(i32),
14}
15
16/// Structured side-effect channel for builtins that need to communicate
17/// state changes back to the interpreter.
18///
19/// Used only for state with invariants that builtins can't enforce directly:
20/// - Arrays: need memory budget checking via `insert_array_checked`
21/// - Positional params: stored on the call stack, not in Context
22/// - History: needs VFS persistence via `save_history`
23/// - Exit code: interpreter tracks `last_exit_code` separately
24///
25/// Simple state (aliases, traps) is mutated directly via [`ShellRef`].
26#[derive(Debug, Clone)]
27pub enum BuiltinSideEffect {
28    /// Shift N positional parameters (replaces `_SHIFT_COUNT`).
29    ShiftPositional(usize),
30    /// Replace all positional parameters (replaces `_SET_POSITIONAL`).
31    SetPositional(Vec<String>),
32    /// Populate an indexed array variable (replaces `_ARRAY_READ_*`).
33    SetArray { name: String, elements: Vec<String> },
34    /// Populate an indexed array with index->value pairs (for mapfile).
35    SetIndexedArray {
36        name: String,
37        entries: Vec<(usize, String)>,
38    },
39    /// Remove an indexed array.
40    RemoveArray(String),
41    /// Clear command history (interpreter persists to VFS).
42    ClearHistory,
43    /// Set the last exit code (for wait builtin).
44    SetLastExitCode(i32),
45}
46
47/// Result of executing a bash script.
48#[derive(Debug, Clone, Default)]
49pub struct ExecResult {
50    /// Standard output
51    pub stdout: String,
52    /// Standard error
53    pub stderr: String,
54    /// Exit code
55    pub exit_code: i32,
56    /// Control flow signal (break, continue, return)
57    pub control_flow: ControlFlow,
58    /// Whether stdout was truncated due to output size limits
59    pub stdout_truncated: bool,
60    /// Whether stderr was truncated due to output size limits
61    pub stderr_truncated: bool,
62    /// Final environment state after execution (opt-in via `capture_final_env`)
63    pub final_env: Option<std::collections::HashMap<String, String>>,
64    /// Structured trace events (empty when `TraceMode::Off`).
65    pub events: Vec<crate::trace::TraceEvent>,
66    /// Structured side effects from builtin execution.
67    /// The interpreter processes these after the builtin returns.
68    pub side_effects: Vec<BuiltinSideEffect>,
69}
70
71impl ExecResult {
72    /// Create a successful result with the given stdout.
73    pub fn ok(stdout: impl Into<String>) -> Self {
74        Self {
75            stdout: stdout.into(),
76            stderr: String::new(),
77            exit_code: 0,
78            ..Default::default()
79        }
80    }
81
82    /// Create a failed result with the given stderr.
83    pub fn err(stderr: impl Into<String>, exit_code: i32) -> Self {
84        Self {
85            stdout: String::new(),
86            stderr: stderr.into(),
87            exit_code,
88            ..Default::default()
89        }
90    }
91
92    /// Create a result with stdout and custom exit code.
93    pub fn with_code(stdout: impl Into<String>, exit_code: i32) -> Self {
94        Self {
95            stdout: stdout.into(),
96            stderr: String::new(),
97            exit_code,
98            ..Default::default()
99        }
100    }
101
102    /// Create a result with a control flow signal
103    pub fn with_control_flow(control_flow: ControlFlow) -> Self {
104        Self {
105            control_flow,
106            ..Default::default()
107        }
108    }
109
110    /// Check if the result indicates success.
111    pub fn is_success(&self) -> bool {
112        self.exit_code == 0
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    // --- ControlFlow ---
121
122    #[test]
123    fn control_flow_default_is_none() {
124        assert_eq!(ControlFlow::default(), ControlFlow::None);
125    }
126
127    #[test]
128    fn control_flow_break_stores_level() {
129        let cf = ControlFlow::Break(2);
130        assert_eq!(cf, ControlFlow::Break(2));
131        assert_ne!(cf, ControlFlow::Break(1));
132    }
133
134    #[test]
135    fn control_flow_continue_stores_level() {
136        let cf = ControlFlow::Continue(3);
137        assert_eq!(cf, ControlFlow::Continue(3));
138    }
139
140    #[test]
141    fn control_flow_return_stores_code() {
142        let cf = ControlFlow::Return(42);
143        assert_eq!(cf, ControlFlow::Return(42));
144    }
145
146    #[test]
147    fn control_flow_variants_not_equal() {
148        assert_ne!(ControlFlow::None, ControlFlow::Break(0));
149        assert_ne!(ControlFlow::Break(1), ControlFlow::Continue(1));
150        assert_ne!(ControlFlow::Continue(1), ControlFlow::Return(1));
151    }
152
153    #[test]
154    fn control_flow_clone() {
155        let cf = ControlFlow::Return(5);
156        let cloned = cf;
157        assert_eq!(cf, cloned);
158    }
159
160    // --- ExecResult::ok ---
161
162    #[test]
163    fn exec_result_ok_sets_stdout() {
164        let r = ExecResult::ok("hello");
165        assert_eq!(r.stdout, "hello");
166        assert_eq!(r.stderr, "");
167        assert_eq!(r.exit_code, 0);
168        assert_eq!(r.control_flow, ControlFlow::None);
169        assert!(!r.stdout_truncated);
170        assert!(!r.stderr_truncated);
171    }
172
173    #[test]
174    fn exec_result_ok_empty_string() {
175        let r = ExecResult::ok("");
176        assert_eq!(r.stdout, "");
177        assert!(r.is_success());
178    }
179
180    #[test]
181    fn exec_result_ok_accepts_string() {
182        let s = String::from("owned");
183        let r = ExecResult::ok(s);
184        assert_eq!(r.stdout, "owned");
185    }
186
187    // --- ExecResult::err ---
188
189    #[test]
190    fn exec_result_err_sets_stderr_and_code() {
191        let r = ExecResult::err("bad command", 127);
192        assert_eq!(r.stdout, "");
193        assert_eq!(r.stderr, "bad command");
194        assert_eq!(r.exit_code, 127);
195        assert_eq!(r.control_flow, ControlFlow::None);
196    }
197
198    #[test]
199    fn exec_result_err_is_not_success() {
200        let r = ExecResult::err("fail", 1);
201        assert!(!r.is_success());
202    }
203
204    #[test]
205    fn exec_result_err_with_code_zero_is_success() {
206        // Edge case: err constructor with exit_code 0
207        let r = ExecResult::err("warning", 0);
208        assert!(r.is_success());
209    }
210
211    // --- ExecResult::with_code ---
212
213    #[test]
214    fn exec_result_with_code_sets_stdout_and_code() {
215        let r = ExecResult::with_code("partial", 2);
216        assert_eq!(r.stdout, "partial");
217        assert_eq!(r.stderr, "");
218        assert_eq!(r.exit_code, 2);
219        assert_eq!(r.control_flow, ControlFlow::None);
220    }
221
222    #[test]
223    fn exec_result_with_code_zero() {
224        let r = ExecResult::with_code("ok", 0);
225        assert!(r.is_success());
226    }
227
228    #[test]
229    fn exec_result_with_code_negative() {
230        let r = ExecResult::with_code("", -1);
231        assert!(!r.is_success());
232        assert_eq!(r.exit_code, -1);
233    }
234
235    // --- ExecResult::with_control_flow ---
236
237    #[test]
238    fn exec_result_with_control_flow_break() {
239        let r = ExecResult::with_control_flow(ControlFlow::Break(1));
240        assert_eq!(r.stdout, "");
241        assert_eq!(r.stderr, "");
242        assert_eq!(r.exit_code, 0);
243        assert_eq!(r.control_flow, ControlFlow::Break(1));
244    }
245
246    #[test]
247    fn exec_result_with_control_flow_continue() {
248        let r = ExecResult::with_control_flow(ControlFlow::Continue(1));
249        assert_eq!(r.control_flow, ControlFlow::Continue(1));
250    }
251
252    #[test]
253    fn exec_result_with_control_flow_return() {
254        let r = ExecResult::with_control_flow(ControlFlow::Return(0));
255        assert_eq!(r.control_flow, ControlFlow::Return(0));
256    }
257
258    #[test]
259    fn exec_result_with_control_flow_none() {
260        let r = ExecResult::with_control_flow(ControlFlow::None);
261        assert_eq!(r.control_flow, ControlFlow::None);
262        assert!(r.is_success());
263    }
264
265    // --- ExecResult::is_success ---
266
267    #[test]
268    fn exec_result_is_success_true_for_zero() {
269        let r = ExecResult::ok("x");
270        assert!(r.is_success());
271    }
272
273    #[test]
274    fn exec_result_is_success_false_for_nonzero() {
275        let r = ExecResult::err("x", 1);
276        assert!(!r.is_success());
277        let r2 = ExecResult::with_code("", 255);
278        assert!(!r2.is_success());
279    }
280
281    // --- ExecResult::default ---
282
283    #[test]
284    fn exec_result_default() {
285        let r = ExecResult::default();
286        assert_eq!(r.stdout, "");
287        assert_eq!(r.stderr, "");
288        assert_eq!(r.exit_code, 0);
289        assert_eq!(r.control_flow, ControlFlow::None);
290        assert!(!r.stdout_truncated);
291        assert!(!r.stderr_truncated);
292        assert!(r.final_env.is_none());
293        assert!(r.is_success());
294    }
295
296    // --- Debug ---
297
298    #[test]
299    fn exec_result_debug_format() {
300        let r = ExecResult::ok("test");
301        let dbg = format!("{:?}", r);
302        assert!(dbg.contains("ExecResult"));
303        assert!(dbg.contains("test"));
304    }
305
306    #[test]
307    fn control_flow_debug_format() {
308        let cf = ControlFlow::Break(3);
309        let dbg = format!("{:?}", cf);
310        assert!(dbg.contains("Break"));
311        assert!(dbg.contains("3"));
312    }
313}