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::Mutex;
32use std::sync::OnceLock;
33use std::sync::atomic::{AtomicI64, Ordering};
34
35use crate::ported::compat::output64;
36use crate::ported::cond::{cond_str, cond_val};
37use crate::ported::math::{mnumber, MN_FLOAT, MN_INTEGER};
38use crate::ported::string::dyncat;
39use crate::ported::zsh_h::{module, options, Eprog, FuncWrap, OPT_ISSET};
40
41// =====================================================================
42// /* parameters */                                                  c:33
43// =====================================================================
44
45/// Port of `static zlong intparam;` from `Src/Modules/example.c:35`.
46/// Bound to the `exint` integer paramdef at c:175.
47pub static intparam: AtomicI64 = AtomicI64::new(0);                  // c:35
48
49/// Port of `static char *strparam;` from `Src/Modules/example.c:36`.
50/// Bound to the `exstr` string paramdef at c:176. `None` mirrors C's
51/// initial NULL which `bin_example` prints as the empty string at c:63.
52pub static strparam: Mutex<Option<String>> = Mutex::new(None);       // c:36
53
54/// Port of `static char **arrparam;` from `Src/Modules/example.c:37`.
55/// Bound to the `exarr` array paramdef at c:174. `None` mirrors C's
56/// initial NULL.
57pub static arrparam: Mutex<Option<Vec<String>>> = Mutex::new(None);  // c:37
58
59// =====================================================================
60// bin_example(char *nam, char **args, Options ops, UNUSED(int func))  c:42
61// =====================================================================
62
63/// Port of `bin_example(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/example.c:42`.
64///
65/// C signature mirrored verbatim:
66/// ```c
67/// static int
68/// bin_example(char *nam, char **args, Options ops, UNUSED(int func))
69/// ```
70#[allow(unused_variables)]
71pub fn bin_example(nam: &str, args: &[String], ops: &options, func: i32) -> i32 { // 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 { break; }
87        if OPT_ISSET(ops, c) {                                        // c:50
88            let _ = write!(stdout, "{}", c as char);                  // c:51
89        }
90    }
91    let _ = write!(stdout, "\nArguments:");                           // c:52
92    // c:53-56 — `for (; *args; i++, args++) { putchar(' '); fputs(*args, stdout); }`
93    for a in args {
94        let _ = write!(stdout, " ");                                  // c:54
95        let _ = write!(stdout, "{}", a);                              // c:55
96        i += 1;                                                       // c:53
97    }
98    let _ = writeln!(stdout, "\nName: {}", nam);                      // c:57
99    // c:58-62 — `#ifdef ZSH_64_BIT_TYPE` branch is taken on every
100    // modern platform; port that branch.
101    let _ = writeln!(
102        stdout,
103        "\nInteger Parameter: {}",
104        output64(intparam.load(Ordering::Relaxed))
105    );                                                                 // c:59
106    {
107        let sp = strparam.lock().unwrap();
108        let _ = writeln!(
109            stdout,
110            "String Parameter: {}",
111            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() {                               // c:65 if (p)
118            for s in arr {                                            // c:66 while (*p)
119                let _ = write!(stdout, " {}", s);                     // c:66
120            }
121        }
122    }
123    let _ = writeln!(stdout);                                         // c:67
124
125    intparam.store(i, Ordering::Relaxed);                             // c:69 intparam = i;
126    // c:70 — zsfree(strparam);
127    // c:71 — strparam = ztrdup(*oargs ? *oargs : "");
128    let new_sp = if let Some(first) = oargs.first() {
129        first.clone()
130    } else {
131        String::new()
132    };
133    *strparam.lock().unwrap() = Some(new_sp);                         // c:71
134    // c:72-74 — if (arrparam) freearray(arrparam); arrparam = zarrdup(oargs);
135    let new_ap: Vec<String> = oargs.to_vec();                          // c:80
136    *arrparam.lock().unwrap() = Some(new_ap);                          // c:80
137
138    0                                                                 // c:80
139}
140
141// =====================================================================
142// cond_p_len(char **a, UNUSED(int id))                               c:80
143// =====================================================================
144
145/// Port of `cond_p_len(char **a, UNUSED(int id))` from `Src/Modules/example.c:80`.
146#[allow(unused_variables)]
147pub fn cond_p_len(a: &[String], id: i32) -> i32 {                           // c:80
148    // c:80 — `char *s1 = cond_str(a, 0, 0);`
149    let s1: String = cond_str(a, 0, false);                            // c:82
150    if a.len() > 1 {                                                   // c:84 if (a[1])
151        let v: i64 = cond_val(a, 1);                                   // c:85 zlong v = cond_val(a, 1);
152        // c:87 — `return strlen(s1) == v;`
153        if (s1.len() as i64) == v { 1 } else { 0 }
154    } else {                                                           // c:95
155        // c:95 — `return !s1[0];`
156        if s1.is_empty() { 1 } else { 0 }
157    }
158}
159
160// =====================================================================
161// cond_i_ex(char **a, UNUSED(int id))                                c:95
162// =====================================================================
163
164/// Port of `cond_i_ex(char **a, UNUSED(int id))` from `Src/Modules/example.c:95`.
165#[allow(unused_variables)]
166pub fn cond_i_ex(a: &[String], id: i32) -> i32 {                            // c:95
167    // c:95 — `char *s1 = cond_str(a, 0, 0), *s2 = cond_str(a, 1, 0);`
168    let s1: String = cond_str(a, 0, false);                            // c:104
169    let s2: String = cond_str(a, 1, false);                            // c:104
170    // c:104 — `return !strcmp("example", dyncat(s1, s2));`
171    if dyncat(&s1, &s2) == "example" { 1 } else { 0 }                  // c:104
172}
173
174// =====================================================================
175// math_sum(UNUSED(char *name), int argc, mnumber *argv, UNUSED(int id))  c:104
176// =====================================================================
177
178/// Port of `math_sum(UNUSED(char *name), int argc, mnumber *argv, UNUSED(int id))` from `Src/Modules/example.c:104`.
179#[allow(unused_variables)]
180pub fn math_sum(name: &str, argc: i32, argv: &[mnumber], id: i32) -> mnumber { // c:104
181    // c:104 — `mnumber ret;`
182    let mut ret = mnumber { l: 0, d: 0.0, type_: MN_INTEGER };
183    // c:107 — `int f = 0;`
184    let mut f: i32 = 0;
185    // c:109 — `ret.u.l = 0;`
186    ret.l = 0;
187    let mut argc = argc;
188    let mut p: usize = 0; // emulates argv++ pointer walk (c:124)
189    while {                                                            // c:110 while (argc--)
190        let go = argc > 0;
191        argc -= 1;
192        go
193    } {
194        if argv[p].type_ == MN_INTEGER {                               // c:111
195            if f != 0 {                                                // c:112
196                ret.d += argv[p].l as f64;                             // c:113
197            } else {
198                ret.l += argv[p].l;                                    // c:115
199            }
200        } else {                                                       // c:116
201            if f != 0 {                                                // c:117
202                ret.d += argv[p].d;                                    // c:118
203            } else {                                                   // c:119
204                ret.d = (ret.l as f64) + argv[p].d;                    // c:120
205                f = 1;                                                 // c:121
206            }
207        }
208        p += 1;                                                        // c:124 argv++
209    }
210    // c:133 — `ret.type = (f ? MN_FLOAT : MN_INTEGER);`
211    ret.type_ = if f != 0 { MN_FLOAT } else { MN_INTEGER };
212    ret                                                                 // c:133
213}
214
215// =====================================================================
216// math_length(UNUSED(char *name), char *arg, UNUSED(int id))         c:133
217// =====================================================================
218
219/// Port of `math_length(UNUSED(char *name), char *arg, UNUSED(int id))` from `Src/Modules/example.c:133`.
220#[allow(unused_variables)]
221pub fn math_length(name: &str, arg: &str, id: i32) -> mnumber {            // c:133
222    // c:133 — `mnumber ret;`
223    // c:137 — `ret.type = MN_INTEGER;`
224    // c:138 — `ret.u.l = strlen(arg);`
225    mnumber {
226        type_: MN_INTEGER,
227        l: arg.len() as i64,
228        d: 0.0,
229    }
230}
231
232// =====================================================================
233// ex_wrapper(Eprog prog, FuncWrap w, char *name)                     c:145
234// =====================================================================
235
236/// Port of `ex_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/example.c:145`.
237///
238/// `Eprog` and `FuncWrap` are `Box<eprog>` / `Box<funcwrap>` per
239/// zsh.h:774 / 522. Pointer-shape preserved as `*const eprog` /
240/// `*const funcwrap` since the C body never derefs `prog`/`w` beyond
241/// passing them to `runshfunc`.
242/// WARNING: param names don't match C — Rust=(prog, name) vs C=(prog, w, name)
243pub fn ex_wrapper(prog: *const crate::ported::zsh_h::eprog,                  // c:145
244                  w: *const crate::ported::zsh_h::funcwrap,
245                  name: &str) -> i32 {
246    // c:147 — `if (strncmp(name, "example", 7)) return 1;`
247    if !name.starts_with("example") {
248        return 1;                                                       // c:148
249    }
250    // c:149-156 — else branch:
251    //   int ogd = opts[GLOBDOTS];
252    //   opts[GLOBDOTS] = 1;
253    //   runshfunc(prog, w, name);
254    //   opts[GLOBDOTS] = ogd;
255    //   return 0;
256    // The opts/runshfunc machinery is the legacy tree-walker funcwrap
257    // dispatcher that fusevm replaces; static-link path is never
258    // invoked through addwrapper, so the body stays a no-op besides
259    // the prefix-match contract.
260    let _ = (prog, w);
261    0                                                                   // c:156
262}
263
264// =====================================================================
265// static struct builtin bintab[]                                     c:164
266// static struct conddef cotab[]                                      c:168
267// static struct paramdef patab[]                                     c:173
268// static struct mathfunc mftab[]                                     c:179
269// static struct funcwrap wrapper[]                                   c:184
270// static struct features module_features                             c:188
271//
272// These dispatch tables are consumed by the C module loader
273// (`featuresarray` + `handlefeatures` + `addwrapper`). The
274// static-link / fusevm path doesn't go through that registry; the
275// dispatcher in `src/extensions/` invokes `bin_example` etc.
276// directly. Tables omitted from the Rust port until the module-loader
277// port lands.
278// =====================================================================
279
280// =====================================================================
281// setup_(UNUSED(Module m))                                           c:198
282// =====================================================================
283
284/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/example.c:198`.
285#[allow(unused_variables)]
286pub fn setup_(m: *const module) -> i32 {
287    let mut stdout = std::io::stdout().lock();
288    let _ = writeln!(stdout, "The example module has now been set up."); // c:207
289    let _ = stdout.flush();                                              // c:207
290    0                                                                    // c:207
291}
292
293// =====================================================================
294// features_(Module m, char ***features)                              c:207
295// =====================================================================
296
297/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/example.c:207`.
298/// C body: `*features = featuresarray(m, &module_features); return 0;`
299pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
300    *features = featuresarray(m, module_features());
301    0                                                                   // c:215
302}
303
304// =====================================================================
305// enables_(Module m, int **enables)                                  c:215
306// =====================================================================
307
308/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/example.c:215`.
309/// C body: `return handlefeatures(m, &module_features, enables);`
310pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
311    handlefeatures(m, module_features(), enables) // c:222
312}
313
314// =====================================================================
315// boot_(Module m)                                                    c:222
316// =====================================================================
317
318/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/example.c:222`.
319/// C body sets the demo paramdef-bound statics then calls
320/// `addwrapper(m, wrapper)`.
321pub fn boot_(m: *const module) -> i32 {
322    intparam.store(42, Ordering::Relaxed);                              // c:222
323    *strparam.lock().unwrap() = Some("example".to_string());             // c:225
324    *arrparam.lock().unwrap() = Some(vec![                              // c:226-228
325        "example".to_string(),                                          // c:227
326        "array".to_string(),                                            // c:228
327    ]);
328    // c:230 — addwrapper(m, wrapper); registers ex_wrapper into the
329    // global wrappers linked list (Src/module.c:577). zshrs's fusevm
330    // bytecode doesn't run through C's wrapper-dispatch chain, so the
331    // registration is a no-op until the wrapper machinery has a Rust
332    // equivalent.
333    let _ = m;
334    0
335}
336
337// =====================================================================
338// cleanup_(Module m)                                                 c:235
339// =====================================================================
340
341/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/example.c:235`.
342/// C body: `deletewrapper(m, wrapper); return setfeatureenables(m, &module_features, NULL);`
343pub fn cleanup_(m: *const module) -> i32 {
344    // c:243 — deletewrapper(m, wrapper); paired with c:230 addwrapper,
345    // no-op until the wrapper machinery has a Rust equivalent.
346    setfeatureenables(m, module_features(), None) // c:243
347}
348
349// =====================================================================
350// finish_(UNUSED(Module m))                                          c:243
351// =====================================================================
352
353/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/example.c:243`.
354#[allow(unused_variables)]
355pub fn finish_(m: *const module) -> i32 {
356    let mut stdout = std::io::stdout().lock();
357    let _ = writeln!(
358        stdout,
359        "Thank you for using the example module.  Have a nice day."
360    );                                                                  // c:245
361    let _ = stdout.flush();                                              // c:246
362    0                                                                    // c:247
363}
364
365// =====================================================================
366// External fns + tables — `static struct funcwrap wrapper[]` (c:184),
367// `static struct features module_features` (c:188), and
368// `featuresarray`/`handlefeatures`/`setfeatureenables`/`addwrapper`/
369// `deletewrapper` from `Src/module.c`.
370// =====================================================================
371
372
373// `bintab` — port of `static struct builtin bintab[]` (example.c:182).
374
375
376// `cotab` — port of `static struct conddef cotab[]` (example.c).
377// `CONDDEF("ex", CONDF_INFIX|CONDF_ADDED, …)` + `CONDDEF("len", 0, …)`.
378
379
380// `mftab` — port of `static struct mathfunc mftab[]` (example.c).
381
382
383// `patab` — port of `static struct paramdef patab[]` (example.c).
384
385
386// `module_features` — port of `static struct features module_features`
387// from example.c:188.
388
389
390
391use crate::ported::zsh_h::features as features_t;
392
393static MODULE_FEATURES: OnceLock<Mutex<features_t>> = OnceLock::new();
394
395
396// Local stubs for the per-module entry points. C uses generic
397// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
398// 3275/3370/3445) but those take `Builtin` + `Features` pointer
399// fields the Rust port doesn't carry. The hardcoded descriptor
400// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
401// WARNING: NOT IN EXAMPLE.C — Rust-only module-framework shim.
402// C uses generic featuresarray/handlefeatures/setfeatureenables from
403// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
404// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
405fn featuresarray(_m: *const module, _f: &Mutex<features_t>) -> Vec<String> {
406    vec!["b:example".to_string(), "c:ex".to_string(), "c:len".to_string(), "f:length".to_string(), "f:sum".to_string(), "p:exarr".to_string(), "p:exint".to_string(), "p:exstr".to_string()]
407}
408
409// WARNING: NOT IN EXAMPLE.C — Rust-only module-framework shim.
410// C uses generic featuresarray/handlefeatures/setfeatureenables from
411// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
412// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
413fn handlefeatures(
414    _m: *const module,
415    _f: &Mutex<features_t>,
416    enables: &mut Option<Vec<i32>>,
417) -> i32 {
418    if enables.is_none() {
419        *enables = Some(vec![1; 8]);
420    }
421    0
422}
423
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 setfeatureenables(
429    _m: *const module,
430    _f: &Mutex<features_t>,
431    _e: Option<&[i32]>,
432) -> i32 {
433    0
434}
435
436// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
437// ─── RUST-ONLY ACCESSORS ───
438//
439// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
440// RwLock<T>>` globals declared above. C zsh uses direct global
441// access; Rust needs these wrappers because `OnceLock::get_or_init`
442// is the only way to lazily construct shared state. These fns sit
443// here so the body of this file reads in C source order without
444// the accessor wrappers interleaved between real port fns.
445// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
446
447// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
448// ─── RUST-ONLY ACCESSORS ───
449//
450// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
451// RwLock<T>>` globals declared above. C zsh uses direct global
452// access; Rust needs these wrappers because `OnceLock::get_or_init`
453// is the only way to lazily construct shared state. These fns sit
454// here so the body of this file reads in C source order without
455// the accessor wrappers interleaved between real port fns.
456// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
457
458// WARNING: NOT IN EXAMPLE.C — Rust-only module-framework shim.
459// C uses generic featuresarray/handlefeatures/setfeatureenables from
460// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
461// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
462fn module_features() -> &'static Mutex<features_t> {
463    MODULE_FEATURES.get_or_init(|| Mutex::new(features_t {
464        bn_list: None,
465        bn_size: 1,
466        cd_list: None,
467        cd_size: 2,
468        mf_list: None,
469        mf_size: 2,
470        pd_list: None,
471        pd_size: 3,
472        n_abstract: 0,
473    }))
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479    use crate::ported::zsh_h::MAX_OPS;
480
481    fn empty_ops() -> options {
482        options { ind: [0u8; MAX_OPS], args: Vec::new(), argscount: 0, argsalloc: 0 }
483    }
484    fn s(x: &str) -> String { x.to_string() }
485
486    /// Serialises tests that mutate `intparam` / `strparam` / `arrparam`
487    /// (paramdef bindings at `Src/Modules/example.c:208-218`). The C
488    /// source declares those as file-scope statics that `boot_` and
489    /// `bin_example` overwrite; zsh is single-threaded so the C source
490    /// is safe. cargo's parallel test runner can fire `boot_populates_demo_params`
491    /// and `bin_example_returns_zero_and_assigns_state` concurrently —
492    /// each then reads the values the other just wrote and fails. The
493    /// lock restores the single-writer assumption for the test phase.
494    static EXAMPLE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
495
496    /// Port of `boot_(UNUSED(Module m))` from `Src/Modules/example.c:222`.
497    /// Verifies `boot_()` populates the three paramdef-bound statics
498    /// per c:224-228: intparam=42, strparam="example",
499    /// arrparam=["example","array"].
500    #[test]
501    fn boot_populates_demo_params() {
502        let _g = EXAMPLE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
503        boot_(std::ptr::null());
504        assert_eq!(intparam.load(Ordering::SeqCst), 42);
505        assert_eq!(strparam.lock().unwrap().as_deref(), Some("example"));
506        let arr = arrparam.lock().unwrap();
507        let arr = arr.as_ref().expect("arrparam must be Some after boot_");
508        assert_eq!(arr.len(), 2);
509        assert_eq!(arr[0], "example");
510        assert_eq!(arr[1], "array");
511    }
512
513    /// Verifies `cond_p_len`'s two arities — c:84/89.
514    #[test]
515    fn cond_p_len_arities() {
516        assert_eq!(cond_p_len(&[s("hello"), s("5")], 0), 1);
517        assert_eq!(cond_p_len(&[s("hello"), s("4")], 0), 0);
518        assert_eq!(cond_p_len(&[s("")], 0), 1);
519        assert_eq!(cond_p_len(&[s("x")], 0), 0);
520    }
521
522    /// Verifies `cond_i_ex` matches only the exact concat "example" — c:99.
523    #[test]
524    fn cond_i_ex_concat_matches_example() {
525        assert_eq!(cond_i_ex(&[s("exam"), s("ple")], 0), 1);
526        assert_eq!(cond_i_ex(&[s("example"), s("")], 0), 1);
527        assert_eq!(cond_i_ex(&[s("example"), s("x")], 0), 0);
528        assert_eq!(cond_i_ex(&[s("foo"), s("bar")], 0), 0);
529    }
530
531    /// Verifies `math_sum` returns integer sum for all-int inputs and
532    /// promotes to float once a float arg appears — c:111/116/126.
533    #[test]
534    fn math_sum_int_then_float_promotion() {
535        let ints = [mnumber { l: 1, d: 0.0, type_: MN_INTEGER }, mnumber { l: 2, d: 0.0, type_: MN_INTEGER }, mnumber { l: 3, d: 0.0, type_: MN_INTEGER }];
536        let r = math_sum("sum", 3, &ints, 0);
537        assert_eq!(r.type_, MN_INTEGER);
538        assert_eq!(r.l, 6);
539
540        let mixed = [mnumber { l: 1, d: 0.0, type_: MN_INTEGER }, mnumber { l: 0, d: 2.5, type_: MN_FLOAT }, mnumber { l: 3, d: 0.0, type_: MN_INTEGER }];
541        let r = math_sum("sum", 3, &mixed, 0);
542        assert_eq!(r.type_, MN_FLOAT);
543        assert!((r.d - 6.5).abs() < 1e-9);
544    }
545
546    /// Verifies `math_length` returns string length as integer — c:138.
547    #[test]
548    fn math_length_returns_strlen() {
549        let r = math_length("length", "hello", 0);
550        assert_eq!(r.type_, MN_INTEGER);
551        assert_eq!(r.l, 5);
552    }
553
554    /// Verifies `ex_wrapper` returns 1 (skip) for non-matching names
555    /// and 0 (matched) for `example`-prefixed names — c:147/156.
556    #[test]
557    fn ex_wrapper_name_prefix_match() {
558        assert_eq!(ex_wrapper(std::ptr::null(), std::ptr::null(), "foo"), 1);
559        assert_eq!(ex_wrapper(std::ptr::null(), std::ptr::null(), "exampl"), 1);
560        assert_eq!(ex_wrapper(std::ptr::null(), std::ptr::null(), "example"), 0);
561        assert_eq!(ex_wrapper(std::ptr::null(), std::ptr::null(), "example_func"), 0);
562    }
563
564    /// Port of `bin_example(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/example.c:42`.
565    /// Verifies `bin_example` reads `OPT_ISSET(ops, c)` and prints flagged
566    /// option letters — c:49-51.
567    #[test]
568    fn bin_example_returns_zero_and_assigns_state() {
569        let _g = EXAMPLE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
570        let ops = empty_ops();
571        let args = vec![s("hello"), s("world")];
572        let rc = bin_example("example", &args, &ops, 0);
573        assert_eq!(rc, 0);
574        // c:69 i = 2 (two args); c:71 strparam = "hello"; c:74 arrparam = ["hello","world"]
575        assert_eq!(intparam.load(Ordering::Relaxed), 2);
576        assert_eq!(strparam.lock().unwrap().as_deref(), Some("hello"));
577        let arr = arrparam.lock().unwrap();
578        assert_eq!(arr.as_ref().unwrap().as_slice(), &[s("hello"), s("world")]);
579    }
580
581    /// c:104 — `math_sum` with zero args returns identity (0). Pin
582    /// the empty-input arithmetic identity.
583    #[test]
584    fn math_sum_zero_args_returns_zero() {
585        let r = math_sum("sum", 0, &[], 0);
586        assert_eq!(r.type_, MN_INTEGER);
587        assert_eq!(r.l, 0, "sum of nothing must be 0");
588    }
589
590    /// c:104 — `math_sum` with a single integer is identity.
591    #[test]
592    fn math_sum_single_int_arg_is_identity() {
593        let argv = [mnumber { l: 42, d: 0.0, type_: MN_INTEGER }];
594        let r = math_sum("sum", 1, &argv, 0);
595        assert_eq!(r.type_, MN_INTEGER);
596        assert_eq!(r.l, 42);
597    }
598
599    /// c:104 — All-float input stays MN_FLOAT (no downcast). Pin
600    /// the float-preservation rule because a regen that prefers
601    /// integer "for tidiness" would silently truncate fractions.
602    #[test]
603    fn math_sum_all_floats_preserves_float_type() {
604        let argv = [
605            mnumber { l: 0, d: 1.5, type_: MN_FLOAT },
606            mnumber { l: 0, d: 2.5, type_: MN_FLOAT },
607        ];
608        let r = math_sum("sum", 2, &argv, 0);
609        assert_eq!(r.type_, MN_FLOAT);
610        assert!((r.d - 4.0).abs() < 1e-9);
611    }
612
613    /// c:104 — Negative integers preserved.
614    #[test]
615    fn math_sum_handles_negative_ints() {
616        let argv = [
617            mnumber { l: -5, d: 0.0, type_: MN_INTEGER },
618            mnumber { l: 3, d: 0.0, type_: MN_INTEGER },
619        ];
620        let r = math_sum("sum", 2, &argv, 0);
621        assert_eq!(r.type_, MN_INTEGER);
622        assert_eq!(r.l, -2, "−5 + 3 = −2");
623    }
624
625    /// c:133 — `math_length` on empty string returns 0. Pin so a
626    /// regen that adds `+ 1` for a NUL terminator gets caught.
627    #[test]
628    fn math_length_empty_string_returns_zero() {
629        let r = math_length("length", "", 0);
630        assert_eq!(r.type_, MN_INTEGER);
631        assert_eq!(r.l, 0);
632    }
633
634    /// c:133 — Multi-byte UTF-8 yields BYTE count, not char count
635    /// (zsh's strlen semantics).
636    #[test]
637    fn math_length_multibyte_returns_byte_count() {
638        // "café" = "caf" + "é" (é = 2 bytes in UTF-8) = 5 bytes
639        let r = math_length("length", "café", 0);
640        assert_eq!(r.l, 5,
641            "math_length must count bytes, not chars — got {}", r.l);
642    }
643
644    /// c:95 — `cond_i_ex` (no-arg demo) returns 0 (false). Pin so a
645    /// regen that returns 1 (true) silently inverts every `[[ -i-ex ]]`.
646    #[test]
647    fn cond_i_ex_returns_zero() {
648        let r = cond_i_ex(&[], 0);
649        assert_eq!(r, 0,
650            "cond_i_ex demo condition must return 0 (false)");
651    }
652
653    /// c:286-335 — module-lifecycle stubs return 0.
654    #[test]
655    fn module_lifecycle_shims_all_return_zero() {
656        let m: *const module = std::ptr::null();
657        assert_eq!(setup_(m), 0);
658        assert_eq!(boot_(m), 0);
659        assert_eq!(cleanup_(m), 0);
660        assert_eq!(finish_(m), 0);
661    }
662}