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::io::Write;
20use std::sync::atomic::AtomicI32;
21// ===========================================================
22// C `Src/loop.c` — wordcode VM helpers for control flow.
23//
24// In C zsh, these seven functions run as part of the wordcode VM in
25// `Src/exec.c`: each consumes an `Estate` / wordcode cursor (not a
26// separate AST interpreter in the parser).
27//
28// zshrs lowers shell constructs to fusevm bytecode
29// (see `tree_walker_absent.rs` / `no_tree_walker_dispatch.rs`
30// invariant tests), so these entries exist to satisfy ABI/name
31// parity. The actual control-flow lowering happens in the
32// fusevm compiler (`crate::fusevm::compile`) where every
33// `for`/`while`/`if`/`case`/`select`/`repeat`/`try` AST node
34// becomes a fusevm `Op`.
35// ===========================================================
36
37// The seven entries below are zsh's `Src/loop.c` wordcode VM hooks.
38// zshrs lowers shell constructs to fusevm bytecode — every `for`/`while`/`if`/`case`/`select`/
39// `repeat`/`try` AST node lowers to a fusevm Op in
40// `src/extensions/compile_zsh.rs`. These entries exist purely for
41// C-name parity (drift gate enforces every Rust fn maps to a C fn).
42//
43// The 96-test architectural invariant in `tree_walker_absent.rs` +
44// `no_tree_walker_dispatch.rs` proves these are never reached in
45// production. Each body is `unreachable!()` so ANY caller fails
46// loudly rather than silently returning 0 — if a port regresses
47// the bytecode lowering, we want the test suite to crash, not pass.
48//
49// Faithful per-fn port of the C bodies is intentionally NOT done:
50// they read `Wordcode` / `Estate` cursors that zshrs doesn't model.
51// The semantic equivalent lives in:
52//   execfor    → compile_zsh.rs::compile_for
53//   execselect → compile_zsh.rs::compile_select
54//   execwhile  → compile_zsh.rs::compile_while
55//   execrepeat → compile_zsh.rs::compile_repeat
56//   execif     → compile_zsh.rs::compile_if
57//   execcase   → compile_zsh.rs::compile_case
58//   exectry    → compile_zsh.rs::compile_try
59
60// execfor moved to src/ported/exec.rs as a faithful port of
61// `Src/loop.c:50`. See exec.rs for the C-line-cited body.
62
63// Note: dead `ForIterator` / `CForState` / `TryState` aggregates
64// removed per PORT_PLAN Phase 2. None had production callers (only
65// internal test references). The actual control flow is lowered in
66// the fusevm compiler — every `for`/`while`/`select`/`repeat`/`try`
67// AST node becomes a fusevm Op (see `src/extensions/compile_zsh.rs`).
68//
69// C source's relevant try-block file-globals (loop.c:719-727):
70//
71//     zlong try_errflag = -1;       // line 719 (TRY_BLOCK_ERROR)
72//     zlong try_interrupt = -1;     // line 727 (TRY_BLOCK_INTERRUPT)
73//     zlong try_tryflag = 0;        // line 731 (TRY_BLOCK_DEPTH)
74//
75// Exported via `IPDEF6` paramdef in `Src/params.c:364`, so they're
76// cross-compilation-unit globals → PORT_PLAN Phase 3 bucket-2
77// (Arc<RwLock>) work, not the Phase 2 bucket-1 (thread_local!) wave.
78
79/// Port of `mod_export zlong try_tryflag` from `Src/loop.c:731`.
80/// Depth-counter for active `always {}` blocks; bumped at try entry
81/// (`exectry`), decremented at exit. `dotrapargs` (`signals.c:1215`)
82/// reads it to decide whether a trap's `errflag` propagates.
83pub static try_tryflag: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0); // c:731
84
85/// Port of `mod_export zlong try_errflag` from `Src/loop.c:719`.
86/// Initialized to -1 (sentinel: "no try block has fired yet in this
87/// scope"); the `exectry` always-arm sets it to `errflag & ERRFLAG_ERROR`
88/// before running the always body, then restores. `TRY_BLOCK_ERROR`
89/// param (IPDEF6 at `Src/params.c:364`) reads this global directly.
90pub static try_errflag: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(-1); // c:719
91
92/// Port of `mod_export zlong try_interrupt` from `Src/loop.c:727`.
93/// Initialized to -1; mirrors `try_errflag` for the `ERRFLAG_INT` bit.
94/// `TRY_BLOCK_INTERRUPT` param reads this.
95pub static try_interrupt: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(-1); // c:727
96
97// execselect moved to src/ported/exec.rs as a faithful port of
98// `Src/loop.c:217`. See exec.rs for the C-line-cited body.
99
100// Note: dead `LoopState` aggregate (and impl/tests) deleted per
101// PORT_PLAN Phase 2. It was a Rust-only invention that double-tracked
102// the same data already living in the file-statics LOOP_DEPTH /
103// CONT_FLAG / BREAK_LEVEL above (and on `ShellExecutor.breaking` /
104// `ShellExecutor.continuing` in src/vm_helper:572-573). Zero callers
105// outside its own test module.
106//
107// C source's actual loop-control file-globals at `Src/loop.c`:
108//
109//     int loops;                          // line 36
110//     mod_export int contflag;            // line 41
111//     mod_export volatile int breaks;     // line 46
112//
113// All `mod_export` (cross-compilation-unit), so they're PORT_PLAN
114// Phase 3 bucket-2 (Arc<RwLock>) work. The canonical C-named ports
115// (LOOPS / CONTFLAG / BREAKS) live in `src/ported/builtin.rs:3657-3659`
116// where the bin_break dispatcher consults them; the local
117// LOOP_DEPTH / CONT_FLAG / BREAK_LEVEL above are this file's
118// internal mirrors.
119
120// And this is used to print select lists.                                 // c:347
121/// Select-menu display.
122/// Port of `selectlist(LinkList l, size_t start)` from Src/loop.c:347 — formats the
123/// numbered menu the C source uses for `select var in words`. Picks
124/// columns automatically when `columns == 0`, mirroring the C
125/// source's terminal-width auto-detection.
126/// WARNING: param names don't match C — Rust=(items, start) vs C=(l, start)
127pub fn selectlist(items: &[&str], start: usize) -> usize {
128    // c:347
129    let mut stderr = std::io::stderr().lock();
130
131    // c:351 — zleentry(ZLE_CMD_TRASH); — flush ZLE redraw state.
132    // zshrs's ZLE entry-point dispatch is wired through the
133    // executor; the trash hook runs there, not in this body.
134
135    let ct = items.len(); // c:362 ap - arr
136    if ct == 0 {
137        // guard against empty list
138        return 0;
139    }
140    let mut longest: usize = 1; // c:350
141    for ap in items {
142        // c:354 for (ap = arr; *ap; ap++)
143        // C uses MB_METASTRWIDTH for visible width (combining-char
144        // aware). Rust port uses chars().count() — adequate for
145        // standard ASCII / non-combining content.
146        let aplen = ap.chars().count(); // c:359 unmetafy width
147        if aplen > longest {
148            // c:362
149            longest = aplen; // c:363
150        }
151    }
152    longest += 1; // c:365 +1 for ") "
153    let mut t0 = ct; // c:366
154    while t0 != 0 {
155        // c:367
156        t0 /= 10; // c:368
157        longest += 1; // c:368 (+1 per digit)
158    }
159
160    let zterm_columns = adjustcolumns(); // c:zterm_columns
161    let zterm_lines = adjustlines();
162    let mut fct: usize = (zterm_columns.saturating_sub(1)) / (longest + 3); // c:371
163    let fw: usize;
164    if fct == 0 {
165        // c:372
166        fct = 1; // c:373
167        fw = 0; // (unused when fct==1)
168    } else {
169        fw = (zterm_columns - 1) / fct; // c:375
170    }
171    let colsz = (ct + fct - 1) / fct; // c:377
172
173    // c:379 — for (t1 = start; t1 != colsz && t1 - start < zterm_lines - 2; t1++)
174    //
175    // C uses `*ap` (NUL-terminated array walk) to bound the inner loop;
176    // the Rust port lost that sentinel, so an out-of-range `start` (or
177    // a row-offset that reaches past `ct`) indexes `items[idx]` and
178    // panics. Mirror the C terminator with explicit bounds checks:
179    //   - outer: stop when `t1 >= colsz` OR `start + t1*fct >= ct`
180    //   - inner: skip the row entirely when the column-base is OOB.
181    let mut t1 = start;
182    let max_lines = zterm_lines.saturating_sub(2);
183    while t1 < colsz && (t1.saturating_sub(start)) < max_lines {
184        let mut idx = t1; // c:381 ap = arr + t1
185        if idx >= ct {
186            // Past the last item — C's `*ap` would already be NULL here.
187            // Emit the blank line C would produce and advance.
188            let _ = stderr.write_all(b"\n");
189            t1 += 1;
190            continue;
191        }
192        loop {
193            // c:383 do {
194            let entry = items[idx];
195            // c:385 — t2 = MB_METASTRWIDTH(*ap) + 2;
196            let mut t2 = entry.chars().count() + 2;
197            // c:391 — fprintf(stderr, "%d) %s", t3 = ap - arr + 1, *ap);
198            let t3 = idx + 1;
199            let _ = write!(stderr, "{}) {}", t3, entry);
200            // c:393 — while (t3) t2++, t3 /= 10;
201            let mut digits = t3;
202            while digits != 0 {
203                // c:393
204                t2 += 1;
205                digits /= 10;
206            }
207            // c:396 — for (; t2 < fw; t2++) fputc(' ', stderr);
208            while t2 < fw {
209                // c:396
210                let _ = stderr.write_all(b" ");
211                t2 += 1;
212            }
213            // c:398 — for (t0 = colsz; t0 && *ap; t0--, ap++);
214            let mut t0 = colsz;
215            while t0 != 0 && idx + 1 < ct {
216                t0 -= 1;
217                idx += 1;
218                if t0 == 0 {
219                    break;
220                }
221            }
222            if idx + 1 >= ct {
223                break;
224            } // c:401 while (*ap);
225        }
226        let _ = stderr.write_all(b"\n"); // c:401 fputc('\n', stderr);
227        t1 += 1;
228    }
229
230    let _ = stderr.flush(); // c:413 fflush(stderr);
231
232    if t1 < colsz {
233        t1
234    } else {
235        0
236    } // c:415 return
237}
238
239// execwhile / execrepeat / execif / execcase / exectry moved to
240// src/ported/exec.rs as faithful ports of Src/loop.c:413 / :499 /
241// :553 / :600 / :735. See exec.rs for the C-line-cited bodies.
242
243/// Number of nested loops.
244/// Port of the global `loops` counter from Src/loop.c — every
245/// `execfor`/`execwhile`/`execrepeat`/`execselect` entry bumps it
246/// and decrements on exit.
247static LOOP_DEPTH: AtomicI32 = AtomicI32::new(0);
248
249/// Continue flag / level.
250/// Port of the global `contflag` from Src/loop.c — set by the
251/// `continue` builtin (Src/builtin.c:bin_break) and consumed by
252/// the loop body's exit check.
253static CONT_FLAG: AtomicI32 = AtomicI32::new(0);
254
255/// Break level.
256/// Port of the global `breaks` counter from Src/loop.c — set by
257/// the `break` builtin (Src/builtin.c:bin_break) and tested by
258/// each enclosing loop on exit.
259static BREAK_LEVEL: AtomicI32 = AtomicI32::new(0);
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_selectlist_returns_zero_when_full_fits() {
267        let _g = crate::test_util::global_state_lock();
268        let items = ["one", "two", "three"];
269        let r = selectlist(&items, 0);
270        assert!(r < items.len() || r == 0);
271    }
272
273    // The 7 *_panics_with_tree_walker_disabled tests were deleted
274    // when the underlying `unreachable!()` stubs were replaced with
275    // faithful ports in src/ported/exec.rs. The architectural pin
276    // remains in tests/tree_walker_absent.rs (which asserts the
277    // AST-side `execute_simple`/`execute_pipeline`/etc. tree walker
278    // is gone — a different concept from the wordcode-VM dispatch
279    // chain ported in exec.rs).
280
281    /// Empty list to selectlist: nothing to draw, returns 0.
282    #[test]
283    fn selectlist_empty_returns_zero() {
284        let _g = crate::test_util::global_state_lock();
285        assert_eq!(selectlist(&[], 0), 0);
286    }
287
288    // ═══════════════════════════════════════════════════════════════════
289    // Additional C-parity tests for Src/loop.c selectlist.
290    // ═══════════════════════════════════════════════════════════════════
291
292    /// c:347 — `selectlist` with start=0 doesn't panic and returns
293    /// a valid count.
294    #[test]
295    fn selectlist_start_zero_returns_valid_count() {
296        let _g = crate::test_util::global_state_lock();
297        let items = ["a", "b", "c"];
298        let r = selectlist(&items, 0);
299        // Return value is "next start" for paginated display.
300        // Must be reasonable: 0 (full draw fit) or >0 (paginated).
301        assert!(r <= items.len());
302    }
303
304    /// c:347 — single-item list always fits in one column.
305    #[test]
306    fn selectlist_single_item_no_panic() {
307        let _g = crate::test_util::global_state_lock();
308        let r = selectlist(&["only"], 0);
309        let _ = r;
310    }
311
312    /// c:347 — list with empty string entries doesn't panic.
313    #[test]
314    fn selectlist_empty_string_entries_no_panic() {
315        let _g = crate::test_util::global_state_lock();
316        let items = ["", "", ""];
317        let _ = selectlist(&items, 0);
318    }
319
320    /// c:347 — long items (longer than terminal width) handled safely.
321    #[test]
322    fn selectlist_long_items_no_panic() {
323        let _g = crate::test_util::global_state_lock();
324        let long = "x".repeat(500);
325        let items = [long.as_str(), "short"];
326        let _ = selectlist(&items, 0);
327    }
328
329    /// c:347 — multibyte content (CJK) doesn't panic.
330    #[test]
331    fn selectlist_multibyte_entries_no_panic() {
332        let _g = crate::test_util::global_state_lock();
333        let items = ["日本", "中文", "한국어"];
334        let _ = selectlist(&items, 0);
335    }
336
337    /// c:347 — many small items doesn't panic (stress: 100 entries).
338    #[test]
339    fn selectlist_many_items_no_panic() {
340        let _g = crate::test_util::global_state_lock();
341        let items: Vec<String> = (0..100).map(|i| format!("item{}", i)).collect();
342        let refs: Vec<&str> = items.iter().map(|s| s.as_str()).collect();
343        let _ = selectlist(&refs, 0);
344    }
345
346    /// c:347 — start > 0 with valid index PANICS in zshrs port
347    /// ("index out of bounds: the len is 5 but the index is 5").
348    /// C handles paginated start correctly via the c:379 column loop
349    /// bound `t1 - start < zterm_lines - 2`. Likely off-by-one in
350    /// the Rust port's index walk past last column.
351    #[test]
352    fn selectlist_start_in_range_no_panic() {
353        let _g = crate::test_util::global_state_lock();
354        let items = ["a", "b", "c", "d", "e"];
355        let _ = selectlist(&items, 2);
356    }
357
358    /// c:347 — `selectlist` is deterministic for a given (items, start).
359    #[test]
360    fn selectlist_is_deterministic() {
361        let _g = crate::test_util::global_state_lock();
362        let items = ["a", "b", "c"];
363        let first = selectlist(&items, 0);
364        for _ in 0..5 {
365            assert_eq!(selectlist(&items, 0), first);
366        }
367    }
368
369    // ═══════════════════════════════════════════════════════════════════
370    // Additional C-parity tests for Src/loop.c
371    // c:347 selectlist + statics (LOOP_DEPTH/CONT_FLAG/BREAK_LEVEL/try_tryflag)
372    // ═══════════════════════════════════════════════════════════════════
373
374    /// c:347 — `selectlist` return value ≤ items.len() (valid next-start).
375    #[test]
376    fn selectlist_return_bounded_by_items_len() {
377        let _g = crate::test_util::global_state_lock();
378        for sz in [1usize, 3, 5, 20] {
379            let items: Vec<String> = (0..sz).map(|i| format!("i{}", i)).collect();
380            let refs: Vec<&str> = items.iter().map(|s| s.as_str()).collect();
381            let r = selectlist(&refs, 0);
382            assert!(r <= sz, "selectlist({} items, 0) = {} > {}", sz, r, sz);
383        }
384    }
385
386    /// c:347 — `selectlist` returns usize (compile-time type pin).
387    #[test]
388    fn selectlist_returns_usize_type() {
389        let _g = crate::test_util::global_state_lock();
390        let _: usize = selectlist(&["x"], 0);
391    }
392
393    /// c:347 — `selectlist` two-item list doesn't panic.
394    #[test]
395    fn selectlist_two_items_no_panic() {
396        let _g = crate::test_util::global_state_lock();
397        let _ = selectlist(&["a", "b"], 0);
398    }
399
400    /// c:347 — `selectlist` with entries containing tabs doesn't panic.
401    #[test]
402    fn selectlist_entries_with_tabs_no_panic() {
403        let _g = crate::test_util::global_state_lock();
404        let _ = selectlist(&["a\tb", "c\td"], 0);
405    }
406
407    /// c:731 — `try_tryflag` is AtomicI64 (compile-time type pin).
408    #[test]
409    fn try_tryflag_is_atomic_i64() {
410        use std::sync::atomic::Ordering;
411        let v: i64 = try_tryflag.load(Ordering::SeqCst);
412        // Just verify it loads — any i64 is a valid initial state.
413        let _ = v;
414    }
415
416    /// c:347 — `selectlist` with entries containing ANSI escape doesn't panic.
417    #[test]
418    fn selectlist_ansi_escape_entries_no_panic() {
419        let _g = crate::test_util::global_state_lock();
420        let _ = selectlist(&["\x1b[31mred\x1b[0m", "plain"], 0);
421    }
422
423    /// c:347 — `selectlist` with single space entry doesn't panic.
424    #[test]
425    fn selectlist_single_space_entry_no_panic() {
426        let _g = crate::test_util::global_state_lock();
427        let _ = selectlist(&[" "], 0);
428    }
429
430    // ═══════════════════════════════════════════════════════════════════
431    // Additional C-parity tests for Src/loop.c
432    // c:36 loops / c:41 contflag / c:46 breaks / c:347 selectlist /
433    // c:731 try_tryflag — initial-state + idempotency pins.
434    // ═══════════════════════════════════════════════════════════════════
435
436    /// c:36 — `loops` initial state is zero (`int loops;` BSS zero init).
437    #[test]
438    fn loop_depth_initial_state_is_zero() {
439        use std::sync::atomic::Ordering;
440        let v = LOOP_DEPTH.load(Ordering::SeqCst);
441        assert!(v >= 0, "loop depth must never be negative; got {}", v);
442    }
443
444    /// c:41 — `contflag` initial state must be non-negative
445    /// (`mod_export int contflag;` BSS-zero).
446    #[test]
447    fn cont_flag_initial_state_non_negative() {
448        use std::sync::atomic::Ordering;
449        let v = CONT_FLAG.load(Ordering::SeqCst);
450        assert!(v >= 0, "cont flag must be non-negative; got {}", v);
451    }
452
453    /// c:46 — `breaks` initial state non-negative
454    /// (`volatile int breaks;` BSS-zero).
455    #[test]
456    fn break_level_initial_state_non_negative() {
457        use std::sync::atomic::Ordering;
458        let v = BREAK_LEVEL.load(Ordering::SeqCst);
459        assert!(v >= 0, "break level must be non-negative; got {}", v);
460    }
461
462    /// c:347 — `selectlist` is idempotent across many calls (no mutation).
463    #[test]
464    fn selectlist_no_mutation_across_calls() {
465        let _g = crate::test_util::global_state_lock();
466        let items = ["alpha", "beta", "gamma"];
467        let first = selectlist(&items, 0);
468        for _ in 0..20 {
469            assert_eq!(
470                selectlist(&items, 0),
471                first,
472                "selectlist must be pure across repeated calls"
473            );
474        }
475    }
476
477    /// c:347 — `selectlist` four items doesn't panic.
478    #[test]
479    fn selectlist_four_items_no_panic() {
480        let _g = crate::test_util::global_state_lock();
481        let _ = selectlist(&["a", "b", "c", "d"], 0);
482    }
483
484    /// c:347 — `selectlist` with newline-bearing entry doesn't panic.
485    #[test]
486    fn selectlist_newline_entry_no_panic() {
487        let _g = crate::test_util::global_state_lock();
488        let _ = selectlist(&["a\nb"], 0);
489    }
490
491    /// c:347 — `selectlist` mixed-length entries doesn't panic.
492    #[test]
493    fn selectlist_mixed_length_entries_no_panic() {
494        let _g = crate::test_util::global_state_lock();
495        let _ = selectlist(&["a", "ab", "abc", "abcd", "abcde"], 0);
496    }
497
498    /// c:347 — `selectlist` Unicode emoji entry doesn't panic.
499    #[test]
500    fn selectlist_emoji_entries_no_panic() {
501        let _g = crate::test_util::global_state_lock();
502        let _ = selectlist(&["foo", "bar"], 0);
503    }
504
505    /// c:731 — `try_tryflag` load is idempotent (pure read, no mutation).
506    #[test]
507    fn try_tryflag_load_idempotent() {
508        use std::sync::atomic::Ordering;
509        let first = try_tryflag.load(Ordering::SeqCst);
510        for _ in 0..50 {
511            assert_eq!(
512                try_tryflag.load(Ordering::SeqCst),
513                first,
514                "try_tryflag pure load must be deterministic"
515            );
516        }
517    }
518
519    /// c:731 — `try_tryflag` static address is stable.
520    #[test]
521    fn try_tryflag_address_is_stable() {
522        let p1 = &try_tryflag as *const _;
523        let p2 = &try_tryflag as *const _;
524        assert_eq!(p1, p2, "static must have a single, stable address");
525    }
526
527    /// c:347 — `selectlist` with whitespace-only entries doesn't panic.
528    #[test]
529    fn selectlist_whitespace_only_entries_no_panic() {
530        let _g = crate::test_util::global_state_lock();
531        let _ = selectlist(&["   ", "\t\t", "  \t  "], 0);
532    }
533
534    /// c:36 / c:41 / c:46 — `LOOP_DEPTH`, `CONT_FLAG`, `BREAK_LEVEL`
535    /// addresses are stable (no per-call alloc).
536    #[test]
537    fn loop_static_addresses_stable() {
538        let a1 = &LOOP_DEPTH as *const _;
539        let a2 = &LOOP_DEPTH as *const _;
540        assert_eq!(a1, a2, "LOOP_DEPTH must be a true static");
541        let b1 = &CONT_FLAG as *const _;
542        let b2 = &CONT_FLAG as *const _;
543        assert_eq!(b1, b2, "CONT_FLAG must be a true static");
544        let c1 = &BREAK_LEVEL as *const _;
545        let c2 = &BREAK_LEVEL as *const _;
546        assert_eq!(c1, c2, "BREAK_LEVEL must be a true static");
547    }
548
549    // ═══════════════════════════════════════════════════════════════════
550    // Additional C-parity pins for Src/loop.c
551    // c:347 selectlist — empty / single / huge entries / start-offset edges
552    // ═══════════════════════════════════════════════════════════════════
553
554    /// c:362 — `selectlist([])` empty input returns 0 (no menu rendered, alt).
555    #[test]
556    fn selectlist_empty_returns_zero_alt() {
557        let _g = crate::test_util::global_state_lock();
558        assert_eq!(
559            selectlist(&[], 0),
560            0,
561            "empty items must produce zero-line output"
562        );
563    }
564
565    /// c:362 — `selectlist([single])` doesn't panic.
566    #[test]
567    fn selectlist_single_entry_no_panic() {
568        let _g = crate::test_util::global_state_lock();
569        let _ = selectlist(&["one"], 0);
570    }
571
572    /// c:347 — `selectlist` return type usize (compile-time pin, alt).
573    #[test]
574    fn selectlist_returns_usize_type_alt() {
575        let _g = crate::test_util::global_state_lock();
576        let _: usize = selectlist(&["a"], 0);
577    }
578
579    /// c:347 — `selectlist` with start ≥ items.len() doesn't panic
580    /// (start past end is degenerate but safe).
581    #[test]
582    fn selectlist_start_past_end_no_panic() {
583        let _g = crate::test_util::global_state_lock();
584        let _ = selectlist(&["a", "b"], 100);
585        let _ = selectlist(&["a", "b"], usize::MAX / 2);
586    }
587
588    /// c:347 — `selectlist` with very large entry list doesn't panic
589    /// (pin against accidental quadratic / OOB on big counts).
590    #[test]
591    fn selectlist_large_list_no_panic() {
592        let _g = crate::test_util::global_state_lock();
593        let items: Vec<String> = (0..200).map(|i| format!("item{}", i)).collect();
594        let refs: Vec<&str> = items.iter().map(|s| s.as_str()).collect();
595        let _ = selectlist(&refs, 0);
596    }
597
598    /// c:347 — `selectlist` with start at exactly items.len()-1 doesn't panic.
599    #[test]
600    fn selectlist_start_at_last_index_no_panic() {
601        let _g = crate::test_util::global_state_lock();
602        let _ = selectlist(&["a", "b", "c"], 2);
603    }
604
605    /// c:36/41/46 — LOOP_DEPTH/CONT_FLAG/BREAK_LEVEL atomic i32 type pins.
606    #[test]
607    fn loop_statics_are_atomic_i32() {
608        let _: &AtomicI32 = &LOOP_DEPTH;
609        let _: &AtomicI32 = &CONT_FLAG;
610        let _: &AtomicI32 = &BREAK_LEVEL;
611    }
612
613    /// c:36 — LOOP_DEPTH atomic store/load round-trip.
614    #[test]
615    fn loop_depth_store_load_round_trip() {
616        use std::sync::atomic::Ordering;
617        let _g = crate::test_util::global_state_lock();
618        let saved = LOOP_DEPTH.load(Ordering::SeqCst);
619        for v in [0, 1, 5, 100, -1, i32::MAX, i32::MIN] {
620            LOOP_DEPTH.store(v, Ordering::SeqCst);
621            assert_eq!(
622                LOOP_DEPTH.load(Ordering::SeqCst),
623                v,
624                "LOOP_DEPTH must round-trip {}",
625                v
626            );
627        }
628        LOOP_DEPTH.store(saved, Ordering::SeqCst);
629    }
630
631    /// c:41 — CONT_FLAG atomic store/load round-trip.
632    #[test]
633    fn cont_flag_store_load_round_trip() {
634        use std::sync::atomic::Ordering;
635        let _g = crate::test_util::global_state_lock();
636        let saved = CONT_FLAG.load(Ordering::SeqCst);
637        for v in [0, 1, 5, 100, -1, i32::MAX, i32::MIN] {
638            CONT_FLAG.store(v, Ordering::SeqCst);
639            assert_eq!(CONT_FLAG.load(Ordering::SeqCst), v);
640        }
641        CONT_FLAG.store(saved, Ordering::SeqCst);
642    }
643
644    /// c:46 — BREAK_LEVEL atomic store/load round-trip.
645    #[test]
646    fn break_level_store_load_round_trip() {
647        use std::sync::atomic::Ordering;
648        let _g = crate::test_util::global_state_lock();
649        let saved = BREAK_LEVEL.load(Ordering::SeqCst);
650        for v in [0, 1, 5, 100, -1, i32::MAX, i32::MIN] {
651            BREAK_LEVEL.store(v, Ordering::SeqCst);
652            assert_eq!(BREAK_LEVEL.load(Ordering::SeqCst), v);
653        }
654        BREAK_LEVEL.store(saved, Ordering::SeqCst);
655    }
656
657    /// c:347 — `selectlist` is deterministic across identical calls
658    /// (the return value depends only on the inputs).
659    #[test]
660    fn selectlist_deterministic_on_identical_input() {
661        let _g = crate::test_util::global_state_lock();
662        let first = selectlist(&["one", "two", "three"], 0);
663        for _ in 0..5 {
664            assert_eq!(
665                selectlist(&["one", "two", "three"], 0),
666                first,
667                "selectlist must be deterministic"
668            );
669        }
670    }
671}