Skip to main content

zsh/
loop_port.rs

1//! Loop execution for zshrs
2//!
3//! Port from zsh/Src/loop.c (802 lines)
4//!
5//! In C, loop.c contains execfor, execwhile, execif, execcase, execselect,
6//! execrepeat, and exectry as separate functions operating on bytecode.
7//! In Rust, all of these are implemented as match arms in
8//! ShellExecutor::execute_compound() in exec.rs, operating on the typed AST
9//! (CompoundCommand::For, While, If, Case, Select, Repeat, Try).
10//!
11//! This module provides the loop state management and helper functions
12//! that support the executor's loop implementation.
13
14use std::sync::atomic::{AtomicI32, Ordering};
15
16/// Number of nested loops (from loop.c `loops`)
17static LOOP_DEPTH: AtomicI32 = AtomicI32::new(0);
18
19/// Continue flag / level (from loop.c `contflag`)
20static CONT_FLAG: AtomicI32 = AtomicI32::new(0);
21
22/// Break level (from loop.c `breaks`)
23static BREAK_LEVEL: AtomicI32 = AtomicI32::new(0);
24
25/// Loop state for the executor
26#[derive(Debug, Clone, Default)]
27pub struct LoopState {
28    /// Current nesting depth
29    pub depth: i32,
30    /// Break requested (and how many levels)
31    pub breaks: i32,
32    /// Continue requested (and how many levels)
33    pub contflag: i32,
34}
35
36impl LoopState {
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Enter a loop (from loop.c loops++)
42    pub fn enter(&mut self) {
43        self.depth += 1;
44        LOOP_DEPTH.store(self.depth, Ordering::Relaxed);
45    }
46
47    /// Exit a loop (from loop.c loops--)
48    pub fn exit(&mut self) {
49        self.depth -= 1;
50        if self.depth < 0 {
51            self.depth = 0;
52        }
53        LOOP_DEPTH.store(self.depth, Ordering::Relaxed);
54
55        // Decrement break/continue levels as we leave
56        if self.breaks > 0 {
57            self.breaks -= 1;
58        }
59        if self.contflag > 0 {
60            self.contflag -= 1;
61        }
62        BREAK_LEVEL.store(self.breaks, Ordering::Relaxed);
63        CONT_FLAG.store(self.contflag, Ordering::Relaxed);
64    }
65
66    /// Request break (from builtin break)
67    pub fn do_break(&mut self, levels: i32) {
68        self.breaks = levels.min(self.depth);
69        BREAK_LEVEL.store(self.breaks, Ordering::Relaxed);
70    }
71
72    /// Request continue (from builtin continue)
73    pub fn do_continue(&mut self, levels: i32) {
74        self.contflag = levels.min(self.depth);
75        CONT_FLAG.store(self.contflag, Ordering::Relaxed);
76    }
77
78    /// Check if break is active
79    pub fn should_break(&self) -> bool {
80        self.breaks > 0
81    }
82
83    /// Check if continue is active
84    pub fn should_continue(&self) -> bool {
85        self.contflag > 0
86    }
87
88    /// Check if we're inside any loop
89    pub fn in_loop(&self) -> bool {
90        self.depth > 0
91    }
92
93    /// Reset break/continue (after handling)
94    pub fn reset_flow(&mut self) {
95        self.contflag = 0;
96        CONT_FLAG.store(0, Ordering::Relaxed);
97    }
98
99    /// Get current nesting depth
100    pub fn current_depth(&self) -> i32 {
101        self.depth
102    }
103}
104
105/// Get global loop depth
106pub fn loop_depth() -> i32 {
107    LOOP_DEPTH.load(Ordering::Relaxed)
108}
109
110/// Get global break level
111pub fn break_level() -> i32 {
112    BREAK_LEVEL.load(Ordering::Relaxed)
113}
114
115/// Get global continue flag
116pub fn cont_flag() -> i32 {
117    CONT_FLAG.load(Ordering::Relaxed)
118}
119
120/// Select menu display (from loop.c selectlist)
121///
122/// Prints a numbered menu for `select var in words` loops.
123/// Returns the formatted menu string.
124pub fn selectlist(items: &[String], prompt: &str, columns: usize) -> String {
125    let mut output = String::new();
126    let max_width = items.iter().map(|s| s.len()).max().unwrap_or(0);
127    let item_width = max_width + 4; // number + ") " + padding
128    let cols = if columns > 0 {
129        columns
130    } else {
131        // Auto-detect columns based on terminal width
132        let term_width = crate::utils::get_term_width();
133        (term_width / item_width.max(1)).max(1)
134    };
135
136    for (i, item) in items.iter().enumerate() {
137        let num = i + 1;
138        let entry = format!("{:>2}) {:<width$}", num, item, width = max_width);
139        output.push_str(&entry);
140
141        if (i + 1) % cols == 0 || i + 1 == items.len() {
142            output.push('\n');
143        } else {
144            output.push_str("  ");
145        }
146    }
147
148    if !prompt.is_empty() {
149        output.push_str(prompt);
150    }
151
152    output
153}
154
155/// Parse select reply (from loop.c execselect)
156///
157/// Given the user's input and the item list, returns the selected item
158/// or None if the input is invalid.
159pub fn select_parse_reply(reply: &str, items: &[String]) -> Option<String> {
160    let reply = reply.trim();
161    if reply.is_empty() {
162        return None;
163    }
164
165    // Try as number
166    if let Ok(n) = reply.parse::<usize>() {
167        if n >= 1 && n <= items.len() {
168            return Some(items[n - 1].clone());
169        }
170    }
171
172    None
173}
174
175/// For loop variable iteration helpers
176pub struct ForIterator {
177    items: Vec<String>,
178    pos: usize,
179}
180
181impl ForIterator {
182    pub fn new(items: Vec<String>) -> Self {
183        ForIterator { items, pos: 0 }
184    }
185
186    pub fn from_range(start: i64, end: i64, step: i64) -> Self {
187        let mut items = Vec::new();
188        let step = if step == 0 { 1 } else { step };
189        if step > 0 {
190            let mut i = start;
191            while i <= end {
192                items.push(i.to_string());
193                i += step;
194            }
195        } else {
196            let mut i = start;
197            while i >= end {
198                items.push(i.to_string());
199                i += step;
200            }
201        }
202        ForIterator { items, pos: 0 }
203    }
204
205    pub fn len(&self) -> usize {
206        self.items.len()
207    }
208
209    pub fn is_empty(&self) -> bool {
210        self.items.is_empty()
211    }
212}
213
214impl Iterator for ForIterator {
215    type Item = String;
216
217    fn next(&mut self) -> Option<String> {
218        if self.pos < self.items.len() {
219            let item = self.items[self.pos].clone();
220            self.pos += 1;
221            Some(item)
222        } else {
223            None
224        }
225    }
226}
227
228/// C-style for loop state ((init; cond; advance))
229pub struct CForState {
230    pub init_done: bool,
231}
232
233impl CForState {
234    pub fn new() -> Self {
235        CForState { init_done: false }
236    }
237}
238
239impl Default for CForState {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245/// Try/always block state (from loop.c exectry)
246#[derive(Debug, Clone, Default)]
247pub struct TryState {
248    pub in_try: bool,
249    pub try_errflag: i32,
250    pub try_retval: i32,
251}
252
253impl TryState {
254    pub fn new() -> Self {
255        Self::default()
256    }
257
258    pub fn enter_try(&mut self) {
259        self.in_try = true;
260        self.try_errflag = 0;
261        self.try_retval = 0;
262    }
263
264    pub fn exit_try(&mut self) {
265        self.in_try = false;
266    }
267
268    pub fn set_error(&mut self, errflag: i32, retval: i32) {
269        self.try_errflag = errflag;
270        self.try_retval = retval;
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_loop_state() {
280        let mut state = LoopState::new();
281        assert!(!state.in_loop());
282
283        state.enter();
284        assert!(state.in_loop());
285        assert_eq!(state.current_depth(), 1);
286
287        state.enter();
288        assert_eq!(state.current_depth(), 2);
289
290        state.exit();
291        assert_eq!(state.current_depth(), 1);
292        assert!(state.in_loop());
293
294        state.exit();
295        assert!(!state.in_loop());
296    }
297
298    #[test]
299    fn test_break_continue() {
300        let mut state = LoopState::new();
301        state.enter();
302        state.enter();
303
304        state.do_break(1);
305        assert!(state.should_break());
306
307        state.exit();
308        assert!(!state.should_break());
309    }
310
311    #[test]
312    fn test_for_iterator() {
313        let iter = ForIterator::new(vec!["a".into(), "b".into(), "c".into()]);
314        let items: Vec<String> = iter.collect();
315        assert_eq!(items, vec!["a", "b", "c"]);
316    }
317
318    #[test]
319    fn test_for_range() {
320        let iter = ForIterator::from_range(1, 5, 1);
321        let items: Vec<String> = iter.collect();
322        assert_eq!(items, vec!["1", "2", "3", "4", "5"]);
323    }
324
325    #[test]
326    fn test_select_parse() {
327        let items = vec!["apple".into(), "banana".into(), "cherry".into()];
328        assert_eq!(select_parse_reply("1", &items), Some("apple".to_string()));
329        assert_eq!(select_parse_reply("3", &items), Some("cherry".to_string()));
330        assert_eq!(select_parse_reply("0", &items), None);
331        assert_eq!(select_parse_reply("4", &items), None);
332        assert_eq!(select_parse_reply("", &items), None);
333    }
334
335    #[test]
336    fn test_selectlist() {
337        let items = vec!["one".into(), "two".into(), "three".into()];
338        let output = selectlist(&items, "? ", 0);
339        assert!(output.contains("1)"));
340        assert!(output.contains("one"));
341        assert!(output.contains("three"));
342    }
343
344    #[test]
345    fn test_try_state() {
346        let mut state = TryState::new();
347        assert!(!state.in_try);
348
349        state.enter_try();
350        assert!(state.in_try);
351
352        state.set_error(1, 42);
353        assert_eq!(state.try_errflag, 1);
354        assert_eq!(state.try_retval, 42);
355
356        state.exit_try();
357        assert!(!state.in_try);
358    }
359}