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}