Skip to main content

zsh/ported/modules/
example.rs

1//! `zsh/example` module — port of `Src/Modules/example.c`.
2//!
3//! Top-level declaration order matches C source line-by-line:
4//!   - `static zlong intparam;`                     c:35
5//!   - `static char *strparam;`                     c:36
6//!   - `static char **arrparam;`                    c:37
7//!   - `bin_example(nam, args, ops, func)`          c:42
8//!   - `cond_p_len(a, id)`                          c:80
9//!   - `cond_i_ex(a, id)`                           c:95
10//!   - `math_sum(name, argc, argv, id)`             c:104
11//!   - `math_length(name, arg, id)`                 c:133
12//!   - `ex_wrapper(prog, w, name)`                  c:145
13//!   - `static struct builtin bintab[]`             c:164
14//!   - `static struct conddef cotab[]`              c:168
15//!   - `static struct paramdef patab[]`             c:173
16//!   - `static struct mathfunc mftab[]`             c:179
17//!   - `static struct funcwrap wrapper[]`           c:184
18//!   - `static struct features module_features`     c:188
19//!   - `setup_(m)`                                   c:198
20//!   - `features_(m, features)`                      c:207
21//!   - `enables_(m, enables)`                        c:215
22//!   - `boot_(m)`                                    c:222
23//!   - `cleanup_(m)`                                 c:235
24//!   - `finish_(m)`                                  c:243
25
26#![allow(non_camel_case_types)]
27#![allow(non_upper_case_globals)]
28#![allow(non_snake_case)]
29
30use std::io::Write;
31use std::sync::atomic::{AtomicI64, Ordering};
32use std::sync::{Mutex, OnceLock};
33
34use crate::ported::compat::output64;
35use crate::ported::cond::{cond_str, cond_val};
36use crate::ported::math::{mnumber, MN_FLOAT, MN_INTEGER};
37use crate::ported::string::dyncat;
38use crate::ported::zsh_h::{features, module, options, MAX_OPS, OPT_ISSET};
39
40// =====================================================================
41// /* parameters */                                                  c:33
42// =====================================================================
43
44/// Port of `static zlong intparam;` from `Src/Modules/example.c:35`.
45/// Bound to the `exint` integer paramdef at c:175.
46pub static intparam: AtomicI64 = AtomicI64::new(0); // c:35
47
48/// Port of `static char *strparam;` from `Src/Modules/example.c:36`.
49/// Bound to the `exstr` string paramdef at c:176. `None` mirrors C's
50/// initial NULL which `bin_example` prints as the empty string at c:63.
51pub static strparam: Mutex<Option<String>> = Mutex::new(None); // c:36
52
53/// Port of `static char **arrparam;` from `Src/Modules/example.c:37`.
54/// Bound to the `exarr` array paramdef at c:174. `None` mirrors C's
55/// initial NULL.
56pub static arrparam: Mutex<Option<Vec<String>>> = Mutex::new(None); // c:37
57
58// =====================================================================
59// bin_example(char *nam, char **args, Options ops, UNUSED(int func))  c:42
60// =====================================================================
61
62/// Port of `bin_example(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/example.c:42`.
63///
64/// C signature mirrored verbatim:
65/// ```c
66/// static int
67/// bin_example(char *nam, char **args, Options ops, UNUSED(int func))
68/// ```
69#[allow(unused_variables)]
70pub fn bin_example(nam: &str, args: &[String], ops: &options, func: i32) -> i32 {
71    // c:42
72    let mut stdout = std::io::stdout().lock();
73    // c:44 — `unsigned char c;`
74    let mut c: u8;
75    // c:45 — `char **oargs = args, **p = arrparam;`
76    let oargs = args; // c:45
77                      // `p` walks `arrparam` below; lock acquired in the print block.
78                      // c:46 — `long i = 0;`
79    let mut i: i64 = 0; // c:46
80
81    let _ = write!(stdout, "Options: "); // c:48
82                                         // c:49-51 — `for (c = 32; ++c < 128;) if (OPT_ISSET(ops,c)) putchar(c);`
83    c = 32; // c:49
84    loop {
85        c += 1;
86        if c >= 128 {
87            break;
88        }
89        if OPT_ISSET(ops, c) {
90            // c:50
91            let _ = write!(stdout, "{}", c as char); // c:51
92        }
93    }
94    let _ = write!(stdout, "\nArguments:"); // c:52
95                                            // c:53-56 — `for (; *args; i++, args++) { putchar(' '); fputs(*args, stdout); }`
96    for a in args {
97        let _ = write!(stdout, " "); // c:54
98        let _ = write!(stdout, "{}", a); // c:55
99        i += 1; // c:53
100    }
101    let _ = writeln!(stdout, "\nName: {}", nam); // c:57
102                                                 // c:58-62 — `#ifdef ZSH_64_BIT_TYPE` branch is taken on every
103                                                 // modern platform; port that branch.
104    let _ = writeln!(
105        stdout,
106        "\nInteger Parameter: {}",
107        output64(intparam.load(Ordering::Relaxed))
108    ); // c:59
109    {
110        let sp = strparam.lock().unwrap();
111        let _ = writeln!(stdout, "String Parameter: {}", sp.as_deref().unwrap_or(""));
112        // c:63
113    }
114    let _ = write!(stdout, "Array Parameter:"); // c:64
115    {
116        let p = arrparam.lock().unwrap();
117        if let Some(arr) = p.as_ref() {
118            // c:65 if (p)
119            for s in arr {
120                // c:66 while (*p)
121                let _ = write!(stdout, " {}", s); // c:66
122            }
123        }
124    }
125    let _ = writeln!(stdout); // c:67
126
127    intparam.store(i, Ordering::Relaxed); // c:69 intparam = i;
128                                          // c:70 — zsfree(strparam);
129                                          // c:71 — strparam = ztrdup(*oargs ? *oargs : "");
130    let new_sp = if let Some(first) = oargs.first() {
131        first.clone()
132    } else {
133        String::new()
134    };
135    *strparam.lock().unwrap() = Some(new_sp); // c:71
136                                              // c:72-74 — if (arrparam) freearray(arrparam); arrparam = zarrdup(oargs);
137    let new_ap: Vec<String> = oargs.to_vec(); // c:80
138    *arrparam.lock().unwrap() = Some(new_ap); // c:80
139
140    0 // c:80
141}
142
143// =====================================================================
144// cond_p_len(char **a, UNUSED(int id))                               c:80
145// =====================================================================
146
147/// Port of `cond_p_len(char **a, UNUSED(int id))` from `Src/Modules/example.c:80`.
148#[allow(unused_variables)]
149pub fn cond_p_len(a: &[String], id: i32) -> i32 {
150    // c:80
151    // c:80 — `char *s1 = cond_str(a, 0, 0);`
152    let s1: String = cond_str(a, 0, false); // c:82
153    if a.len() > 1 {
154        // c:84 if (a[1])
155        let v: i64 = cond_val(a, 1); // c:85 zlong v = cond_val(a, 1);
156                                     // c:87 — `return strlen(s1) == v;`
157        if (s1.len() as i64) == v {
158            1
159        } else {
160            0
161        }
162    } else {
163        // c:95
164        // c:95 — `return !s1[0];`
165        if s1.is_empty() {
166            1
167        } else {
168            0
169        }
170    }
171}
172
173// =====================================================================
174// cond_i_ex(char **a, UNUSED(int id))                                c:95
175// =====================================================================
176
177/// Port of `cond_i_ex(char **a, UNUSED(int id))` from `Src/Modules/example.c:95`.
178#[allow(unused_variables)]
179pub fn cond_i_ex(a: &[String], id: i32) -> i32 {
180    // c:95
181    // c:95 — `char *s1 = cond_str(a, 0, 0), *s2 = cond_str(a, 1, 0);`
182    let s1: String = cond_str(a, 0, false); // c:104
183    let s2: String = cond_str(a, 1, false); // c:104
184                                            // c:104 — `return !strcmp("example", dyncat(s1, s2));`
185    if dyncat(&s1, &s2) == "example" {
186        1
187    } else {
188        0
189    } // c:104
190}
191
192// =====================================================================
193// math_sum(UNUSED(char *name), int argc, mnumber *argv, UNUSED(int id))  c:104
194// =====================================================================
195
196/// Port of `math_sum(UNUSED(char *name), int argc, mnumber *argv, UNUSED(int id))` from `Src/Modules/example.c:104`.
197#[allow(unused_variables)]
198pub fn math_sum(name: &str, argc: i32, argv: &[mnumber], id: i32) -> mnumber {
199    // c:104
200    // c:104 — `mnumber ret;`
201    let mut ret = mnumber {
202        l: 0,
203        d: 0.0,
204        type_: MN_INTEGER,
205    };
206    // c:107 — `int f = 0;`
207    let mut f: i32 = 0;
208    // c:109 — `ret.u.l = 0;`
209    ret.l = 0;
210    let mut argc = argc;
211    let mut p: usize = 0; // emulates argv++ pointer walk (c:124)
212    while {
213        // c:110 while (argc--)
214        let go = argc > 0;
215        argc -= 1;
216        go
217    } {
218        if argv[p].type_ == MN_INTEGER {
219            // c:111
220            if f != 0 {
221                // c:112
222                ret.d += argv[p].l as f64; // c:113
223            } else {
224                ret.l += argv[p].l; // c:115
225            }
226        } else {
227            // c:116
228            if f != 0 {
229                // c:117
230                ret.d += argv[p].d; // c:118
231            } else {
232                // c:119
233                ret.d = (ret.l as f64) + argv[p].d; // c:120
234                f = 1; // c:121
235            }
236        }
237        p += 1; // c:124 argv++
238    }
239    // c:133 — `ret.type = (f ? MN_FLOAT : MN_INTEGER);`
240    ret.type_ = if f != 0 { MN_FLOAT } else { MN_INTEGER };
241    ret // c:133
242}
243
244// =====================================================================
245// math_length(UNUSED(char *name), char *arg, UNUSED(int id))         c:133
246// =====================================================================
247
248/// Port of `math_length(UNUSED(char *name), char *arg, UNUSED(int id))` from `Src/Modules/example.c:133`.
249#[allow(unused_variables)]
250pub fn math_length(name: &str, arg: &str, id: i32) -> mnumber {
251    // c:133
252    // c:133 — `mnumber ret;`
253    // c:137 — `ret.type = MN_INTEGER;`
254    // c:138 — `ret.u.l = strlen(arg);`
255    mnumber {
256        type_: MN_INTEGER,
257        l: arg.len() as i64,
258        d: 0.0,
259    }
260}
261
262// =====================================================================
263// ex_wrapper(Eprog prog, FuncWrap w, char *name)                     c:145
264// =====================================================================
265
266/// Port of `ex_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/example.c:145`.
267///
268/// `Eprog` and `FuncWrap` are `Box<eprog>` / `Box<funcwrap>` per
269/// zsh.h:774 / 522. Pointer-shape preserved as `*const eprog` /
270/// `*const funcwrap` since the C body never derefs `prog`/`w` beyond
271/// passing them to `runshfunc`.
272/// WARNING: param names don't match C — Rust=(prog, name) vs C=(prog, w, name)
273pub fn ex_wrapper(
274    prog: *const crate::ported::zsh_h::eprog, // c:145
275    w: *const crate::ported::zsh_h::funcwrap,
276    name: &str,
277) -> i32 {
278    // c:147 — `if (strncmp(name, "example", 7)) return 1;`
279    if !name.starts_with("example") {
280        return 1; // c:148
281    }
282    // c:149-156 — else branch:
283    //   int ogd = opts[GLOBDOTS];
284    //   opts[GLOBDOTS] = 1;
285    //   runshfunc(prog, w, name);
286    //   opts[GLOBDOTS] = ogd;
287    //   return 0;
288    // The opts/runshfunc machinery is the legacy tree-walker funcwrap
289    // dispatcher that fusevm replaces; static-link path is never
290    // invoked through addwrapper, so the body stays a no-op besides
291    // the prefix-match contract.
292    let _ = (prog, w);
293    0 // c:156
294}
295
296// =====================================================================
297// static struct builtin bintab[]                                     c:164
298// static struct conddef cotab[]                                      c:168
299// static struct paramdef patab[]                                     c:173
300// static struct mathfunc mftab[]                                     c:179
301// static struct funcwrap wrapper[]                                   c:184
302// static struct features module_features                             c:188
303//
304// These dispatch tables are consumed by the C module loader
305// (`featuresarray` + `handlefeatures` + `addwrapper`). The
306// static-link / fusevm path doesn't go through that registry; the
307// dispatcher in `src/extensions/` invokes `bin_example` etc.
308// directly. Tables omitted from the Rust port until the module-loader
309// port lands.
310// =====================================================================
311
312// =====================================================================
313// setup_(UNUSED(Module m))                                           c:198
314// =====================================================================
315
316/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/example.c:198`.
317#[allow(unused_variables)]
318pub fn setup_(m: *const module) -> i32 {
319    let mut stdout = std::io::stdout().lock();
320    let _ = writeln!(stdout, "The example module has now been set up."); // c:207
321    let _ = stdout.flush(); // c:207
322    0 // c:207
323}
324
325// =====================================================================
326// features_(Module m, char ***features)                              c:207
327// =====================================================================
328
329/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/example.c:207`.
330/// C body: `*features = featuresarray(m, &module_features); return 0;`
331pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
332    *features = featuresarray(m, module_features());
333    0 // c:215
334}
335
336// =====================================================================
337// enables_(Module m, int **enables)                                  c:215
338// =====================================================================
339
340/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/example.c:215`.
341/// C body: `return handlefeatures(m, &module_features, enables);`
342pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
343    handlefeatures(m, module_features(), enables) // c:222
344}
345
346// =====================================================================
347// boot_(Module m)                                                    c:222
348// =====================================================================
349
350/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/example.c:222`.
351/// C body sets the demo paramdef-bound statics then calls
352/// `addwrapper(m, wrapper)`.
353pub fn boot_(m: *const module) -> i32 {
354    intparam.store(42, Ordering::Relaxed); // c:222
355    *strparam.lock().unwrap() = Some("example".to_string()); // c:225
356    *arrparam.lock().unwrap() = Some(vec![
357        // c:226-228
358        "example".to_string(), // c:227
359        "array".to_string(),   // c:228
360    ]);
361    // c:230 — addwrapper(m, wrapper); registers ex_wrapper into the
362    // global wrappers linked list (Src/module.c:577). zshrs's fusevm
363    // bytecode doesn't run through C's wrapper-dispatch chain, so the
364    // registration is a no-op until the wrapper machinery has a Rust
365    // equivalent.
366    let _ = m;
367    0
368}
369
370// =====================================================================
371// cleanup_(Module m)                                                 c:235
372// =====================================================================
373
374/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/example.c:235`.
375/// C body: `deletewrapper(m, wrapper); return setfeatureenables(m, &module_features, NULL);`
376pub fn cleanup_(m: *const module) -> i32 {
377    // c:243 — deletewrapper(m, wrapper); paired with c:230 addwrapper,
378    // no-op until the wrapper machinery has a Rust equivalent.
379    setfeatureenables(m, module_features(), None) // c:243
380}
381
382// =====================================================================
383// finish_(UNUSED(Module m))                                          c:243
384// =====================================================================
385
386/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/example.c:243`.
387#[allow(unused_variables)]
388pub fn finish_(m: *const module) -> i32 {
389    let mut stdout = std::io::stdout().lock();
390    let _ = writeln!(
391        stdout,
392        "Thank you for using the example module.  Have a nice day."
393    ); // c:245
394    let _ = stdout.flush(); // c:246
395    0 // c:247
396}
397
398// =====================================================================
399// External ported + tables — `static struct funcwrap wrapper[]` (c:184),
400// `static struct features module_features` (c:188), and
401// `featuresarray`/`handlefeatures`/`setfeatureenables`/`addwrapper`/
402// `deletewrapper` from `Src/module.c`.
403// =====================================================================
404
405// `bintab` — port of `static struct builtin bintab[]` (example.c:182).
406
407// `cotab` — port of `static struct conddef cotab[]` (example.c).
408// `CONDDEF("ex", CONDF_INFIX|CONDF_ADDED, …)` + `CONDDEF("len", 0, …)`.
409
410// `mftab` — port of `static struct mathfunc mftab[]` (example.c).
411
412// `patab` — port of `static struct paramdef patab[]` (example.c).
413
414// `module_features` — port of `static struct features module_features`
415// from example.c:188.
416
417static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
418
419// Local stubs for the per-module entry points. C uses generic
420// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
421// 3275/3370/3445) but those take `Builtin` + `Features` pointer
422// fields the Rust port doesn't carry. The hardcoded descriptor
423// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
424// WARNING: NOT IN EXAMPLE.C — Rust-only module-framework shim.
425// C uses generic featuresarray/handlefeatures/setfeatureenables from
426// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
427// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
428fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
429    vec![
430        "b:example".to_string(),
431        "c:ex".to_string(),
432        "c:len".to_string(),
433        "f:length".to_string(),
434        "f:sum".to_string(),
435        "p:exarr".to_string(),
436        "p:exint".to_string(),
437        "p:exstr".to_string(),
438    ]
439}
440
441// WARNING: NOT IN EXAMPLE.C — Rust-only module-framework shim.
442// C uses generic featuresarray/handlefeatures/setfeatureenables from
443// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
444// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
445fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
446    if enables.is_none() {
447        *enables = Some(vec![1; 8]);
448    }
449    0
450}
451
452// WARNING: NOT IN EXAMPLE.C — Rust-only module-framework shim.
453// C uses generic featuresarray/handlefeatures/setfeatureenables from
454// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
455// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
456fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
457    0
458}
459
460// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
461// ─── RUST-ONLY ACCESSORS ───
462//
463// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
464// RwLock<T>>` globals declared above. C zsh uses direct global
465// access; Rust needs these wrappers because `OnceLock::get_or_init`
466// is the only way to lazily construct shared state. These ported sit
467// here so the body of this file reads in C source order without
468// the accessor wrappers interleaved between real port ported.
469// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
470
471// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
472// ─── RUST-ONLY ACCESSORS ───
473//
474// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
475// RwLock<T>>` globals declared above. C zsh uses direct global
476// access; Rust needs these wrappers because `OnceLock::get_or_init`
477// is the only way to lazily construct shared state. These ported sit
478// here so the body of this file reads in C source order without
479// the accessor wrappers interleaved between real port ported.
480// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
481
482// WARNING: NOT IN EXAMPLE.C — Rust-only module-framework shim.
483// C uses generic featuresarray/handlefeatures/setfeatureenables from
484// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
485// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
486fn module_features() -> &'static Mutex<features> {
487    MODULE_FEATURES.get_or_init(|| {
488        Mutex::new(features {
489            bn_list: None,
490            bn_size: 1,
491            cd_list: None,
492            cd_size: 2,
493            mf_list: None,
494            mf_size: 2,
495            pd_list: None,
496            pd_size: 3,
497            n_abstract: 0,
498        })
499    })
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    fn empty_ops() -> options {
507        options {
508            ind: [0u8; MAX_OPS],
509            args: Vec::new(),
510            argscount: 0,
511            argsalloc: 0,
512        }
513    }
514    fn s(x: &str) -> String {
515        x.to_string()
516    }
517
518    /// Serialises tests that mutate `intparam` / `strparam` / `arrparam`
519    /// (paramdef bindings at `Src/Modules/example.c:208-218`). The C
520    /// source declares those as file-scope statics that `boot_` and
521    /// `bin_example` overwrite; zsh is single-threaded so the C source
522    /// is safe. cargo's parallel test runner can fire `boot_populates_demo_params`
523    /// and `bin_example_returns_zero_and_assigns_state` concurrently —
524    /// each then reads the values the other just wrote and fails. The
525    /// lock restores the single-writer assumption for the test phase.
526    static EXAMPLE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
527
528    /// Port of `boot_(UNUSED(Module m))` from `Src/Modules/example.c:222`.
529    /// Verifies `boot_()` populates the three paramdef-bound statics
530    /// per c:224-228: intparam=42, strparam="example",
531    /// arrparam=["example","array"].
532    #[test]
533    fn boot_populates_demo_params() {
534        let _g = crate::test_util::global_state_lock();
535        let _g = EXAMPLE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
536        boot_(std::ptr::null());
537        assert_eq!(intparam.load(Ordering::SeqCst), 42);
538        assert_eq!(strparam.lock().unwrap().as_deref(), Some("example"));
539        let arr = arrparam.lock().unwrap();
540        let arr = arr.as_ref().expect("arrparam must be Some after boot_");
541        assert_eq!(arr.len(), 2);
542        assert_eq!(arr[0], "example");
543        assert_eq!(arr[1], "array");
544    }
545
546    /// Verifies `cond_p_len`'s two arities — c:84/89.
547    #[test]
548    fn cond_p_len_arities() {
549        let _g = crate::test_util::global_state_lock();
550        assert_eq!(cond_p_len(&[s("hello"), s("5")], 0), 1);
551        assert_eq!(cond_p_len(&[s("hello"), s("4")], 0), 0);
552        assert_eq!(cond_p_len(&[s("")], 0), 1);
553        assert_eq!(cond_p_len(&[s("x")], 0), 0);
554    }
555
556    /// Verifies `cond_i_ex` matches only the exact concat "example" — c:99.
557    #[test]
558    fn cond_i_ex_concat_matches_example() {
559        let _g = crate::test_util::global_state_lock();
560        assert_eq!(cond_i_ex(&[s("exam"), s("ple")], 0), 1);
561        assert_eq!(cond_i_ex(&[s("example"), s("")], 0), 1);
562        assert_eq!(cond_i_ex(&[s("example"), s("x")], 0), 0);
563        assert_eq!(cond_i_ex(&[s("foo"), s("bar")], 0), 0);
564    }
565
566    /// Verifies `math_sum` returns integer sum for all-int inputs and
567    /// promotes to float once a float arg appears — c:111/116/126.
568    #[test]
569    fn math_sum_int_then_float_promotion() {
570        let _g = crate::test_util::global_state_lock();
571        let ints = [
572            mnumber {
573                l: 1,
574                d: 0.0,
575                type_: MN_INTEGER,
576            },
577            mnumber {
578                l: 2,
579                d: 0.0,
580                type_: MN_INTEGER,
581            },
582            mnumber {
583                l: 3,
584                d: 0.0,
585                type_: MN_INTEGER,
586            },
587        ];
588        let r = math_sum("sum", 3, &ints, 0);
589        assert_eq!(r.type_, MN_INTEGER);
590        assert_eq!(r.l, 6);
591
592        let mixed = [
593            mnumber {
594                l: 1,
595                d: 0.0,
596                type_: MN_INTEGER,
597            },
598            mnumber {
599                l: 0,
600                d: 2.5,
601                type_: MN_FLOAT,
602            },
603            mnumber {
604                l: 3,
605                d: 0.0,
606                type_: MN_INTEGER,
607            },
608        ];
609        let r = math_sum("sum", 3, &mixed, 0);
610        assert_eq!(r.type_, MN_FLOAT);
611        assert!((r.d - 6.5).abs() < 1e-9);
612    }
613
614    /// Verifies `math_length` returns string length as integer — c:138.
615    #[test]
616    fn math_length_returns_strlen() {
617        let _g = crate::test_util::global_state_lock();
618        let r = math_length("length", "hello", 0);
619        assert_eq!(r.type_, MN_INTEGER);
620        assert_eq!(r.l, 5);
621    }
622
623    /// Verifies `ex_wrapper` returns 1 (skip) for non-matching names
624    /// and 0 (matched) for `example`-prefixed names — c:147/156.
625    #[test]
626    fn ex_wrapper_name_prefix_match() {
627        let _g = crate::test_util::global_state_lock();
628        assert_eq!(ex_wrapper(std::ptr::null(), std::ptr::null(), "foo"), 1);
629        assert_eq!(ex_wrapper(std::ptr::null(), std::ptr::null(), "exampl"), 1);
630        assert_eq!(ex_wrapper(std::ptr::null(), std::ptr::null(), "example"), 0);
631        assert_eq!(
632            ex_wrapper(std::ptr::null(), std::ptr::null(), "example_func"),
633            0
634        );
635    }
636
637    /// Port of `bin_example(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/example.c:42`.
638    /// Verifies `bin_example` reads `OPT_ISSET(ops, c)` and prints flagged
639    /// option letters — c:49-51.
640    #[test]
641    fn bin_example_returns_zero_and_assigns_state() {
642        let _g = crate::test_util::global_state_lock();
643        let _g = EXAMPLE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
644        let ops = empty_ops();
645        let args = vec![s("hello"), s("world")];
646        let rc = bin_example("example", &args, &ops, 0);
647        assert_eq!(rc, 0);
648        // c:69 i = 2 (two args); c:71 strparam = "hello"; c:74 arrparam = ["hello","world"]
649        assert_eq!(intparam.load(Ordering::Relaxed), 2);
650        assert_eq!(strparam.lock().unwrap().as_deref(), Some("hello"));
651        let arr = arrparam.lock().unwrap();
652        assert_eq!(arr.as_ref().unwrap().as_slice(), &[s("hello"), s("world")]);
653    }
654
655    /// c:104 — `math_sum` with zero args returns identity (0). Pin
656    /// the empty-input arithmetic identity.
657    #[test]
658    fn math_sum_zero_args_returns_zero() {
659        let _g = crate::test_util::global_state_lock();
660        let r = math_sum("sum", 0, &[], 0);
661        assert_eq!(r.type_, MN_INTEGER);
662        assert_eq!(r.l, 0, "sum of nothing must be 0");
663    }
664
665    /// c:104 — `math_sum` with a single integer is identity.
666    #[test]
667    fn math_sum_single_int_arg_is_identity() {
668        let _g = crate::test_util::global_state_lock();
669        let argv = [mnumber {
670            l: 42,
671            d: 0.0,
672            type_: MN_INTEGER,
673        }];
674        let r = math_sum("sum", 1, &argv, 0);
675        assert_eq!(r.type_, MN_INTEGER);
676        assert_eq!(r.l, 42);
677    }
678
679    /// c:104 — All-float input stays MN_FLOAT (no downcast). Pin
680    /// the float-preservation rule because a regen that prefers
681    /// integer "for tidiness" would silently truncate fractions.
682    #[test]
683    fn math_sum_all_floats_preserves_float_type() {
684        let _g = crate::test_util::global_state_lock();
685        let argv = [
686            mnumber {
687                l: 0,
688                d: 1.5,
689                type_: MN_FLOAT,
690            },
691            mnumber {
692                l: 0,
693                d: 2.5,
694                type_: MN_FLOAT,
695            },
696        ];
697        let r = math_sum("sum", 2, &argv, 0);
698        assert_eq!(r.type_, MN_FLOAT);
699        assert!((r.d - 4.0).abs() < 1e-9);
700    }
701
702    /// c:104 — Negative integers preserved.
703    #[test]
704    fn math_sum_handles_negative_ints() {
705        let _g = crate::test_util::global_state_lock();
706        let argv = [
707            mnumber {
708                l: -5,
709                d: 0.0,
710                type_: MN_INTEGER,
711            },
712            mnumber {
713                l: 3,
714                d: 0.0,
715                type_: MN_INTEGER,
716            },
717        ];
718        let r = math_sum("sum", 2, &argv, 0);
719        assert_eq!(r.type_, MN_INTEGER);
720        assert_eq!(r.l, -2, "−5 + 3 = −2");
721    }
722
723    /// c:133 — `math_length` on empty string returns 0. Pin so a
724    /// regen that adds `+ 1` for a NUL terminator gets caught.
725    #[test]
726    fn math_length_empty_string_returns_zero() {
727        let _g = crate::test_util::global_state_lock();
728        let r = math_length("length", "", 0);
729        assert_eq!(r.type_, MN_INTEGER);
730        assert_eq!(r.l, 0);
731    }
732
733    /// c:133 — Multi-byte UTF-8 yields BYTE count, not char count
734    /// (zsh's strlen semantics).
735    #[test]
736    fn math_length_multibyte_returns_byte_count() {
737        let _g = crate::test_util::global_state_lock();
738        // "café" = "caf" + "é" (é = 2 bytes in UTF-8) = 5 bytes
739        let r = math_length("length", "café", 0);
740        assert_eq!(
741            r.l, 5,
742            "math_length must count bytes, not chars — got {}",
743            r.l
744        );
745    }
746
747    /// c:95 — `cond_i_ex` (no-arg demo) returns 0 (false). Pin so a
748    /// regen that returns 1 (true) silently inverts every `[[ -i-ex ]]`.
749    #[test]
750    fn cond_i_ex_returns_zero() {
751        let _g = crate::test_util::global_state_lock();
752        let r = cond_i_ex(&[], 0);
753        assert_eq!(r, 0, "cond_i_ex demo condition must return 0 (false)");
754    }
755
756    /// c:286-335 — module-lifecycle stubs return 0.
757    #[test]
758    fn module_lifecycle_shims_all_return_zero() {
759        let _g = crate::test_util::global_state_lock();
760        let m: *const module = std::ptr::null();
761        assert_eq!(setup_(m), 0);
762        assert_eq!(boot_(m), 0);
763        assert_eq!(cleanup_(m), 0);
764        assert_eq!(finish_(m), 0);
765    }
766
767    // ─── zsh-corpus pins for math_sum / math_length ────────────────
768
769    /// `math_sum` of zero args returns integer 0.
770    #[test]
771    fn example_corpus_math_sum_empty_is_zero_integer() {
772        let _g = crate::test_util::global_state_lock();
773        let r = math_sum("sum", 0, &[], 0);
774        assert_eq!(r.l, 0);
775        assert_eq!(r.type_, MN_INTEGER, "empty sum stays integer-typed");
776    }
777
778    /// `math_sum` of three integers stays integer-typed.
779    #[test]
780    fn example_corpus_math_sum_int_args_stay_integer() {
781        let _g = crate::test_util::global_state_lock();
782        let args = vec![
783            mnumber {
784                l: 1,
785                d: 0.0,
786                type_: MN_INTEGER,
787            },
788            mnumber {
789                l: 2,
790                d: 0.0,
791                type_: MN_INTEGER,
792            },
793            mnumber {
794                l: 3,
795                d: 0.0,
796                type_: MN_INTEGER,
797            },
798        ];
799        let r = math_sum("sum", 3, &args, 0);
800        assert_eq!(r.l, 6, "1+2+3=6");
801        assert_eq!(r.type_, MN_INTEGER);
802    }
803
804    /// `math_sum` mixing int + float promotes to float per c:121.
805    #[test]
806    fn example_corpus_math_sum_mixed_promotes_to_float() {
807        let _g = crate::test_util::global_state_lock();
808        let args = vec![
809            mnumber {
810                l: 2,
811                d: 0.0,
812                type_: MN_INTEGER,
813            },
814            mnumber {
815                l: 0,
816                d: 1.5,
817                type_: MN_FLOAT,
818            },
819        ];
820        let r = math_sum("sum", 2, &args, 0);
821        assert_eq!(r.type_, MN_FLOAT, "int+float → float");
822        assert!((r.d - 3.5).abs() < 1e-9);
823    }
824
825    /// `math_sum` of two floats stays float-typed.
826    #[test]
827    fn example_corpus_math_sum_two_floats() {
828        let _g = crate::test_util::global_state_lock();
829        let args = vec![
830            mnumber {
831                l: 0,
832                d: 1.25,
833                type_: MN_FLOAT,
834            },
835            mnumber {
836                l: 0,
837                d: 2.75,
838                type_: MN_FLOAT,
839            },
840        ];
841        let r = math_sum("sum", 2, &args, 0);
842        assert_eq!(r.type_, MN_FLOAT);
843        assert!((r.d - 4.0).abs() < 1e-9);
844    }
845
846    /// `math_length("hello")` returns 5 as integer.
847    #[test]
848    fn example_corpus_math_length_ascii_byte_count() {
849        let _g = crate::test_util::global_state_lock();
850        let r = math_length("length", "hello", 0);
851        assert_eq!(r.l, 5);
852        assert_eq!(r.type_, MN_INTEGER);
853    }
854
855    /// `math_length("")` returns 0.
856    #[test]
857    fn example_corpus_math_length_empty_is_zero() {
858        let _g = crate::test_util::global_state_lock();
859        let r = math_length("length", "", 0);
860        assert_eq!(r.l, 0);
861    }
862
863    /// `math_length` counts UTF-8 BYTES not codepoints (per C strlen).
864    #[test]
865    fn example_corpus_math_length_multibyte_byte_count() {
866        let _g = crate::test_util::global_state_lock();
867        // '日' is 3 bytes in UTF-8.
868        let r = math_length("length", "日", 0);
869        assert_eq!(r.l, 3, "C strlen-style byte count for UTF-8");
870    }
871
872    /// `cond_p_len(["abc"], 0)` returns 0 (the demo cond is len-3 check).
873    /// Pin existing behavior — `cond_p_len` evaluates len == 3 → "false"
874    /// (per zsh's cond return convention: 0 = true, 1 = false).
875    #[test]
876    fn example_corpus_cond_p_len_does_not_panic() {
877        let _g = crate::test_util::global_state_lock();
878        let _ = cond_p_len(&["abc".to_string()], 0);
879        // Just pin: no panic.
880    }
881
882    // ═══════════════════════════════════════════════════════════════════
883    // Additional C-parity tests for Src/Modules/example.c.
884    // ═══════════════════════════════════════════════════════════════════
885
886    /// c:80-95 — `cond_p_len([""], 0)` with single empty-string arg:
887    /// no second arg, so c:95 branch fires `return !s1[0]` = !"" = 1.
888    #[test]
889    fn cond_p_len_empty_single_arg_returns_one() {
890        let _g = crate::test_util::global_state_lock();
891        let r = cond_p_len(&["".to_string()], 0);
892        assert_eq!(r, 1, "empty s1 with no len arg → !s1[0] = 1");
893    }
894
895    /// c:80-87 — `cond_p_len([s, "N"])` returns 1 when len(s) == N,
896    /// 0 otherwise.
897    #[test]
898    fn cond_p_len_length_match_returns_one() {
899        let _g = crate::test_util::global_state_lock();
900        // s1="abc" (len=3), arg[1]="3" → 1.
901        let r = cond_p_len(&["abc".to_string(), "3".to_string()], 0);
902        assert_eq!(r, 1, "len(abc) == 3 → 1");
903    }
904
905    /// c:80-87 — len mismatch returns 0.
906    #[test]
907    fn cond_p_len_length_mismatch_returns_zero() {
908        let _g = crate::test_util::global_state_lock();
909        let r = cond_p_len(&["abc".to_string(), "5".to_string()], 0);
910        assert_eq!(r, 0, "len(abc) != 5 → 0");
911    }
912
913    /// c:95-104 — `cond_i_ex(["exam", "ple"])` returns 1
914    /// (dyncat == "example").
915    #[test]
916    fn cond_i_ex_concatenation_to_example_returns_one() {
917        let _g = crate::test_util::global_state_lock();
918        let r = cond_i_ex(&["exam".to_string(), "ple".to_string()], 0);
919        assert_eq!(r, 1, "exam+ple = 'example' → 1");
920    }
921
922    /// c:95-104 — non-matching concat returns 0.
923    #[test]
924    fn cond_i_ex_non_matching_concat_returns_zero() {
925        let _g = crate::test_util::global_state_lock();
926        let r = cond_i_ex(&["foo".to_string(), "bar".to_string()], 0);
927        assert_eq!(r, 0, "foo+bar = 'foobar' != 'example' → 0");
928    }
929
930    /// c:95-104 — empty inputs (dyncat("","") = "" != "example") → 0.
931    #[test]
932    fn cond_i_ex_empty_inputs_returns_zero() {
933        let _g = crate::test_util::global_state_lock();
934        let r = cond_i_ex(&["".to_string(), "".to_string()], 0);
935        assert_eq!(r, 0, "'' != 'example' → 0");
936    }
937
938    /// c:104 — `math_sum([])` with empty args returns 0 (integer init).
939    #[test]
940    fn math_sum_empty_returns_zero() {
941        let _g = crate::test_util::global_state_lock();
942        let r = math_sum("sum", 0, &[], 0);
943        assert_eq!(r.l, 0);
944        assert_eq!(r.type_, MN_INTEGER);
945    }
946
947    /// c:104-115 — `math_sum` of two integers returns integer sum.
948    #[test]
949    fn math_sum_two_integers_returns_integer_sum() {
950        let _g = crate::test_util::global_state_lock();
951        let args = vec![
952            mnumber {
953                l: 3,
954                d: 0.0,
955                type_: MN_INTEGER,
956            },
957            mnumber {
958                l: 4,
959                d: 0.0,
960                type_: MN_INTEGER,
961            },
962        ];
963        let r = math_sum("sum", 2, &args, 0);
964        assert_eq!(r.l, 7, "3 + 4 = 7");
965        assert_eq!(r.type_, MN_INTEGER);
966    }
967
968    /// c:250 — `math_length` of empty string returns 0.
969    #[test]
970    fn math_length_empty_returns_zero_pin() {
971        let _g = crate::test_util::global_state_lock();
972        let r = math_length("length", "", 0);
973        assert_eq!(r.l, 0);
974    }
975
976    /// c:250 — `math_length` returns byte count (not codepoints).
977    #[test]
978    fn math_length_ascii_returns_byte_count() {
979        let _g = crate::test_util::global_state_lock();
980        let r = math_length("length", "hello", 0);
981        assert_eq!(r.l, 5);
982    }
983
984    // ═══════════════════════════════════════════════════════════════════
985    // Additional C-parity tests for Src/Modules/example.c
986    // c:70 bin_example / c:149 cond_p_len / c:179 cond_i_ex
987    // c:198 math_sum / c:250 math_length / c:273 ex_wrapper / lifecycle
988    // ═══════════════════════════════════════════════════════════════════
989
990    /// c:149 — cond_p_len returns 0/1 only (boolean i32).
991    #[test]
992    fn cond_p_len_return_value_in_boolean_set() {
993        let _g = crate::test_util::global_state_lock();
994        for args in [
995            vec![s(""), s("")],
996            vec![s("abc"), s("3")],
997            vec![s("abc"), s("4")],
998            vec![s(""), s("0")],
999        ] {
1000            let r = cond_p_len(&args, 0);
1001            assert!(
1002                r == 0 || r == 1,
1003                "cond_p_len({:?}) = {} not in {{0,1}}",
1004                args,
1005                r
1006            );
1007        }
1008    }
1009
1010    /// c:179 — cond_i_ex returns 0/1 only (boolean i32).
1011    #[test]
1012    fn cond_i_ex_return_value_in_boolean_set() {
1013        let _g = crate::test_util::global_state_lock();
1014        for args in [
1015            vec![s(""), s("")],
1016            vec![s("ex"), s("ample")],
1017            vec![s("foo"), s("bar")],
1018        ] {
1019            let r = cond_i_ex(&args, 0);
1020            assert!(
1021                r == 0 || r == 1,
1022                "cond_i_ex({:?}) = {} not in {{0,1}}",
1023                args,
1024                r
1025            );
1026        }
1027    }
1028
1029    /// c:198 — math_sum returns mnumber type (compile-time pinning).
1030    #[test]
1031    fn math_sum_returns_mnumber_type() {
1032        let _g = crate::test_util::global_state_lock();
1033        let _: mnumber = math_sum("sum", 0, &[], 0);
1034    }
1035
1036    /// c:250 — math_length returns mnumber type.
1037    #[test]
1038    fn math_length_returns_mnumber_type() {
1039        let _g = crate::test_util::global_state_lock();
1040        let _: mnumber = math_length("length", "x", 0);
1041    }
1042
1043    /// c:250 — `math_length` is deterministic for any input.
1044    #[test]
1045    fn math_length_is_deterministic() {
1046        let _g = crate::test_util::global_state_lock();
1047        let first = math_length("length", "hello", 0).l;
1048        for _ in 0..5 {
1049            assert_eq!(math_length("length", "hello", 0).l, first);
1050        }
1051    }
1052
1053    /// c:70 — bin_example empty args returns 0 (default success).
1054    #[test]
1055    fn bin_example_no_args_returns_zero_or_one() {
1056        let _g = crate::test_util::global_state_lock();
1057        let ops = empty_ops();
1058        let r = bin_example("example", &[], &ops, 0);
1059        assert!(r == 0 || r == 1, "result must be 0 or 1, got {}", r);
1060    }
1061
1062    /// c:198 — `math_sum(empty)` returns integer 0 (l=0, type integer).
1063    #[test]
1064    fn math_sum_empty_is_canonical_zero_integer() {
1065        let _g = crate::test_util::global_state_lock();
1066        let r = math_sum("sum", 0, &[], 0);
1067        assert_eq!(r.l, 0, "empty args → l=0");
1068    }
1069
1070    /// c:273 — `ex_wrapper(null, null, "")` is safe (empty name).
1071    #[test]
1072    fn ex_wrapper_empty_name_no_panic() {
1073        let _g = crate::test_util::global_state_lock();
1074        let _ = ex_wrapper(std::ptr::null(), std::ptr::null(), "");
1075    }
1076
1077    /// c:318-388 — full lifecycle setup→features→enables→boot→cleanup→finish.
1078    #[test]
1079    fn example_full_lifecycle_returns_zero_for_all() {
1080        let _g = crate::test_util::global_state_lock();
1081        let null = std::ptr::null();
1082        assert_eq!(setup_(null), 0);
1083        let mut feats = Vec::new();
1084        let _ = features_(null, &mut feats);
1085        let mut enables: Option<Vec<i32>> = None;
1086        let _ = enables_(null, &mut enables);
1087        assert_eq!(boot_(null), 0);
1088        assert_eq!(cleanup_(null), 0);
1089        assert_eq!(finish_(null), 0);
1090    }
1091
1092    /// c:318 — setup_ idempotent.
1093    #[test]
1094    fn example_setup_idempotent() {
1095        let _g = crate::test_util::global_state_lock();
1096        for _ in 0..10 {
1097            assert_eq!(setup_(std::ptr::null()), 0);
1098        }
1099    }
1100
1101    // ═══════════════════════════════════════════════════════════════════
1102    // Additional C-parity tests for Src/Modules/example.c
1103    // c:149 cond_p_len / c:179 cond_i_ex / c:198 math_sum / c:250 math_length
1104    // ═══════════════════════════════════════════════════════════════════
1105
1106    /// c:149 — `cond_p_len` returns i32 (compile-time pin).
1107    #[test]
1108    fn cond_p_len_returns_i32_type() {
1109        let _g = crate::test_util::global_state_lock();
1110        let _: i32 = cond_p_len(&[], 0);
1111    }
1112
1113    /// c:149 — `cond_p_len([empty])` (single empty-string arg) returns 1
1114    /// per c:95 `return !s1[0]`.
1115    #[test]
1116    fn cond_p_len_single_empty_returns_one() {
1117        let _g = crate::test_util::global_state_lock();
1118        let r = cond_p_len(&["".to_string()], 0);
1119        assert_eq!(r, 1, "single empty-arg case must match `!s1[0]` → 1");
1120    }
1121
1122    /// c:149 — return value is boolean 0 or 1 only.
1123    #[test]
1124    fn cond_p_len_returns_boolean_i32_only() {
1125        let _g = crate::test_util::global_state_lock();
1126        for a in [
1127            vec!["".to_string()],
1128            vec!["hello".to_string()],
1129            vec!["hello".to_string(), "5".to_string()],
1130            vec!["hello".to_string(), "0".to_string()],
1131        ] {
1132            let r = cond_p_len(&a, 0);
1133            assert!(
1134                r == 0 || r == 1,
1135                "cond_p_len({:?}) must return 0 or 1; got {}",
1136                a,
1137                r
1138            );
1139        }
1140    }
1141
1142    /// c:179 — `cond_i_ex` returns i32 (compile-time pin).
1143    #[test]
1144    fn cond_i_ex_returns_i32_type() {
1145        let _g = crate::test_util::global_state_lock();
1146        let _: i32 = cond_i_ex(&["".to_string(), "".to_string()], 0);
1147    }
1148
1149    /// c:179 — `cond_i_ex(["exam", "ple"])` concat = "example" → returns 1.
1150    #[test]
1151    fn cond_i_ex_split_example_returns_one() {
1152        let _g = crate::test_util::global_state_lock();
1153        let r = cond_i_ex(&["exam".to_string(), "ple".to_string()], 0);
1154        assert_eq!(r, 1, "exam+ple = example → 1");
1155    }
1156
1157    /// c:179 — `cond_i_ex(["foo", "bar"])` concat ≠ "example" → returns 0.
1158    #[test]
1159    fn cond_i_ex_mismatch_returns_zero() {
1160        let _g = crate::test_util::global_state_lock();
1161        let r = cond_i_ex(&["foo".to_string(), "bar".to_string()], 0);
1162        assert_eq!(r, 0, "foo+bar ≠ example → 0");
1163    }
1164
1165    /// c:198 — `math_sum` of integer arguments yields MN_INTEGER.
1166    #[test]
1167    fn math_sum_all_integers_returns_integer_type() {
1168        let _g = crate::test_util::global_state_lock();
1169        let argv = vec![
1170            mnumber {
1171                l: 3,
1172                d: 0.0,
1173                type_: MN_INTEGER,
1174            },
1175            mnumber {
1176                l: 5,
1177                d: 0.0,
1178                type_: MN_INTEGER,
1179            },
1180        ];
1181        let r = math_sum("sum", 2, &argv, 0);
1182        assert_eq!(r.type_, MN_INTEGER, "all-int sum must be MN_INTEGER");
1183        assert_eq!(r.l, 8, "3+5=8");
1184    }
1185
1186    /// c:198 — `math_sum` with any float arg promotes result to MN_FLOAT.
1187    #[test]
1188    fn math_sum_mixed_promotes_to_float() {
1189        let _g = crate::test_util::global_state_lock();
1190        let argv = vec![
1191            mnumber {
1192                l: 3,
1193                d: 0.0,
1194                type_: MN_INTEGER,
1195            },
1196            mnumber {
1197                l: 0,
1198                d: 2.5,
1199                type_: MN_FLOAT,
1200            },
1201        ];
1202        let r = math_sum("sum", 2, &argv, 0);
1203        assert_eq!(
1204            r.type_, MN_FLOAT,
1205            "mixed int+float must promote to MN_FLOAT"
1206        );
1207        assert!((r.d - 5.5).abs() < 1e-9, "3+2.5 = 5.5, got {}", r.d);
1208    }
1209
1210    /// c:250 — `math_length("")` returns l=0 + MN_INTEGER (combined pin, alt name).
1211    #[test]
1212    fn math_length_empty_string_returns_zero_and_int_type() {
1213        let _g = crate::test_util::global_state_lock();
1214        let r = math_length("length", "", 0);
1215        assert_eq!(r.l, 0, "len('') = 0");
1216        assert_eq!(r.type_, MN_INTEGER, "math_length is always MN_INTEGER");
1217    }
1218
1219    /// c:250 — `math_length(arg)` matches arg.len() (byte length per
1220    /// c:138 `strlen(arg)`).
1221    #[test]
1222    fn math_length_matches_byte_length_for_ascii() {
1223        let _g = crate::test_util::global_state_lock();
1224        for s in ["x", "hello", "a longer string here"] {
1225            let r = math_length("length", s, 0);
1226            assert_eq!(
1227                r.l,
1228                s.len() as i64,
1229                "math_length({:?}) must equal byte length {}",
1230                s,
1231                s.len()
1232            );
1233        }
1234    }
1235
1236    /// c:198 — `math_sum` empty argv is canonically integer-typed
1237    /// (`f=0` path at c:133 → `type_ = MN_INTEGER`).
1238    #[test]
1239    fn math_sum_empty_is_integer_type() {
1240        let _g = crate::test_util::global_state_lock();
1241        let r = math_sum("sum", 0, &[], 0);
1242        assert_eq!(
1243            r.type_, MN_INTEGER,
1244            "empty sum must be MN_INTEGER (f stayed 0)"
1245        );
1246    }
1247
1248    // ═══════════════════════════════════════════════════════════════════
1249    // Additional C-parity pins for Src/Modules/example.c
1250    // c:70 bin_example / c:149 cond_p_len / c:179 cond_i_ex /
1251    // c:198 math_sum / c:250 math_length / c:318-388 lifecycle hooks
1252    // ═══════════════════════════════════════════════════════════════════
1253
1254    /// c:198 — `math_sum` empty argv: l field is 0.
1255    #[test]
1256    fn math_sum_empty_l_is_zero() {
1257        let _g = crate::test_util::global_state_lock();
1258        let r = math_sum("sum", 0, &[], 0);
1259        assert_eq!(r.l, 0, "empty sum's integer field must be 0");
1260    }
1261
1262    /// c:250 — `math_length("")` empty string has length 0.
1263    #[test]
1264    fn math_length_empty_string_is_zero() {
1265        let _g = crate::test_util::global_state_lock();
1266        let r = math_length("length", "", 0);
1267        assert_eq!(r.l, 0);
1268        assert_eq!(r.type_, MN_INTEGER);
1269    }
1270
1271    /// c:149 — `cond_p_len("")` empty operand returns boolean i32.
1272    #[test]
1273    fn cond_p_len_empty_operand_returns_i32() {
1274        let _g = crate::test_util::global_state_lock();
1275        let _: i32 = cond_p_len(&["".to_string()], 0);
1276    }
1277
1278    /// c:179 — `cond_i_ex` empty args returns i32 (alt).
1279    #[test]
1280    fn cond_i_ex_returns_i32_type_alt() {
1281        let _g = crate::test_util::global_state_lock();
1282        let _: i32 = cond_i_ex(&[], 0);
1283    }
1284
1285    /// c:70 — `bin_example` empty args non-negative.
1286    #[test]
1287    fn bin_example_empty_args_non_negative() {
1288        let _g = crate::test_util::global_state_lock();
1289        let ops = crate::ported::zsh_h::options {
1290            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1291            args: Vec::new(),
1292            argscount: 0,
1293            argsalloc: 0,
1294        };
1295        let r = bin_example("example", &[], &ops, 0);
1296        assert!(r >= 0, "bin_example empty must be ≥ 0, got {}", r);
1297    }
1298
1299    /// c:70 — `bin_example` various func values don't panic.
1300    #[test]
1301    fn bin_example_various_func_values_no_panic() {
1302        let _g = crate::test_util::global_state_lock();
1303        let ops = crate::ported::zsh_h::options {
1304            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1305            args: Vec::new(),
1306            argscount: 0,
1307            argsalloc: 0,
1308        };
1309        for func in [-1, 0, 1, 100, i32::MAX] {
1310            let _ = bin_example("example", &[], &ops, func);
1311        }
1312    }
1313
1314    /// c:198 — `math_sum` is pure (same input → same output).
1315    #[test]
1316    fn math_sum_is_deterministic_for_empty_argv() {
1317        let _g = crate::test_util::global_state_lock();
1318        let r1 = math_sum("sum", 0, &[], 0);
1319        let r2 = math_sum("sum", 0, &[], 0);
1320        assert_eq!(r1.l, r2.l);
1321        assert_eq!(r1.type_, r2.type_);
1322    }
1323
1324    /// c:250 — `math_length` is pure across calls (alt).
1325    #[test]
1326    fn math_length_is_deterministic_alt() {
1327        let _g = crate::test_util::global_state_lock();
1328        for s in ["", "x", "hello"] {
1329            let a = math_length("length", s, 0);
1330            let b = math_length("length", s, 0);
1331            assert_eq!(a.l, b.l);
1332            assert_eq!(a.type_, b.type_);
1333        }
1334    }
1335
1336    /// c:318 — `setup_` is idempotent.
1337    #[test]
1338    fn example_setup_idempotent_alt_pin() {
1339        let _g = crate::test_util::global_state_lock();
1340        for _ in 0..10 {
1341            assert_eq!(setup_(std::ptr::null()), 0);
1342        }
1343    }
1344
1345    /// c:376 — `cleanup_` is idempotent.
1346    #[test]
1347    fn example_cleanup_idempotent_alt_pin() {
1348        let _g = crate::test_util::global_state_lock();
1349        for _ in 0..10 {
1350            assert_eq!(cleanup_(std::ptr::null()), 0);
1351        }
1352    }
1353
1354    /// c:388 — `finish_` is idempotent.
1355    #[test]
1356    fn example_finish_idempotent_alt_pin() {
1357        let _g = crate::test_util::global_state_lock();
1358        for _ in 0..10 {
1359            assert_eq!(finish_(std::ptr::null()), 0);
1360        }
1361    }
1362
1363    /// c:149 — `cond_p_len` is pure across calls.
1364    #[test]
1365    fn cond_p_len_is_deterministic() {
1366        let _g = crate::test_util::global_state_lock();
1367        let args = vec!["hi".to_string()];
1368        for _ in 0..3 {
1369            let a = cond_p_len(&args, 0);
1370            let b = cond_p_len(&args, 0);
1371            assert_eq!(a, b, "cond_p_len must be pure");
1372        }
1373    }
1374}