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}