Skip to main content

zsh/ported/
loop.rs

1//! Loop execution for zshrs
2//!
3//! Port from zsh/Src/loop.c (802 lines)
4//!
5//! # of nested loops we are in                                              // c:33
6//! # of continue levels                                                     // c:38
7//! # of break levels                                                        // c:43
8//!
9//! In C, loop.c contains execfor, execwhile, execif, execcase, execselect,
10//! execrepeat, and exectry as separate functions operating on bytecode.
11//! In Rust, all of these are implemented as match arms in
12//! ShellExecutor::execute_compound() in vm_helper, operating on the typed AST
13//! (CompoundCommand::For, While, If, Case, Select, Repeat, Try).
14//!
15//! This module provides the loop state management and helper functions
16//! that support the executor's loop implementation.
17
18use crate::ported::utils::{adjustcolumns, adjustlines};
19use std::sync::atomic::Ordering;
20
21// ── Imports for the `Src/loop.c` wordcode-VM entries below ───────
22// These were `use` items of `src/ported/exec.rs` while the seven
23// exec* bodies lived there; they move with the bodies.
24use crate::ported::builtin::{BREAKS, CONTFLAG, LASTVAL, LOOPS, RETFLAG};
25use crate::ported::exec::{execlist, execsubst, noerrexit, simple_pline, this_noerrexit};
26use crate::ported::lex::untokenize;
27use crate::ported::math::{matheval as wc_matheval, mathevali as wc_mathevali};
28use crate::ported::mem::{freeheap, popheap, pushheap};
29use crate::ported::params::setloopvar;
30use crate::ported::parse::{ecgetlist, ecgetstr, ecrawstr};
31use crate::ported::pattern::{patcompile, pattry};
32use crate::ported::prompt::{cmdpop, cmdpush};
33use crate::ported::signals::{queue_signals, unqueue_signals};
34use crate::ported::subst::singsub;
35use crate::ported::utils::{errflag, printprompt4, zerr, ERRFLAG_ERROR};
36use crate::ported::zsh_h::{
37    estate, isset, wc_code, CS_ALWAYS, CS_CASE, CS_CURSH, CS_ELIF, CS_ELIFTHEN, CS_ELSE, CS_FOR,
38    CS_IF, CS_IFTHEN, CS_REPEAT, CS_UNTIL, CS_WHILE, EC_DUP, EC_DUPTOK, EC_NODUP, ERRFLAG_INT,
39    NOERREXIT_EXIT, NOERREXIT_RETURN, PAT_STATIC, WC_CASE, WC_CASE_AND, WC_CASE_OR, WC_CASE_SKIP,
40    WC_CASE_TESTAND, WC_CASE_TYPE, WC_END, WC_FOR_COND, WC_FOR_LIST, WC_FOR_SKIP, WC_FOR_TYPE,
41    WC_IF, WC_IF_ELSE, WC_IF_SKIP, WC_IF_TYPE, WC_REPEAT_SKIP, WC_TRY_SKIP, WC_WHILE_SKIP,
42    WC_WHILE_TYPE, WC_WHILE_UNTIL,
43};
44use crate::zsh_h::XTRACE;
45use std::io::Write;
46use std::sync::atomic::AtomicI32;
47// ===========================================================
48// C `Src/loop.c` — wordcode VM helpers for control flow.
49//
50// In C zsh, these seven functions run as part of the wordcode VM in
51// `Src/exec.c`: each consumes an `Estate` / wordcode cursor (not a
52// separate AST interpreter in the parser).
53//
54// zshrs lowers shell constructs to fusevm bytecode
55// (see `tree_walker_absent.rs` / `no_tree_walker_dispatch.rs`
56// invariant tests), so these entries exist to satisfy ABI/name
57// parity. The actual control-flow lowering happens in the
58// fusevm compiler (`crate::fusevm::compile`) where every
59// `for`/`while`/`if`/`case`/`select`/`repeat`/`try` AST node
60// becomes a fusevm `Op`.
61// ===========================================================
62
63// The seven entries below are zsh's `Src/loop.c` wordcode VM hooks.
64// zshrs lowers shell constructs to fusevm bytecode — every `for`/`while`/`if`/`case`/`select`/
65// `repeat`/`try` AST node lowers to a fusevm Op in
66// `src/extensions/compile_zsh.rs`. These entries exist purely for
67// C-name parity (drift gate enforces every Rust fn maps to a C fn).
68//
69// The 96-test architectural invariant in `tree_walker_absent.rs` +
70// `no_tree_walker_dispatch.rs` proves these are never reached in
71// production. Each body is `unreachable!()` so ANY caller fails
72// loudly rather than silently returning 0 — if a port regresses
73// the bytecode lowering, we want the test suite to crash, not pass.
74//
75// Faithful per-fn port of the C bodies is intentionally NOT done:
76// they read `Wordcode` / `Estate` cursors that zshrs doesn't model.
77// The semantic equivalent lives in:
78//   execfor    → compile_zsh.rs::compile_for
79//   execselect → compile_zsh.rs::compile_select
80//   execwhile  → compile_zsh.rs::compile_while
81//   execrepeat → compile_zsh.rs::compile_repeat
82//   execif     → compile_zsh.rs::compile_if
83//   execcase   → compile_zsh.rs::compile_case
84//   exectry    → compile_zsh.rs::compile_try
85
86// execfor is ported below in this file (Src/loop.c:50).
87
88
89// Note: dead `ForIterator` / `CForState` / `TryState` aggregates
90// removed per PORT_PLAN Phase 2. None had production callers (only
91// internal test references). The actual control flow is lowered in
92// the fusevm compiler — every `for`/`while`/`select`/`repeat`/`try`
93// AST node becomes a fusevm Op (see `src/extensions/compile_zsh.rs`).
94//
95// C source's relevant try-block file-globals (loop.c:719-727):
96//
97//     zlong try_errflag = -1;       // line 719 (TRY_BLOCK_ERROR)
98//     zlong try_interrupt = -1;     // line 727 (TRY_BLOCK_INTERRUPT)
99//     zlong try_tryflag = 0;        // line 731 (TRY_BLOCK_DEPTH)
100//
101// Exported via `IPDEF6` paramdef in `Src/params.c:364`, so they're
102// cross-compilation-unit globals → PORT_PLAN Phase 3 bucket-2
103// (Arc<RwLock>) work, not the Phase 2 bucket-1 (thread_local!) wave.
104
105/// Port of `mod_export zlong try_tryflag` from `Src/loop.c:731`.
106/// Depth-counter for active `always {}` blocks; bumped at try entry
107/// (`exectry`), decremented at exit. `dotrapargs` (`signals.c:1215`)
108/// reads it to decide whether a trap's `errflag` propagates.
109pub static try_tryflag: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0); // c:731
110
111/// Port of `mod_export zlong try_errflag` from `Src/loop.c:719`.
112/// Initialized to -1 (sentinel: "no try block has fired yet in this
113/// scope"); the `exectry` always-arm sets it to `errflag & ERRFLAG_ERROR`
114/// before running the always body, then restores. `TRY_BLOCK_ERROR`
115/// param (IPDEF6 at `Src/params.c:364`) reads this global directly.
116pub static try_errflag: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(-1); // c:719
117
118/// Port of `mod_export zlong try_interrupt` from `Src/loop.c:727`.
119/// Initialized to -1; mirrors `try_errflag` for the `ERRFLAG_INT` bit.
120/// `TRY_BLOCK_INTERRUPT` param reads this.
121pub static try_interrupt: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(-1); // c:727
122
123// execselect is ported below in this file (Src/loop.c:217).
124
125
126// Note: dead `LoopState` aggregate (and impl/tests) deleted per
127// PORT_PLAN Phase 2. It was a Rust-only invention that double-tracked
128// the same data already living in the file-statics LOOP_DEPTH /
129// CONT_FLAG / BREAK_LEVEL above (and on `ShellExecutor.breaking` /
130// `ShellExecutor.continuing` in src/vm_helper:572-573). Zero callers
131// outside its own test module.
132//
133// C source's actual loop-control file-globals at `Src/loop.c`:
134//
135//     int loops;                          // line 36
136//     mod_export int contflag;            // line 41
137//     mod_export volatile int breaks;     // line 46
138//
139// All `mod_export` (cross-compilation-unit), so they're PORT_PLAN
140// Phase 3 bucket-2 (Arc<RwLock>) work. The canonical C-named ports
141// (LOOPS / CONTFLAG / BREAKS) live in `src/ported/builtin.rs:3657-3659`
142// where the bin_break dispatcher consults them; the local
143// LOOP_DEPTH / CONT_FLAG / BREAK_LEVEL above are this file's
144// internal mirrors.
145
146// And this is used to print select lists.                                 // c:347
147/// Select-menu display.
148/// Port of `selectlist(LinkList l, size_t start)` from Src/loop.c:347 — formats the
149/// numbered menu the C source uses for `select var in words`. Picks
150/// columns automatically when `columns == 0`, mirroring the C
151/// source's terminal-width auto-detection.
152/// WARNING: param names don't match C — Rust=(items, start) vs C=(l, start)
153pub fn selectlist(items: &[&str], start: usize) -> usize {
154    // c:347
155    let mut stderr = std::io::stderr().lock();
156
157    // c:351 — zleentry(ZLE_CMD_TRASH); — flush ZLE redraw state.
158    // zshrs's ZLE entry-point dispatch is wired through the
159    // executor; the trash hook runs there, not in this body.
160
161    let ct = items.len(); // c:362 ap - arr
162    if ct == 0 {
163        // guard against empty list
164        return 0;
165    }
166    let mut longest: usize = 1; // c:350
167    for ap in items {
168        // c:354 for (ap = arr; *ap; ap++)
169        // C uses MB_METASTRWIDTH for visible width (combining-char
170        // aware). Rust port uses chars().count() — adequate for
171        // standard ASCII / non-combining content.
172        let aplen = ap.chars().count(); // c:359 unmetafy width
173        if aplen > longest {
174            // c:362
175            longest = aplen; // c:363
176        }
177    }
178    longest += 1; // c:365 +1 for ") "
179    let mut t0 = ct; // c:366
180    while t0 != 0 {
181        // c:367
182        t0 /= 10; // c:368
183        longest += 1; // c:368 (+1 per digit)
184    }
185
186    let zterm_columns = adjustcolumns(); // c:zterm_columns
187    let zterm_lines = adjustlines();
188    let mut fct: usize = (zterm_columns.saturating_sub(1)) / (longest + 3); // c:371
189    let fw: usize;
190    if fct == 0 {
191        // c:372
192        fct = 1; // c:373
193        fw = 0; // (unused when fct==1)
194    } else {
195        fw = (zterm_columns - 1) / fct; // c:375
196    }
197    let colsz = (ct + fct - 1) / fct; // c:377
198
199    // c:379 — for (t1 = start; t1 != colsz && t1 - start < zterm_lines - 2; t1++)
200    //
201    // C uses `*ap` (NUL-terminated array walk) to bound the inner loop;
202    // the Rust port lost that sentinel, so an out-of-range `start` (or
203    // a row-offset that reaches past `ct`) indexes `items[idx]` and
204    // panics. Mirror the C terminator with explicit bounds checks:
205    //   - outer: stop when `t1 >= colsz` OR `start + t1*fct >= ct`
206    //   - inner: skip the row entirely when the column-base is OOB.
207    let mut t1 = start;
208    let max_lines = zterm_lines.saturating_sub(2);
209    while t1 < colsz && (t1.saturating_sub(start)) < max_lines {
210        let mut idx = t1; // c:381 ap = arr + t1
211        if idx >= ct {
212            // Past the last item — C's `*ap` would already be NULL here.
213            // Emit the blank line C would produce and advance.
214            let _ = stderr.write_all(b"\n");
215            t1 += 1;
216            continue;
217        }
218        loop {
219            // c:383 do {
220            let entry = items[idx];
221            // c:385 — t2 = MB_METASTRWIDTH(*ap) + 2;
222            let mut t2 = entry.chars().count() + 2;
223            // c:391 — fprintf(stderr, "%d) %s", t3 = ap - arr + 1, *ap);
224            let t3 = idx + 1;
225            let _ = write!(stderr, "{}) {}", t3, entry);
226            // c:393 — while (t3) t2++, t3 /= 10;
227            let mut digits = t3;
228            while digits != 0 {
229                // c:393
230                t2 += 1;
231                digits /= 10;
232            }
233            // c:396 — for (; t2 < fw; t2++) fputc(' ', stderr);
234            while t2 < fw {
235                // c:396
236                let _ = stderr.write_all(b" ");
237                t2 += 1;
238            }
239            // c:398 — for (t0 = colsz; t0 && *ap; t0--, ap++);
240            let mut t0 = colsz;
241            while t0 != 0 && idx + 1 < ct {
242                t0 -= 1;
243                idx += 1;
244                if t0 == 0 {
245                    break;
246                }
247            }
248            if idx + 1 >= ct {
249                break;
250            } // c:401 while (*ap);
251        }
252        let _ = stderr.write_all(b"\n"); // c:401 fputc('\n', stderr);
253        t1 += 1;
254    }
255
256    let _ = stderr.flush(); // c:413 fflush(stderr);
257
258    if t1 < colsz {
259        t1
260    } else {
261        0
262    } // c:415 return
263}
264
265// execwhile / execrepeat / execif / execcase / exectry moved to
266// src/ported/exec.rs as faithful ports of Src/loop.c:413 / :499 /
267// :553 / :600 / :735. See exec.rs for the C-line-cited bodies.
268
269/// Number of nested loops.
270/// Port of the global `loops` counter from Src/loop.c — every
271/// `execfor`/`execwhile`/`execrepeat`/`execselect` entry bumps it
272/// and decrements on exit.
273static LOOP_DEPTH: AtomicI32 = AtomicI32::new(0);
274
275/// Continue flag / level.
276/// Port of the global `contflag` from Src/loop.c — set by the
277/// `continue` builtin (Src/builtin.c:bin_break) and consumed by
278/// the loop body's exit check.
279static CONT_FLAG: AtomicI32 = AtomicI32::new(0);
280
281/// Break level.
282/// Port of the global `breaks` counter from Src/loop.c — set by
283/// the `break` builtin (Src/builtin.c:bin_break) and tested by
284/// each enclosing loop on exit.
285static BREAK_LEVEL: AtomicI32 = AtomicI32::new(0);
286
287
288// ===========================================================
289// The seven `Src/loop.c` wordcode-VM entries. These bodies used to
290// live in `src/ported/exec.rs`; they are ports of `Src/loop.c`, so
291// they belong in the Rust mirror of that file. `exec.rs` calls them
292// through `crate::ported::r#loop::{...}` exactly as C's `execfuncs[]`
293// table (`Src/exec.c:275-277`) dispatches into loop.c.
294// ===========================================================
295
296// --- loop.c entries ---------------------------------------------------
297
298/// Port of `execfor(Estate state, int do_exec)` from `Src/loop.c:50-202`.
299/// `for var in args; do body; done` and the C-style `for ((init;cond;adv))`
300/// variant. WC_FOR_TYPE distinguishes PPARAM (use $@) / LIST (explicit
301/// words) / COND (C-style).
302pub fn execfor(state: &mut estate, do_exec: i32) -> i32 {
303    use crate::ported::zsh_h::Z_END;
304    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:54
305    let iscond = WC_FOR_TYPE(code) == WC_FOR_COND; // c:55
306    let mut last_iter = false; // c:57 — `int last = 0;`
307    let mut val: i64 = 0; // c:59
308    let mut vars: Vec<String> = Vec::new();
309    let mut args: Vec<String> = Vec::new();
310    let mut cond_expr: String = String::new();
311    let mut advance_expr: String = String::new();
312    let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:62-63
313    let end_pc = state.pc + WC_FOR_SKIP(code) as usize; // c:65
314    let mut ctok = 0i32;
315    let mut atok = 0i32;
316    if iscond {
317        // c:68-82 — C-style for: init expr at top, then cond/advance.
318        let init = ecgetstr(state, EC_NODUP, None); // c:68
319        let init_sub = singsub(&init); // c:69
320        if isset(XTRACE) {
321            // c:70-75
322            let init_show = untokenize(&init_sub);
323            printprompt4();
324            eprintln!("{}", init_show);
325        }
326        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
327            let _ = wc_matheval(&init_sub); // c:77 — `matheval(str);`
328        }
329        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
330            // c:79-82
331            state.pc = end_pc;
332            simple_pline.store(old_simple_pline, Ordering::Relaxed);
333            return 1;
334        }
335        cond_expr = ecgetstr(state, EC_NODUP, Some(&mut ctok)); // c:83
336        advance_expr = ecgetstr(state, EC_NODUP, Some(&mut atok)); // c:84
337    } else {
338        // c:86 — `vars = ecgetlist(state, *state->pc++, EC_NODUP, NULL);`
339        let count = state.prog.prog[state.pc] as usize;
340        state.pc += 1;
341        vars = ecgetlist(state, count, EC_NODUP, None);
342        if WC_FOR_TYPE(code) == WC_FOR_LIST {
343            // c:88-100 — explicit `for var in words`
344            let mut htok = 0i32;
345            let arg_count = state.prog.prog[state.pc] as usize;
346            state.pc += 1;
347            args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
348            if args.is_empty() {
349                state.pc = end_pc;
350                simple_pline.store(old_simple_pline, Ordering::Relaxed);
351                return 0;
352            }
353            if htok != 0 {
354                execsubst(&mut args); // c:96
355                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
356                    state.pc = end_pc;
357                    simple_pline.store(old_simple_pline, Ordering::Relaxed);
358                    return 1;
359                }
360            }
361        } else {
362            // c:102-107 — implicit `for var` (no `in` clause) uses
363            // the positional params $@ from PPARAMS (params.rs Mutex).
364            args = crate::ported::builtin::PPARAMS
365                .lock()
366                .map(|p| p.clone())
367                .unwrap_or_default();
368        }
369    }
370    // c:111-112 — empty args ⇒ lastval = 0.
371    if !iscond && args.is_empty() {
372        LASTVAL.store(0, Ordering::Relaxed);
373    }
374    LOOPS.fetch_add(1, Ordering::SeqCst); // c:114 — `loops++;`
375    pushheap(); // c:115
376    cmdpush(CS_FOR as u8); // c:116
377    let loop_pc = state.pc; // c:117
378    let mut args_iter = args.into_iter();
379    while !last_iter {
380        if iscond {
381            // c:119-138 — eval cond expression.
382            let mut cs = cond_expr.clone();
383            if ctok != 0 {
384                cs = singsub(&cs);
385            }
386            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
387                let trimmed = cs.trim_start();
388                if !trimmed.is_empty() {
389                    if isset(XTRACE) {
390                        printprompt4();
391                        eprintln!("{}", trimmed);
392                    }
393                    val = wc_mathevali(trimmed).unwrap_or(0);
394                } else {
395                    val = 1;
396                }
397            }
398            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
399                if BREAKS.load(Ordering::SeqCst) > 0 {
400                    BREAKS.fetch_sub(1, Ordering::SeqCst);
401                }
402                LASTVAL.store(1, Ordering::Relaxed);
403                break;
404            }
405            if val == 0 {
406                break;
407            }
408        } else {
409            // c:140-162 — for var binding from args.
410            let mut count = 0;
411            for name in &vars {
412                let value = match args_iter.next() {
413                    Some(v) => v,
414                    None => {
415                        if count != 0 {
416                            last_iter = true;
417                            String::new()
418                        } else {
419                            break;
420                        }
421                    }
422                };
423                if isset(XTRACE) {
424                    printprompt4();
425                    eprintln!("{}={}", name, value);
426                }
427                setloopvar(name, &value);
428                count += 1;
429            }
430            if count == 0 {
431                break;
432            }
433        }
434        state.pc = loop_pc; // c:163
435        let _do_exec_now = do_exec != 0 && !args_iter.clone().any(|_| true); // c:164 — `do_exec && args && empty(args)`
436        let _ = execlist(state, 1, if _do_exec_now { 1 } else { 0 });
437        // c:166-169 — breaks/continue handling.
438        if BREAKS.load(Ordering::SeqCst) > 0 {
439            let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
440            if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
441                break;
442            }
443            CONTFLAG.store(0, Ordering::SeqCst);
444        }
445        if RETFLAG.load(Ordering::SeqCst) != 0 {
446            break;
447        }
448        // c:170-178 — C-style advance step.
449        if iscond && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
450            let mut adv = advance_expr.clone();
451            if atok != 0 {
452                adv = singsub(&adv);
453            }
454            if isset(XTRACE) {
455                printprompt4();
456                eprintln!("{}", adv);
457            }
458            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
459                let _ = wc_matheval(&adv);
460            }
461        }
462        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
463            if BREAKS.load(Ordering::SeqCst) > 0 {
464                BREAKS.fetch_sub(1, Ordering::SeqCst);
465            }
466            LASTVAL.store(1, Ordering::Relaxed);
467            break;
468        }
469        freeheap(); // c:184
470    }
471    popheap(); // c:186
472    cmdpop(); // c:187
473    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:188
474    simple_pline.store(old_simple_pline, Ordering::Relaxed);
475    state.pc = end_pc;
476    this_noerrexit.store(1, Ordering::Relaxed);
477    let _ = Z_END;
478    LASTVAL.load(Ordering::Relaxed)
479}
480
481/// Port of `execselect(Estate state, UNUSED(int do_exec))` from
482/// `Src/loop.c:217-410`. `select var in words; do body; done` REPL.
483pub fn execselect(state: &mut estate, _do_exec: i32) -> i32 {
484    // The full select body manages a REPL prompt, terminal columns,
485    // selectlist redraw, etc. The `selectlist` helper at loop.rs:130
486    // already ports c:347 (menu display). Structural execselect:
487    // c:225-410 — read vars + words like execfor, then loop on stdin
488    // input prompting via PROMPT3, set var=word, run body.
489    let code = state.prog.prog[state.pc.wrapping_sub(1)];
490    let end_pc = state.pc + WC_FOR_SKIP(code) as usize;
491    // c:228-237 — read var name + words. Skip body and use existing
492    // bridge handler at BUILTIN_RUN_SELECT for actual REPL until full
493    // wordcode driver lands.
494    state.pc = end_pc;
495    this_noerrexit.store(1, Ordering::Relaxed);
496    LASTVAL.load(Ordering::Relaxed)
497}
498
499/// Port of `execwhile(Estate state, UNUSED(int do_exec))` from
500/// `Src/loop.c:413-498`. `while/until cond; do body; done`.
501pub fn execwhile(state: &mut estate, _do_exec: i32) -> i32 {
502    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:417
503    let isuntil = WC_WHILE_TYPE(code) == WC_WHILE_UNTIL; // c:419
504    let end_pc = state.pc + WC_WHILE_SKIP(code) as usize; // c:422
505    let olderrexit = noerrexit.load(Ordering::Relaxed); // c:423
506    let mut oldval: i32 = 0; // c:424
507    pushheap(); // c:425
508    cmdpush(if isuntil {
509        CS_UNTIL as u8
510    } else {
511        CS_WHILE as u8
512    }); // c:426
513    LOOPS.fetch_add(1, Ordering::SeqCst); // c:427
514    let loop_pc = state.pc; // c:428
515    let old_simple_pline = simple_pline.load(Ordering::Relaxed); // c:419
516                                                                 // c:430-456 — empty-loop fast path. If loop body is two WC_ENDs,
517                                                                 // sit in a tight signal-wait loop until ^C breaks us.
518    if state.prog.prog.get(loop_pc) == Some(&WC_END)
519        && state.prog.prog.get(loop_pc + 1) == Some(&WC_END)
520    {
521        simple_pline.store(1, Ordering::Relaxed);
522        // c:438-439 — spin until breaks.
523        while BREAKS.load(Ordering::SeqCst) == 0 {
524            std::thread::yield_now();
525        }
526        BREAKS.fetch_sub(1, Ordering::SeqCst);
527        simple_pline.store(old_simple_pline, Ordering::Relaxed);
528    } else {
529        // c:441-485 — normal loop.
530        loop {
531            state.pc = loop_pc; // c:442
532            noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:443
533            simple_pline.store(1, Ordering::Relaxed); // c:446
534            let _ = execlist(state, 1, 0); // c:448 — exec cond.
535            simple_pline.store(old_simple_pline, Ordering::Relaxed);
536            noerrexit.store(olderrexit, Ordering::Relaxed); // c:451
537            let cond_status = LASTVAL.load(Ordering::Relaxed); // c:452
538                                                               // c:453-460 — `if (!((lastval == 0) ^ isuntil)) break;`
539            let cond_passed = (cond_status == 0) ^ isuntil;
540            if !cond_passed {
541                if BREAKS.load(Ordering::SeqCst) > 0 {
542                    BREAKS.fetch_sub(1, Ordering::SeqCst);
543                }
544                if RETFLAG.load(Ordering::SeqCst) == 0 {
545                    LASTVAL.store(oldval, Ordering::Relaxed);
546                }
547                break;
548            }
549            if RETFLAG.load(Ordering::SeqCst) != 0 {
550                // c:461
551                if BREAKS.load(Ordering::SeqCst) > 0 {
552                    BREAKS.fetch_sub(1, Ordering::SeqCst);
553                }
554                break;
555            }
556            simple_pline.store(1, Ordering::Relaxed); // c:468
557            let _ = execlist(state, 1, 0); // c:470 — exec body.
558            simple_pline.store(old_simple_pline, Ordering::Relaxed);
559            // c:472-477 — breaks/continue handling.
560            if BREAKS.load(Ordering::SeqCst) > 0 {
561                let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
562                if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
563                    break;
564                }
565                CONTFLAG.store(0, Ordering::SeqCst);
566            }
567            // c:478-481 — errflag bail.
568            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
569                LASTVAL.store(1, Ordering::Relaxed);
570                break;
571            }
572            // c:482-483 — retflag bail.
573            if RETFLAG.load(Ordering::SeqCst) != 0 {
574                break;
575            }
576            freeheap(); // c:484
577            oldval = LASTVAL.load(Ordering::Relaxed); // c:485
578        }
579    }
580    cmdpop(); // c:489
581    popheap(); // c:490
582    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:491
583    state.pc = end_pc; // c:492
584    this_noerrexit.store(1, Ordering::Relaxed); // c:493
585    LASTVAL.load(Ordering::Relaxed)
586}
587
588/// Port of `execrepeat(Estate state, UNUSED(int do_exec))` from
589/// `Src/loop.c:499-551`. `repeat N; do body; done`.
590pub fn execrepeat(state: &mut estate, _do_exec: i32) -> i32 {
591    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:503
592    let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:507
593    let end_pc = state.pc + WC_REPEAT_SKIP(code) as usize; // c:510
594    let mut htok = 0i32;
595    let mut tmp = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:512
596    if htok != 0 {
597        tmp = singsub(&tmp); // c:514
598        tmp = untokenize(&tmp); // c:515
599    }
600    let count = wc_mathevali(&tmp).unwrap_or(0); // c:517
601    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
602        simple_pline.store(old_simple_pline, Ordering::Relaxed);
603        return 1;
604    }
605    LASTVAL.store(0, Ordering::Relaxed); // c:520
606    pushheap(); // c:521
607    cmdpush(CS_REPEAT as u8); // c:522
608    LOOPS.fetch_add(1, Ordering::SeqCst); // c:523
609    let loop_pc = state.pc; // c:524
610    let mut remaining = count;
611    while remaining > 0 {
612        // c:525
613        remaining -= 1;
614        state.pc = loop_pc;
615        let _ = execlist(state, 1, 0); // c:527
616        freeheap(); // c:528
617                    // c:529-534 — breaks/continue handling.
618        if BREAKS.load(Ordering::SeqCst) > 0 {
619            let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
620            if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
621                break;
622            }
623            CONTFLAG.store(0, Ordering::SeqCst);
624        }
625        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
626            // c:536-538
627            LASTVAL.store(1, Ordering::Relaxed);
628            break;
629        }
630        if RETFLAG.load(Ordering::SeqCst) != 0 {
631            // c:540
632            break;
633        }
634    }
635    cmdpop(); // c:544
636    popheap(); // c:545
637    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:546
638    simple_pline.store(old_simple_pline, Ordering::Relaxed);
639    state.pc = end_pc; // c:548
640    this_noerrexit.store(1, Ordering::Relaxed); // c:549
641    LASTVAL.load(Ordering::Relaxed)
642}
643
644/// Port of `execif(Estate state, int do_exec)` from `Src/loop.c:553-598`.
645/// `if cond; then body; elif ...; else ...; fi`.
646pub fn execif(state: &mut estate, do_exec: i32) -> i32 {
647    let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:558
648    let olderrexit = noerrexit.load(Ordering::Relaxed); // c:559
649    let end_pc = state.pc + WC_IF_SKIP(code0) as usize; // c:560
650    noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:562
651    let mut s = 0i32; // c:557 — `s = 0`
652    let mut run = 0i32; // c:557 — `run = 0`
653    while state.pc < end_pc {
654        // c:563
655        let code = state.prog.prog[state.pc];
656        state.pc += 1;
657        // c:565-571 — non-IF, or IF_ELSE: break out.
658        if wc_code(code) != WC_IF || WC_IF_TYPE(code) == WC_IF_ELSE {
659            run = if wc_code(code) == WC_IF && WC_IF_TYPE(code) == WC_IF_ELSE {
660                2
661            } else {
662                1
663            };
664            if run == 1 {
665                state.pc -= 1; // back up onto the body header
666            }
667            break;
668        }
669        let next_pc = state.pc + WC_IF_SKIP(code) as usize; // c:572
670        cmdpush(if s != 0 { CS_ELIF as u8 } else { CS_IF as u8 }); // c:573
671        let _ = execlist(state, 1, 0); // c:574
672        cmdpop(); // c:575
673                  // c:576-579 — selected branch: lastval == 0.
674        if LASTVAL.load(Ordering::Relaxed) == 0 {
675            run = 1;
676            break;
677        }
678        if RETFLAG.load(Ordering::SeqCst) != 0 {
679            // c:580
680            break;
681        }
682        s = 1;
683        state.pc = next_pc;
684    }
685    noerrexit.store(olderrexit, Ordering::Relaxed); // c:584
686                                                    // c:585-591 — run selected branch.
687    if run != 0 {
688        cmdpush(if run == 2 {
689            CS_ELSE as u8
690        } else if s != 0 {
691            CS_ELIFTHEN as u8
692        } else {
693            CS_IFTHEN as u8
694        });
695        let _ = execlist(state, 1, do_exec);
696        cmdpop();
697    } else if RETFLAG.load(Ordering::SeqCst) == 0
698        && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
699    {
700        LASTVAL.store(0, Ordering::Relaxed); // c:592
701    }
702    state.pc = end_pc; // c:594
703    this_noerrexit.store(1, Ordering::Relaxed); // c:595
704    LASTVAL.load(Ordering::Relaxed)
705}
706
707/// Port of `execcase(Estate state, int do_exec)` from `Src/loop.c:600-733`.
708/// `case word in pat) body ;; ... esac` with `;;`/`;&`/`;|` separators.
709pub fn execcase(state: &mut estate, do_exec: i32) -> i32 {
710    let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:603
711    let end_pc = state.pc + WC_CASE_SKIP(code0) as usize; // c:607
712                                                          // c:609-611 — read & expand the case-word.
713    let raw_word = ecgetstr(state, EC_DUP, None);
714    let word_sub = singsub(&raw_word);
715    let word = untokenize(&word_sub);
716    let mut anypatok = false; // c:613
717    cmdpush(CS_CASE as u8); // c:615
718    let mut code = 0u32;
719    while state.pc < end_pc {
720        // c:616
721        code = state.prog.prog[state.pc];
722        state.pc += 1;
723        if wc_code(code) != WC_CASE {
724            break;
725        }
726        let next_pc = state.pc + WC_CASE_SKIP(code) as usize; // c:621
727        let nalts = state.prog.prog[state.pc] as i32; // c:622
728        state.pc += 1;
729        let mut patok = false;
730        let mut nalts_remaining = nalts;
731        while !patok && nalts_remaining > 0 {
732            // c:629-672 — try each alternative pattern.
733            // c:631-633 — `npat = state->pc[1]; spprog = state->prog->pats + npat;`
734            // zshrs's pat-compile-on-demand path: extract raw pat text + try patcompile/pattry.
735            queue_signals(); // c:636
736            let mut htok = 0i32;
737            let pat_raw = ecrawstr(&state.prog, state.pc, Some(&mut htok));
738            let pat = if htok != 0 {
739                singsub(&pat_raw)
740            } else {
741                pat_raw
742            };
743            if let Some(pprog) = patcompile(
744                &{
745                    let mut __pat_tok = (&pat).to_string();
746                    crate::ported::glob::tokenize(&mut __pat_tok);
747                    __pat_tok
748                },
749                PAT_STATIC,
750                None,
751            ) {
752                // c:660 — `if (pprog && pattry(pprog, word)) patok = anypatok = 1;`
753                if pattry(&pprog, &word) {
754                    patok = true;
755                    anypatok = true;
756                }
757            } else {
758                zerr(&format!("bad pattern: {}", pat)); // c:657
759            }
760            state.pc += 2; // c:664 — `state->pc += 2;`
761            nalts_remaining -= 1;
762            unqueue_signals(); // c:666
763        }
764        state.pc += (2 * nalts_remaining) as usize; // c:668
765        if patok {
766            // c:672-684 — run selected arm body.
767            let _ = execlist(
768                state,
769                1,
770                ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec,
771            );
772            // c:675-682 — chain into ;& and ;| siblings.
773            while RETFLAG.load(Ordering::SeqCst) == 0
774                && wc_code(code) == WC_CASE
775                && WC_CASE_TYPE(code) == WC_CASE_AND
776                && state.pc < end_pc
777            {
778                state.pc = next_pc;
779                code = state.prog.prog[state.pc];
780                state.pc += 1;
781                let inner_next = state.pc + WC_CASE_SKIP(code) as usize;
782                let inner_nalts = state.prog.prog[state.pc] as usize;
783                state.pc += 1 + 2 * inner_nalts;
784                let _ = execlist(
785                    state,
786                    1,
787                    ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec,
788                );
789                let _ = inner_next;
790            }
791            if WC_CASE_TYPE(code) != WC_CASE_TESTAND {
792                break;
793            }
794        }
795        state.pc = next_pc; // c:687
796    }
797    cmdpop(); // c:691
798    state.pc = end_pc; // c:693
799    if !anypatok {
800        // c:695-696
801        LASTVAL.store(0, Ordering::Relaxed);
802    }
803    this_noerrexit.store(1, Ordering::Relaxed); // c:697
804    LASTVAL.load(Ordering::Relaxed)
805}
806
807/// Port of `exectry(Estate state, int do_exec)` from `Src/loop.c:735-798`.
808/// `{ try } always { finally }`: capture errflag/retflag/breaks/contflag
809/// from the try-clause, reset them around the always-clause, then
810/// restore if always-clause didn't override.
811pub fn exectry(state: &mut estate, _do_exec: i32) -> i32 {
812    let header = state.prog.prog[state.pc.wrapping_sub(1)]; // c:741
813    let end_pc = state.pc + WC_TRY_SKIP(header) as usize; // c:742
814    let try_inner = state.prog.prog[state.pc]; // c:743
815    let always_pc = state.pc + 1 + WC_TRY_SKIP(try_inner) as usize; // c:743
816    state.pc += 1; // c:744
817    pushheap(); // c:745
818    cmdpush(CS_CURSH as u8); // c:746
819    try_tryflag.fetch_add(1, Ordering::SeqCst); // c:749
820    let _ = execlist(state, 1, 0); // c:750
821    try_tryflag.fetch_sub(1, Ordering::SeqCst); // c:751
822    let try_status = LASTVAL.load(Ordering::Relaxed);
823    let endval = if try_status != 0 {
824        // c:754
825        try_status
826    } else {
827        (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) as i32
828    };
829    freeheap(); // c:756
830    cmdpop(); // c:758
831    cmdpush(CS_ALWAYS as u8); // c:759
832                              // c:762-763 — save try_errflag / try_interrupt.
833    let saved_err = errflag.load(Ordering::Relaxed);
834    let save_try_err = (saved_err & ERRFLAG_ERROR) != 0;
835    let save_try_int = (saved_err & ERRFLAG_INT) != 0;
836    // c:Src/loop.c:763-766 — save the canonical globals AND update
837    // them to reflect the just-finished try body's exit state before
838    // running the always-arm. `$TRY_BLOCK_ERROR` / `$TRY_BLOCK_INTERRUPT`
839    // read these globals directly (lookup_special_var arms), so the
840    // always body must see "errflag at try-end" not "-1 sentinel".
841    let saved_try_errflag = crate::ported::r#loop::try_errflag.load(Ordering::Relaxed);
842    let saved_try_interrupt = crate::ported::r#loop::try_interrupt.load(Ordering::Relaxed);
843    crate::ported::r#loop::try_errflag.store((saved_err & ERRFLAG_ERROR) as i64, Ordering::Relaxed); // c:765
844    crate::ported::r#loop::try_interrupt.store(
845        if (saved_err & ERRFLAG_INT) != 0 { 1 } else { 0 },
846        Ordering::Relaxed,
847    ); // c:766
848       // c:768 — `errflag = 0;` (clear both bits).
849    errflag.fetch_and(!(ERRFLAG_ERROR | ERRFLAG_INT), Ordering::Relaxed);
850    // c:769-774 — save retflag/breaks/contflag.
851    let save_retflag = RETFLAG.swap(0, Ordering::SeqCst);
852    let save_breaks = BREAKS.swap(0, Ordering::SeqCst);
853    let save_contflag = CONTFLAG.swap(0, Ordering::SeqCst);
854    state.pc = always_pc; // c:776
855    let _ = execlist(state, 1, 0); // c:777
856                                   // c:779-786 — restore errflag bits.
857    if save_try_err {
858        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
859    } else {
860        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
861    }
862    if save_try_int {
863        errflag.fetch_or(ERRFLAG_INT, Ordering::Relaxed);
864    } else {
865        errflag.fetch_and(!ERRFLAG_INT, Ordering::Relaxed);
866    }
867    // c:Src/loop.c:787-788 — restore the canonical globals.
868    crate::ported::r#loop::try_errflag.store(saved_try_errflag, Ordering::Relaxed);
869    crate::ported::r#loop::try_interrupt.store(saved_try_interrupt, Ordering::Relaxed);
870    // c:789-794 — re-arm retflag/breaks/contflag only if always didn't override.
871    if RETFLAG.load(Ordering::SeqCst) == 0 {
872        RETFLAG.store(save_retflag, Ordering::SeqCst);
873    }
874    if BREAKS.load(Ordering::SeqCst) == 0 {
875        BREAKS.store(save_breaks, Ordering::SeqCst);
876    }
877    if CONTFLAG.load(Ordering::SeqCst) == 0 {
878        CONTFLAG.store(save_contflag, Ordering::SeqCst);
879    }
880    cmdpop(); // c:796
881    popheap(); // c:797
882    state.pc = end_pc; // c:798
883    this_noerrexit.store(1, Ordering::Relaxed); // c:799
884    endval
885}
886
887#[cfg(test)]
888mod tests {
889    use super::*;
890
891    #[test]
892    fn test_selectlist_returns_zero_when_full_fits() {
893        let _g = crate::test_util::global_state_lock();
894        let items = ["one", "two", "three"];
895        let r = selectlist(&items, 0);
896        assert!(r < items.len() || r == 0);
897    }
898
899    // The 7 *_panics_with_tree_walker_disabled tests were deleted
900    // when the underlying `unreachable!()` stubs were replaced with
901    // faithful ports in src/ported/exec.rs. The architectural pin
902    // remains in tests/tree_walker_absent.rs (which asserts the
903    // AST-side `execute_simple`/`execute_pipeline`/etc. tree walker
904    // is gone — a different concept from the wordcode-VM dispatch
905    // chain ported in exec.rs).
906
907    /// Empty list to selectlist: nothing to draw, returns 0.
908    #[test]
909    fn selectlist_empty_returns_zero() {
910        let _g = crate::test_util::global_state_lock();
911        assert_eq!(selectlist(&[], 0), 0);
912    }
913
914    // ═══════════════════════════════════════════════════════════════════
915    // Additional C-parity tests for Src/loop.c selectlist.
916    // ═══════════════════════════════════════════════════════════════════
917
918    /// c:347 — `selectlist` with start=0 doesn't panic and returns
919    /// a valid count.
920    #[test]
921    fn selectlist_start_zero_returns_valid_count() {
922        let _g = crate::test_util::global_state_lock();
923        let items = ["a", "b", "c"];
924        let r = selectlist(&items, 0);
925        // Return value is "next start" for paginated display.
926        // Must be reasonable: 0 (full draw fit) or >0 (paginated).
927        assert!(r <= items.len());
928    }
929
930    /// c:347 — single-item list always fits in one column.
931    #[test]
932    fn selectlist_single_item_no_panic() {
933        let _g = crate::test_util::global_state_lock();
934        let r = selectlist(&["only"], 0);
935        let _ = r;
936    }
937
938    /// c:347 — list with empty string entries doesn't panic.
939    #[test]
940    fn selectlist_empty_string_entries_no_panic() {
941        let _g = crate::test_util::global_state_lock();
942        let items = ["", "", ""];
943        let _ = selectlist(&items, 0);
944    }
945
946    /// c:347 — long items (longer than terminal width) handled safely.
947    #[test]
948    fn selectlist_long_items_no_panic() {
949        let _g = crate::test_util::global_state_lock();
950        let long = "x".repeat(500);
951        let items = [long.as_str(), "short"];
952        let _ = selectlist(&items, 0);
953    }
954
955    /// c:347 — multibyte content (CJK) doesn't panic.
956    #[test]
957    fn selectlist_multibyte_entries_no_panic() {
958        let _g = crate::test_util::global_state_lock();
959        let items = ["日本", "中文", "한국어"];
960        let _ = selectlist(&items, 0);
961    }
962
963    /// c:347 — many small items doesn't panic (stress: 100 entries).
964    #[test]
965    fn selectlist_many_items_no_panic() {
966        let _g = crate::test_util::global_state_lock();
967        let items: Vec<String> = (0..100).map(|i| format!("item{}", i)).collect();
968        let refs: Vec<&str> = items.iter().map(|s| s.as_str()).collect();
969        let _ = selectlist(&refs, 0);
970    }
971
972    /// c:347 — start > 0 with valid index PANICS in zshrs port
973    /// ("index out of bounds: the len is 5 but the index is 5").
974    /// C handles paginated start correctly via the c:379 column loop
975    /// bound `t1 - start < zterm_lines - 2`. Likely off-by-one in
976    /// the Rust port's index walk past last column.
977    #[test]
978    fn selectlist_start_in_range_no_panic() {
979        let _g = crate::test_util::global_state_lock();
980        let items = ["a", "b", "c", "d", "e"];
981        let _ = selectlist(&items, 2);
982    }
983
984    /// c:347 — `selectlist` is deterministic for a given (items, start).
985    #[test]
986    fn selectlist_is_deterministic() {
987        let _g = crate::test_util::global_state_lock();
988        let items = ["a", "b", "c"];
989        let first = selectlist(&items, 0);
990        for _ in 0..5 {
991            assert_eq!(selectlist(&items, 0), first);
992        }
993    }
994
995    // ═══════════════════════════════════════════════════════════════════
996    // Additional C-parity tests for Src/loop.c
997    // c:347 selectlist + statics (LOOP_DEPTH/CONT_FLAG/BREAK_LEVEL/try_tryflag)
998    // ═══════════════════════════════════════════════════════════════════
999
1000    /// c:347 — `selectlist` return value ≤ items.len() (valid next-start).
1001    #[test]
1002    fn selectlist_return_bounded_by_items_len() {
1003        let _g = crate::test_util::global_state_lock();
1004        for sz in [1usize, 3, 5, 20] {
1005            let items: Vec<String> = (0..sz).map(|i| format!("i{}", i)).collect();
1006            let refs: Vec<&str> = items.iter().map(|s| s.as_str()).collect();
1007            let r = selectlist(&refs, 0);
1008            assert!(r <= sz, "selectlist({} items, 0) = {} > {}", sz, r, sz);
1009        }
1010    }
1011
1012    /// c:347 — `selectlist` returns usize (compile-time type pin).
1013    #[test]
1014    fn selectlist_returns_usize_type() {
1015        let _g = crate::test_util::global_state_lock();
1016        let _: usize = selectlist(&["x"], 0);
1017    }
1018
1019    /// c:347 — `selectlist` two-item list doesn't panic.
1020    #[test]
1021    fn selectlist_two_items_no_panic() {
1022        let _g = crate::test_util::global_state_lock();
1023        let _ = selectlist(&["a", "b"], 0);
1024    }
1025
1026    /// c:347 — `selectlist` with entries containing tabs doesn't panic.
1027    #[test]
1028    fn selectlist_entries_with_tabs_no_panic() {
1029        let _g = crate::test_util::global_state_lock();
1030        let _ = selectlist(&["a\tb", "c\td"], 0);
1031    }
1032
1033    /// c:731 — `try_tryflag` is AtomicI64 (compile-time type pin).
1034    #[test]
1035    fn try_tryflag_is_atomic_i64() {
1036        use std::sync::atomic::Ordering;
1037        let v: i64 = try_tryflag.load(Ordering::SeqCst);
1038        // Just verify it loads — any i64 is a valid initial state.
1039        let _ = v;
1040    }
1041
1042    /// c:347 — `selectlist` with entries containing ANSI escape doesn't panic.
1043    #[test]
1044    fn selectlist_ansi_escape_entries_no_panic() {
1045        let _g = crate::test_util::global_state_lock();
1046        let _ = selectlist(&["\x1b[31mred\x1b[0m", "plain"], 0);
1047    }
1048
1049    /// c:347 — `selectlist` with single space entry doesn't panic.
1050    #[test]
1051    fn selectlist_single_space_entry_no_panic() {
1052        let _g = crate::test_util::global_state_lock();
1053        let _ = selectlist(&[" "], 0);
1054    }
1055
1056    // ═══════════════════════════════════════════════════════════════════
1057    // Additional C-parity tests for Src/loop.c
1058    // c:36 loops / c:41 contflag / c:46 breaks / c:347 selectlist /
1059    // c:731 try_tryflag — initial-state + idempotency pins.
1060    // ═══════════════════════════════════════════════════════════════════
1061
1062    /// c:36 — `loops` initial state is zero (`int loops;` BSS zero init).
1063    #[test]
1064    fn loop_depth_initial_state_is_zero() {
1065        use std::sync::atomic::Ordering;
1066        let v = LOOP_DEPTH.load(Ordering::SeqCst);
1067        assert!(v >= 0, "loop depth must never be negative; got {}", v);
1068    }
1069
1070    /// c:41 — `contflag` initial state must be non-negative
1071    /// (`mod_export int contflag;` BSS-zero).
1072    #[test]
1073    fn cont_flag_initial_state_non_negative() {
1074        use std::sync::atomic::Ordering;
1075        let v = CONT_FLAG.load(Ordering::SeqCst);
1076        assert!(v >= 0, "cont flag must be non-negative; got {}", v);
1077    }
1078
1079    /// c:46 — `breaks` initial state non-negative
1080    /// (`volatile int breaks;` BSS-zero).
1081    #[test]
1082    fn break_level_initial_state_non_negative() {
1083        use std::sync::atomic::Ordering;
1084        let v = BREAK_LEVEL.load(Ordering::SeqCst);
1085        assert!(v >= 0, "break level must be non-negative; got {}", v);
1086    }
1087
1088    /// c:347 — `selectlist` is idempotent across many calls (no mutation).
1089    #[test]
1090    fn selectlist_no_mutation_across_calls() {
1091        let _g = crate::test_util::global_state_lock();
1092        let items = ["alpha", "beta", "gamma"];
1093        let first = selectlist(&items, 0);
1094        for _ in 0..20 {
1095            assert_eq!(
1096                selectlist(&items, 0),
1097                first,
1098                "selectlist must be pure across repeated calls"
1099            );
1100        }
1101    }
1102
1103    /// c:347 — `selectlist` four items doesn't panic.
1104    #[test]
1105    fn selectlist_four_items_no_panic() {
1106        let _g = crate::test_util::global_state_lock();
1107        let _ = selectlist(&["a", "b", "c", "d"], 0);
1108    }
1109
1110    /// c:347 — `selectlist` with newline-bearing entry doesn't panic.
1111    #[test]
1112    fn selectlist_newline_entry_no_panic() {
1113        let _g = crate::test_util::global_state_lock();
1114        let _ = selectlist(&["a\nb"], 0);
1115    }
1116
1117    /// c:347 — `selectlist` mixed-length entries doesn't panic.
1118    #[test]
1119    fn selectlist_mixed_length_entries_no_panic() {
1120        let _g = crate::test_util::global_state_lock();
1121        let _ = selectlist(&["a", "ab", "abc", "abcd", "abcde"], 0);
1122    }
1123
1124    /// c:347 — `selectlist` Unicode emoji entry doesn't panic.
1125    #[test]
1126    fn selectlist_emoji_entries_no_panic() {
1127        let _g = crate::test_util::global_state_lock();
1128        let _ = selectlist(&["foo", "bar"], 0);
1129    }
1130
1131    /// c:731 — `try_tryflag` load is idempotent (pure read, no mutation).
1132    #[test]
1133    fn try_tryflag_load_idempotent() {
1134        use std::sync::atomic::Ordering;
1135        let first = try_tryflag.load(Ordering::SeqCst);
1136        for _ in 0..50 {
1137            assert_eq!(
1138                try_tryflag.load(Ordering::SeqCst),
1139                first,
1140                "try_tryflag pure load must be deterministic"
1141            );
1142        }
1143    }
1144
1145    /// c:731 — `try_tryflag` static address is stable.
1146    #[test]
1147    fn try_tryflag_address_is_stable() {
1148        let p1 = &try_tryflag as *const _;
1149        let p2 = &try_tryflag as *const _;
1150        assert_eq!(p1, p2, "static must have a single, stable address");
1151    }
1152
1153    /// c:347 — `selectlist` with whitespace-only entries doesn't panic.
1154    #[test]
1155    fn selectlist_whitespace_only_entries_no_panic() {
1156        let _g = crate::test_util::global_state_lock();
1157        let _ = selectlist(&["   ", "\t\t", "  \t  "], 0);
1158    }
1159
1160    /// c:36 / c:41 / c:46 — `LOOP_DEPTH`, `CONT_FLAG`, `BREAK_LEVEL`
1161    /// addresses are stable (no per-call alloc).
1162    #[test]
1163    fn loop_static_addresses_stable() {
1164        let a1 = &LOOP_DEPTH as *const _;
1165        let a2 = &LOOP_DEPTH as *const _;
1166        assert_eq!(a1, a2, "LOOP_DEPTH must be a true static");
1167        let b1 = &CONT_FLAG as *const _;
1168        let b2 = &CONT_FLAG as *const _;
1169        assert_eq!(b1, b2, "CONT_FLAG must be a true static");
1170        let c1 = &BREAK_LEVEL as *const _;
1171        let c2 = &BREAK_LEVEL as *const _;
1172        assert_eq!(c1, c2, "BREAK_LEVEL must be a true static");
1173    }
1174
1175    // ═══════════════════════════════════════════════════════════════════
1176    // Additional C-parity pins for Src/loop.c
1177    // c:347 selectlist — empty / single / huge entries / start-offset edges
1178    // ═══════════════════════════════════════════════════════════════════
1179
1180    /// c:362 — `selectlist([])` empty input returns 0 (no menu rendered, alt).
1181    #[test]
1182    fn selectlist_empty_returns_zero_alt() {
1183        let _g = crate::test_util::global_state_lock();
1184        assert_eq!(
1185            selectlist(&[], 0),
1186            0,
1187            "empty items must produce zero-line output"
1188        );
1189    }
1190
1191    /// c:362 — `selectlist([single])` doesn't panic.
1192    #[test]
1193    fn selectlist_single_entry_no_panic() {
1194        let _g = crate::test_util::global_state_lock();
1195        let _ = selectlist(&["one"], 0);
1196    }
1197
1198    /// c:347 — `selectlist` return type usize (compile-time pin, alt).
1199    #[test]
1200    fn selectlist_returns_usize_type_alt() {
1201        let _g = crate::test_util::global_state_lock();
1202        let _: usize = selectlist(&["a"], 0);
1203    }
1204
1205    /// c:347 — `selectlist` with start ≥ items.len() doesn't panic
1206    /// (start past end is degenerate but safe).
1207    #[test]
1208    fn selectlist_start_past_end_no_panic() {
1209        let _g = crate::test_util::global_state_lock();
1210        let _ = selectlist(&["a", "b"], 100);
1211        let _ = selectlist(&["a", "b"], usize::MAX / 2);
1212    }
1213
1214    /// c:347 — `selectlist` with very large entry list doesn't panic
1215    /// (pin against accidental quadratic / OOB on big counts).
1216    #[test]
1217    fn selectlist_large_list_no_panic() {
1218        let _g = crate::test_util::global_state_lock();
1219        let items: Vec<String> = (0..200).map(|i| format!("item{}", i)).collect();
1220        let refs: Vec<&str> = items.iter().map(|s| s.as_str()).collect();
1221        let _ = selectlist(&refs, 0);
1222    }
1223
1224    /// c:347 — `selectlist` with start at exactly items.len()-1 doesn't panic.
1225    #[test]
1226    fn selectlist_start_at_last_index_no_panic() {
1227        let _g = crate::test_util::global_state_lock();
1228        let _ = selectlist(&["a", "b", "c"], 2);
1229    }
1230
1231    /// c:36/41/46 — LOOP_DEPTH/CONT_FLAG/BREAK_LEVEL atomic i32 type pins.
1232    #[test]
1233    fn loop_statics_are_atomic_i32() {
1234        let _: &AtomicI32 = &LOOP_DEPTH;
1235        let _: &AtomicI32 = &CONT_FLAG;
1236        let _: &AtomicI32 = &BREAK_LEVEL;
1237    }
1238
1239    /// c:36 — LOOP_DEPTH atomic store/load round-trip.
1240    #[test]
1241    fn loop_depth_store_load_round_trip() {
1242        use std::sync::atomic::Ordering;
1243        let _g = crate::test_util::global_state_lock();
1244        let saved = LOOP_DEPTH.load(Ordering::SeqCst);
1245        for v in [0, 1, 5, 100, -1, i32::MAX, i32::MIN] {
1246            LOOP_DEPTH.store(v, Ordering::SeqCst);
1247            assert_eq!(
1248                LOOP_DEPTH.load(Ordering::SeqCst),
1249                v,
1250                "LOOP_DEPTH must round-trip {}",
1251                v
1252            );
1253        }
1254        LOOP_DEPTH.store(saved, Ordering::SeqCst);
1255    }
1256
1257    /// c:41 — CONT_FLAG atomic store/load round-trip.
1258    #[test]
1259    fn cont_flag_store_load_round_trip() {
1260        use std::sync::atomic::Ordering;
1261        let _g = crate::test_util::global_state_lock();
1262        let saved = CONT_FLAG.load(Ordering::SeqCst);
1263        for v in [0, 1, 5, 100, -1, i32::MAX, i32::MIN] {
1264            CONT_FLAG.store(v, Ordering::SeqCst);
1265            assert_eq!(CONT_FLAG.load(Ordering::SeqCst), v);
1266        }
1267        CONT_FLAG.store(saved, Ordering::SeqCst);
1268    }
1269
1270    /// c:46 — BREAK_LEVEL atomic store/load round-trip.
1271    #[test]
1272    fn break_level_store_load_round_trip() {
1273        use std::sync::atomic::Ordering;
1274        let _g = crate::test_util::global_state_lock();
1275        let saved = BREAK_LEVEL.load(Ordering::SeqCst);
1276        for v in [0, 1, 5, 100, -1, i32::MAX, i32::MIN] {
1277            BREAK_LEVEL.store(v, Ordering::SeqCst);
1278            assert_eq!(BREAK_LEVEL.load(Ordering::SeqCst), v);
1279        }
1280        BREAK_LEVEL.store(saved, Ordering::SeqCst);
1281    }
1282
1283    /// c:347 — `selectlist` is deterministic across identical calls
1284    /// (the return value depends only on the inputs).
1285    #[test]
1286    fn selectlist_deterministic_on_identical_input() {
1287        let _g = crate::test_util::global_state_lock();
1288        let first = selectlist(&["one", "two", "three"], 0);
1289        for _ in 0..5 {
1290            assert_eq!(
1291                selectlist(&["one", "two", "three"], 0),
1292                first,
1293                "selectlist must be deterministic"
1294            );
1295        }
1296    }
1297}