Skip to main content

zsh/ported/modules/
mathfunc.rs

1//! Mathematical functions for arithmetic expressions — port of
2//! `Src/Modules/mathfunc.c`.
3//!
4//! C source has THREE anonymous `enum {}` blocks (lines 35, 90,
5//! 104) generating `int`-typed constants — no named C type, so
6//! the Rust port mirrors them as `pub const ... : i32 = ...;`
7//! definitions only (rule 1: no Rust-only struct/enum types).
8//!
9//! All math-fn dispatch lives in a single `math_func()` switch,
10//! matching the C structure 1:1.
11
12#![allow(clippy::approx_constant)]
13
14use crate::ported::math::{mnumber, MN_FLOAT, MN_INTEGER};
15use crate::ported::zsh_h::{features, mathfunc, module};
16use crate::random_real::random_real;
17use std::sync::{Mutex, OnceLock};
18
19// libm bindings used by the math-function dispatcher. Direct port
20// of the calls C's `math_func()` (Src/Modules/mathfunc.c:172-436)
21// makes via `<math.h>`. Bessel functions and `erf` aren't in
22// Rust's `std`, so we declare the C ABI bindings here.
23#[cfg(unix)]
24extern "C" {
25    fn j0(x: f64) -> f64;
26    fn j1(x: f64) -> f64;
27    fn jn(n: i32, x: f64) -> f64;
28    fn y0(x: f64) -> f64;
29    fn y1(x: f64) -> f64;
30    fn yn(n: i32, x: f64) -> f64;
31    fn erf(x: f64) -> f64;
32    fn erfc(x: f64) -> f64;
33    fn lgamma(x: f64) -> f64;
34    fn tgamma(x: f64) -> f64;
35    fn ilogb(x: f64) -> i32;
36    fn logb(x: f64) -> f64;
37    fn nextafter(x: f64, y: f64) -> f64;
38    fn rint(x: f64) -> f64;
39    fn scalbn(x: f64, n: i32) -> f64;
40    fn ldexp(x: f64, exp: i32) -> f64;
41    fn copysign(x: f64, y: f64) -> f64;
42    fn expm1(x: f64) -> f64;
43    fn log1p(x: f64) -> f64;
44}
45
46/// Port of `math_string(UNUSED(char *name), char *arg, int id)` from `Src/Modules/mathfunc.c:439`. The
47/// string-arg math-fn dispatcher behind `rand48("seedvar")` and
48/// future string-takers. C signature:
49///   `static mnumber math_string(char *name, char *arg, int id)`
50///
51/// Strips leading/trailing iblank from `arg` (mathfunc.c:447-451)
52/// then switches on `id`. Currently only `MS_RAND48` exists; the
53/// random-bit production lives in `crate::ported::random` and
54/// `crate::ported::modules::random_real`. Returns `zero_mnumber`
55/// for unrecognised ids (matching C's pre-init `ret = zero_mnumber`).
56#[allow(unused_variables)]
57pub fn math_string(name: &str, arg: &str, id: i32) -> mnumber {
58    // c:439
59    // c:441 — `mnumber ret = zero_mnumber;`
60    let zero_mnumber = mnumber {
61        l: 0,
62        d: 0.0,
63        type_: MN_INTEGER,
64    };
65    // c:448-453 — strip iblank from both ends, then NUL-terminate.
66    // Rust slice form: skip leading iblank, then truncate trailing.
67    let bytes = arg.as_bytes();
68    let mut start = 0;
69    while start < bytes.len() && crate::ported::ztype_h::iblank(bytes[start]) {
70        start += 1;
71    }
72    let mut end = bytes.len();
73    while end > start && crate::ported::ztype_h::iblank(bytes[end - 1]) {
74        end -= 1;
75    }
76    let arg_trim = std::str::from_utf8(&bytes[start..end]).unwrap_or("");
77    match id {
78        MS_RAND48 => {
79            // c:457-530 — MS_RAND48 arm.
80            // c:460-461 — `static unsigned short seedbuf[3]; static int seedbuf_init;`
81            //             — the lifetime-of-process seed state.
82            static SEEDBUF: std::sync::OnceLock<std::sync::Mutex<[u16; 3]>> =
83                std::sync::OnceLock::new();
84            static SEEDBUF_INIT: std::sync::atomic::AtomicBool =
85                std::sync::atomic::AtomicBool::new(false);
86            let seedbuf_mtx = SEEDBUF.get_or_init(|| std::sync::Mutex::new([0u16; 3]));
87            let mut seedbuf = seedbuf_mtx.lock().unwrap_or_else(|e| e.into_inner());
88            // c:462-463 — `unsigned short tmp_seedbuf[3], *seedbufptr; int do_init = 1;`
89            let mut tmp_seedbuf: [u16; 3] = [0; 3];
90            let mut do_init: bool = true; // c:463
91                                          // c:464-506 — choose seedbufptr (tmp from param vs static) and
92                                          // decide do_init.
93                                          //
94                                          // Two-step ptr selection: in C `seedbufptr` is either `&tmp_seedbuf`
95                                          // or `&seedbuf`. In Rust we mirror via a bool — `use_static` —
96                                          // since `&mut [u16; 3]` can't switch between the two without
97                                          // borrow gymnastics; copy in/out of tmp_seedbuf instead.
98            let use_static: bool;
99            if !arg_trim.is_empty() {
100                // c:465 — `if (*arg) { ... }`
101                use_static = false; // c:468 seedbufptr = tmp_seedbuf
102                if let Some(seedstr) = crate::ported::params::getsparam(arg_trim) {
103                    // c:469 — `(seedstr = getsparam(arg)) && strlen(seedstr) >= 12`
104                    let sbytes = seedstr.as_bytes();
105                    if sbytes.len() >= 12 {
106                        do_init = false; // c:471
107                                         // c:476-493 — decode 3 sets of 4 hex chars into tmp_seedbuf.
108                        let mut cursor = 0;
109                        'outer: for i in 0..3 {
110                            let mut acc: u16 = 0;
111                            for j in 0..4 {
112                                let c = sbytes[cursor];
113                                cursor += 1;
114                                let lower = c.to_ascii_lowercase();
115                                let nib: u16 = if c.is_ascii_digit() {
116                                    (c - b'0') as u16
117                                } else if (b'a'..=b'f').contains(&lower) {
118                                    (lower - b'a' + 10) as u16
119                                } else {
120                                    do_init = true; // c:486
121                                    break 'outer;
122                                };
123                                acc += nib;
124                                if j < 3 {
125                                    acc *= 16; // c:491
126                                }
127                            }
128                            tmp_seedbuf[i] = acc; // c:478 *seedptr = ...
129                        }
130                    }
131                } else if crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed)
132                    != 0
133                {
134                    // c:495-496 — `else if (errflag) break;` — bail with zero_mnumber.
135                    return zero_mnumber;
136                }
137            } else {
138                // c:499-506 — `else { seedbufptr = seedbuf; ... }`.
139                use_static = true; // c:501
140                                   // c:502-505 — the C source as written assigns `do_init = 1`
141                                   //             in the else branch, leaving the if-branch as a
142                                   //             pure seedbuf_init flip. Net effect: do_init
143                                   //             stays 1 in both branches (it was 1 from c:463),
144                                   //             so the static seedbuf is re-seeded every call
145                                   //             when arg is empty. Preserved verbatim — quirk
146                                   //             is in the C source, not the port.
147                if !SEEDBUF_INIT.load(std::sync::atomic::Ordering::Relaxed) {
148                    SEEDBUF_INIT.store(true, std::sync::atomic::Ordering::Relaxed);
149                // c:503
150                } else {
151                    do_init = true; // c:505
152                }
153            }
154            // c:507-518 — fresh seed via rand() + seed48().
155            if do_init {
156                let s0 = unsafe { libc::rand() } as u16;
157                let s1 = unsafe { libc::rand() } as u16;
158                let s2 = unsafe { libc::rand() } as u16;
159                if use_static {
160                    seedbuf[0] = s0;
161                    seedbuf[1] = s1;
162                    seedbuf[2] = s2;
163                } else {
164                    tmp_seedbuf[0] = s0;
165                    tmp_seedbuf[1] = s1;
166                    tmp_seedbuf[2] = s2;
167                }
168                // c:517 — `(void)seed48(seedbufptr);`
169                let ptr = if use_static {
170                    seedbuf.as_mut_ptr()
171                } else {
172                    tmp_seedbuf.as_mut_ptr()
173                };
174                unsafe {
175                    libc::seed48(ptr);
176                }
177            }
178            // c:519-520 — `ret.type = MN_FLOAT; ret.u.d = erand48(seedbufptr);`
179            let ret_d = unsafe {
180                let ptr = if use_static {
181                    seedbuf.as_mut_ptr()
182                } else {
183                    tmp_seedbuf.as_mut_ptr()
184                };
185                libc::erand48(ptr)
186            };
187            // c:522-528 — if arg present, encode new seedbuf → $arg (3×4 hex).
188            if !arg_trim.is_empty() {
189                let s = if use_static { &*seedbuf } else { &tmp_seedbuf };
190                let outbuf = format!("{:04x}{:04x}{:04x}", s[0], s[1], s[2]);
191                let _ = crate::ported::params::setsparam(arg_trim, &outbuf);
192            }
193            mnumber {
194                l: 0,
195                d: ret_d,
196                type_: MN_FLOAT,
197            }
198        }
199        _ => zero_mnumber, // c:441 default
200    }
201}
202
203// `mftab` — port of `static struct mathfunc mftab[]` (mathfunc.c:497).
204
205// `module_features` — port of `static struct features module_features`
206// from mathfunc.c:540.
207
208/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/mathfunc.c:548`.
209#[allow(unused_variables)]
210pub fn setup_(m: *const module) -> i32 {
211    // c:548
212    // C body c:550-551 — `return 0`. Faithful empty-body port.
213    0
214}
215
216/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/mathfunc.c:555`.
217/// C body: `*features = featuresarray(m, &module_features); return 0;`
218pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
219    // c:555
220    *features = featuresarray(m, module_features());
221    0 // c:570
222}
223
224/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/mathfunc.c:563`.
225/// C body: `return handlefeatures(m, &module_features, enables);`
226pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
227    // c:563
228    handlefeatures(m, module_features(), enables) // c:570
229}
230
231/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/mathfunc.c:570`.
232#[allow(unused_variables)]
233pub fn boot_(m: *const module) -> i32 {
234    // c:570
235    // C body c:572-573 — `return 0`. Faithful empty-body port; the
236    //                    math functions are registered via the mf_list
237    //                    feature dispatch, no extra boot work needed.
238    0
239}
240
241/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/mathfunc.c:577`.
242/// C body: `return setfeatureenables(m, &module_features, NULL);`
243pub fn cleanup_(m: *const module) -> i32 {
244    // c:577
245    setfeatureenables(m, module_features(), None) // c:584
246}
247
248/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/mathfunc.c:584`.
249#[allow(unused_variables)]
250pub fn finish_(m: *const module) -> i32 {
251    // c:584
252    // C body c:586-587 — `return 0`. Faithful empty-body port; the
253    //                    math functions are unregistered via cleanup_.
254    0
255}
256
257// ============================================================
258// MF_* — port of the anonymous `enum {}` at mathfunc.c:34-84.
259// C `enum {}` with no typedef → untyped int constants. Rust
260// mirrors as `pub const ... : i32` (no Rust-only enum type).
261// ============================================================
262/// `MF_ABS` constant.
263pub const MF_ABS: i32 = 0; // c:35
264/// `MF_ACOS` constant.
265pub const MF_ACOS: i32 = 1; // c:36
266/// `MF_ACOSH` constant.
267pub const MF_ACOSH: i32 = 2;
268/// `MF_ASIN` constant.
269pub const MF_ASIN: i32 = 3;
270/// `MF_ASINH` constant.
271pub const MF_ASINH: i32 = 4;
272/// `MF_ATAN` constant.
273pub const MF_ATAN: i32 = 5;
274/// `MF_ATANH` constant.
275pub const MF_ATANH: i32 = 6;
276/// `MF_CBRT` constant.
277pub const MF_CBRT: i32 = 7;
278/// `MF_CEIL` constant.
279pub const MF_CEIL: i32 = 8;
280/// `MF_COPYSIGN` constant.
281pub const MF_COPYSIGN: i32 = 9;
282/// `MF_COS` constant.
283pub const MF_COS: i32 = 10;
284/// `MF_COSH` constant.
285pub const MF_COSH: i32 = 11;
286/// `MF_ERF` constant.
287pub const MF_ERF: i32 = 12;
288/// `MF_ERFC` constant.
289pub const MF_ERFC: i32 = 13;
290/// `MF_EXP` constant.
291pub const MF_EXP: i32 = 14;
292/// `MF_EXPM1` constant.
293pub const MF_EXPM1: i32 = 15;
294/// `MF_FABS` constant.
295pub const MF_FABS: i32 = 16;
296/// `MF_FLOAT` constant.
297pub const MF_FLOAT: i32 = 17;
298/// `MF_FLOOR` constant.
299pub const MF_FLOOR: i32 = 18;
300/// `MF_FMOD` constant.
301pub const MF_FMOD: i32 = 19;
302/// `MF_GAMMA` constant.
303pub const MF_GAMMA: i32 = 20;
304/// `MF_HYPOT` constant.
305pub const MF_HYPOT: i32 = 21;
306/// `MF_ILOGB` constant.
307pub const MF_ILOGB: i32 = 22;
308/// `MF_INT` constant.
309pub const MF_INT: i32 = 23;
310/// `MF_ISINF` constant.
311pub const MF_ISINF: i32 = 24;
312/// `MF_ISNAN` constant.
313pub const MF_ISNAN: i32 = 25;
314/// `MF_J0` constant.
315pub const MF_J0: i32 = 26;
316/// `MF_J1` constant.
317pub const MF_J1: i32 = 27;
318/// `MF_JN` constant.
319pub const MF_JN: i32 = 28;
320/// `MF_LDEXP` constant.
321pub const MF_LDEXP: i32 = 29;
322/// `MF_LGAMMA` constant.
323pub const MF_LGAMMA: i32 = 30;
324/// `MF_LOG` constant.
325pub const MF_LOG: i32 = 31;
326/// `MF_LOG10` constant.
327pub const MF_LOG10: i32 = 32;
328/// `MF_LOG1P` constant.
329pub const MF_LOG1P: i32 = 33;
330/// `MF_LOG2` constant.
331pub const MF_LOG2: i32 = 34;
332/// `MF_LOGB` constant.
333pub const MF_LOGB: i32 = 35;
334/// `MF_NEXTAFTER` constant.
335pub const MF_NEXTAFTER: i32 = 36;
336/// `MF_RINT` constant.
337pub const MF_RINT: i32 = 37;
338/// `MF_SCALB` constant.
339pub const MF_SCALB: i32 = 38;
340/// `MF_SIGNGAM` constant.
341pub const MF_SIGNGAM: i32 = 39; // c:75 #ifdef HAVE_SIGNGAM
342/// `MF_SIN` constant.
343pub const MF_SIN: i32 = 40;
344/// `MF_SINH` constant.
345pub const MF_SINH: i32 = 41;
346/// `MF_SQRT` constant.
347pub const MF_SQRT: i32 = 42;
348/// `MF_TAN` constant.
349pub const MF_TAN: i32 = 43;
350/// `MF_TANH` constant.
351pub const MF_TANH: i32 = 44;
352/// `MF_Y0` constant.
353pub const MF_Y0: i32 = 45;
354/// `MF_Y1` constant.
355pub const MF_Y1: i32 = 46;
356/// `MF_YN` constant.
357pub const MF_YN: i32 = 47; // c:84
358
359// =====================================================================
360// static struct mathfunc mftab[]                                    c:497
361// static struct features module_features                            c:540
362// =====================================================================
363
364// ============================================================
365// MS_* — port of the anonymous `enum {}` at mathfunc.c:90.
366// String-arg math-fn ids.
367// ============================================================
368/// `MS_RAND48` constant.
369pub const MS_RAND48: i32 = 0; // c:91
370
371// ============================================================
372// TF_* — port of the anonymous `enum {}` at mathfunc.c:104.
373// Type-flag bits, individually testable.
374// ============================================================
375/// `TF_NOCONV` constant.
376pub const TF_NOCONV: i32 = 1; // c:106 don't convert to float
377/// `TF_INT1` constant.
378pub const TF_INT1: i32 = 2; // c:107 first arg is integer
379/// `TF_INT2` constant.
380pub const TF_INT2: i32 = 4; // c:108 second arg is integer
381/// `TF_NOASS` constant.
382pub const TF_NOASS: i32 = 8; // c:109 don't assign result as double
383
384/// Port of the `TFLAG(x)` macro from `mathfunc.c:113`.
385/// `#define TFLAG(x) ((x) << 8)`. Shifts the type-flag bits into
386/// the high byte of the `id` arg passed to `math_func()` so the
387/// MF_* numeric ids can occupy the low byte.
388pub const fn tflag(x: i32) -> i32 {
389    x << 8
390} // c:113
391
392/// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`. The
393/// dispatcher behind every numeric math fn registered via
394/// `NUMMATHFUNC` in `mftab[]` (mathfunc.c:115-167).
395///
396/// C signature:
397///   `static mnumber math_func(char *name, int argc, mnumber *argv, int id)`
398///
399/// Matches that signature exactly: `name` is unused (UNUSED in C);
400/// `argc` is the actual argument count; `argv` is the slice of
401/// argument values; `id` is the MF_* function id ORed with TFLAG()
402/// type flags in its high byte.
403#[allow(non_snake_case)]
404/// WARNING: param names don't match C — Rust=(_name, argc, argv, id) vs C=(name, argc, argv, id)
405pub fn math_func(_name: &str, argc: i32, argv: &[mnumber], id: i32) -> mnumber {
406    // c:173
407    let mut ret = mnumber {
408        l: 0,
409        d: 0.0,
410        type_: MN_FLOAT,
411    }; // c:173,193
412       // C's mathfunc dispatch (via `callmathfunc` at math.c:1037+ and
413       // the `Math_func_set` per-fn `min_args`/`max_args` fields registered
414       // in mftab) rejects out-of-range argc BEFORE calling math_func, so
415       // C's body can index `argv[0]` safely. The Rust port calls this
416       // dispatcher directly from tests and (eventually) other paths
417       // without that upstream guard. Bail to a zero mnumber when argc is
418       // 0 AND argv is empty so MF_ABS-default-id calls don't OOB. Other
419       // arms that genuinely need 2+ args already check `argc > 1` below.
420    if argc <= 0 && argv.is_empty() {
421        return ret;
422    }
423    let mut argd: f64 = 0.0; // c:175
424    let mut argd2: f64 = 0.0; // c:175
425    let mut argi: i32 = 0; // c:176
426
427    // Type-coerce argv[0] (and argv[1]) per the TF_INT1/TF_INT2/
428    // TF_NOCONV flag bits — c:178-191.
429    if argc > 0 && (id & tflag(TF_NOCONV)) == 0 {
430        // c:178
431        if (id & tflag(TF_INT1)) != 0 {
432            // c:179
433            argi = if argv[0].type_ == MN_FLOAT {
434                argv[0].d as i32 // c:180
435            } else {
436                argv[0].l as i32
437            };
438        } else {
439            // c:181
440            argd = if argv[0].type_ == MN_INTEGER {
441                argv[0].l as f64 // c:182
442            } else {
443                argv[0].d
444            };
445        }
446        if argc > 1 {
447            // c:183
448            if (id & tflag(TF_INT2)) != 0 {
449                // c:184
450                argi = if argv[1].type_ == MN_FLOAT {
451                    argv[1].d as i32 // c:185
452                } else {
453                    argv[1].l as i32
454                };
455            } else {
456                // c:187
457                argd2 = if argv[1].type_ == MN_INTEGER {
458                    argv[1].l as f64 // c:188
459                } else {
460                    argv[1].d
461                };
462            }
463        }
464    }
465
466    // C: `if (errflag) return ret;` — c:196. zshrs's errflag is on
467    // the executor; this dispatcher is invoked from the math
468    // evaluator which already short-circuits on error, so the
469    // explicit check is redundant here.
470
471    let mut retd: f64 = 0.0; // c:175
472
473    match id & 0xff {
474        // c:198
475        MF_ABS => {
476            // c:199
477            ret.type_ = argv[0].type_;
478            if argv[0].type_ == MN_INTEGER {
479                ret.l = if argv[0].l < 0 { -argv[0].l } else { argv[0].l };
480            } else {
481                // c:204 — `ret.u.d = fabs(argv->u.d);`. C relies on the
482                // mftab registration (c:115 NUMMATHFUNC("abs", …,
483                // MF_ABS | TFLAG(TF_NOCONV|TF_NOASS))) merging
484                // TF_NOASS into id so the post-match block (c:431-432
485                // `if (!(id & TFLAG(TF_NOASS))) ret.u.d = retd;`)
486                // doesn't clobber ret.d with retd=0.0. The Rust port
487                // is called directly from tests with bare MF_ABS, so
488                // the TF_NOASS-implicit-in-mftab assumption breaks.
489                // Set BOTH ret.d AND retd so the post-block overwrite
490                // is harmless either way — caller-supplied TF_NOASS
491                // is still honoured but no longer required for
492                // correctness.
493                ret.d = argv[0].d.abs();
494                retd = ret.d;
495            }
496        }
497        MF_ACOS => retd = argd.acos(),   // c:208
498        MF_ACOSH => retd = argd.acosh(), // c:212
499        MF_ASIN => retd = argd.asin(),   // c:216
500        MF_ASINH => retd = argd.asinh(), // c:220
501        MF_ATAN => {
502            // c:224
503            retd = if argc == 2 {
504                argd.atan2(argd2)
505            } else {
506                argd.atan()
507            };
508        }
509        MF_ATANH => retd = argd.atanh(),         // c:233
510        MF_CBRT => retd = crate::system_libm::cbrt(argd), // c:237 (the C library's cbrt)
511        MF_CEIL => retd = argd.ceil(),           // c:241
512        MF_COPYSIGN => retd = unsafe { copysign(argd, argd2) }, // c:245
513        MF_COS => retd = argd.cos(),             // c:249
514        MF_COSH => retd = argd.cosh(),           // c:253
515        MF_ERF => retd = unsafe { erf(argd) },   // c:257
516        MF_ERFC => retd = unsafe { erfc(argd) }, // c:261
517        MF_EXP => retd = argd.exp(),             // c:265
518        MF_EXPM1 => retd = unsafe { expm1(argd) }, // c:269
519        MF_FABS => retd = argd.abs(),            // c:273
520        MF_FLOAT => retd = argd,                 // c:277
521        MF_FLOOR => retd = argd.floor(),         // c:281
522        MF_FMOD => retd = argd % argd2,          // c:285
523        MF_GAMMA => retd = unsafe { tgamma(argd) }, // c:289
524        MF_HYPOT => retd = argd.hypot(argd2),    // c:300
525        MF_ILOGB => {
526            // c:304
527            ret.type_ = MN_INTEGER;
528            ret.l = unsafe { ilogb(argd) } as i64;
529        }
530        MF_INT => {
531            // c:309
532            ret.type_ = MN_INTEGER;
533            ret.l = argd as i64;
534        }
535        MF_ISINF => {
536            // c:314
537            ret.type_ = MN_INTEGER;
538            ret.l = argd.is_infinite() as i64;
539        }
540        MF_ISNAN => {
541            // c:319
542            ret.type_ = MN_INTEGER;
543            ret.l = argd.is_nan() as i64;
544        }
545        MF_J0 => retd = unsafe { j0(argd) },             // c:325
546        MF_J1 => retd = unsafe { j1(argd) },             // c:329
547        MF_JN => retd = unsafe { jn(argi, argd2) },      // c:333
548        MF_LDEXP => retd = unsafe { ldexp(argd, argi) }, // c:337
549        MF_LGAMMA => retd = unsafe { lgamma(argd) },     // c:341
550        MF_LOG => retd = argd.ln(),                      // c:345
551        MF_LOG10 => retd = argd.log10(),                 // c:349
552        MF_LOG1P => retd = unsafe { log1p(argd) },       // c:353
553        MF_LOG2 => retd = argd.log2(),                   // c:357
554        MF_LOGB => retd = unsafe { logb(argd) },         // c:365
555        MF_NEXTAFTER => retd = unsafe { nextafter(argd, argd2) }, // c:369
556        MF_RINT => retd = unsafe { rint(argd) },         // c:373
557        MF_SCALB => retd = unsafe { scalbn(argd, argi) }, // c:377
558        MF_SIGNGAM => {
559            // c:386
560            ret.type_ = MN_INTEGER;
561            ret.l = 0; // signgam is libm-internal; not portably exposed.
562        }
563        MF_SIN => retd = argd.sin(),                // c:392
564        MF_SINH => retd = argd.sinh(),              // c:396
565        MF_SQRT => retd = argd.sqrt(),              // c:400
566        MF_TAN => retd = argd.tan(),                // c:404
567        MF_TANH => retd = argd.tanh(),              // c:408
568        MF_Y0 => retd = unsafe { y0(argd) },        // c:412
569        MF_Y1 => retd = unsafe { y1(argd) },        // c:416
570        MF_YN => retd = unsafe { yn(argi, argd2) }, // c:420
571        _ => { // c:425
572             // BUG: mathfunc type not handled. C prints to stderr
573             // under DEBUG; production zsh silently returns 0.
574        }
575    }
576
577    if (id & tflag(TF_NOASS)) == 0 {
578        // c:431
579        ret.d = retd; // c:432
580    }
581
582    ret // c:434
583}
584
585static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
586
587/// Port of `static struct mathfunc mftab[]` from `Src/Modules/mathfunc.c:114-167`.
588///
589/// C macro per entry: `NUMMATHFUNC(name, math_func, min, max, id)` =
590/// `{ NULL, name, 0, func, NULL, NULL, min, max, id }` (zsh.h:133) —
591/// flags 0, module NULL. The table is the registration source for
592/// `setmathfuncs` (module.c:1374): feature-enable inserts entries into
593/// the global MATHFUNCS list with `MFF_ADDED`; disable removes them.
594/// Entry order MUST match `featuresarray` below — enables bitmaps are
595/// positional (module.c:3284 featuresarray ↔ c:3319 getfeatureenables).
596///
597/// `STRMATHFUNC("rand48", math_string, MS_RAND48)` (c:154, inside
598/// `#ifdef HAVE_ERAND48`) sits between `nextafter` (c:152) and `rint`
599/// (c:156). zshrs calls `libc::erand48` unconditionally (`math_string`
600/// above), so the guard holds and the entry belongs in both tables.
601/// Initialized inline by `setfeatureenables` below via
602/// `MFTAB.get_or_init` (single consumer; the src/ported/ build gate
603/// forbids a Rust-only named accessor fn).
604static MFTAB: OnceLock<Mutex<Vec<mathfunc>>> = OnceLock::new();
605
606// Local stubs for the per-module entry points. C uses generic
607// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
608// 3275/3370/3445) but those take `Builtin` + `Features` pointer
609// fields the Rust port doesn't carry. The hardcoded descriptor
610// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
611/// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
612fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
613    vec![
614        "f:abs".to_string(),
615        "f:acos".to_string(),
616        "f:acosh".to_string(),
617        "f:asin".to_string(),
618        "f:asinh".to_string(),
619        "f:atan".to_string(),
620        "f:atanh".to_string(),
621        "f:cbrt".to_string(),
622        "f:ceil".to_string(),
623        "f:copysign".to_string(),
624        "f:cos".to_string(),
625        "f:cosh".to_string(),
626        "f:erf".to_string(),
627        "f:erfc".to_string(),
628        "f:exp".to_string(),
629        "f:expm1".to_string(),
630        "f:fabs".to_string(),
631        "f:float".to_string(),
632        "f:floor".to_string(),
633        "f:fmod".to_string(),
634        "f:gamma".to_string(),
635        "f:hypot".to_string(),
636        "f:ilogb".to_string(),
637        "f:int".to_string(),
638        "f:isinf".to_string(),
639        "f:isnan".to_string(),
640        "f:j0".to_string(),
641        "f:j1".to_string(),
642        "f:jn".to_string(),
643        "f:ldexp".to_string(),
644        "f:lgamma".to_string(),
645        "f:log".to_string(),
646        "f:log10".to_string(),
647        "f:log1p".to_string(),
648        "f:log2".to_string(),
649        "f:logb".to_string(),
650        "f:nextafter".to_string(),
651        // c:154 `STRMATHFUNC("rand48", math_string, MS_RAND48)`, under
652        // `#ifdef HAVE_ERAND48` (c:153). Positional between nextafter and
653        // rint; the enables bitmap is indexed against this order.
654        "f:rand48".to_string(),
655        "f:rint".to_string(),
656        "f:scalb".to_string(),
657        "f:signgam".to_string(),
658        "f:sin".to_string(),
659        "f:sinh".to_string(),
660        "f:sqrt".to_string(),
661        "f:tan".to_string(),
662        "f:tanh".to_string(),
663        "f:y0".to_string(),
664        "f:y1".to_string(),
665        "f:yn".to_string(),
666    ]
667}
668
669// WARNING: NOT IN MATHFUNC.C — Rust-only module-framework shim.
670// C uses generic featuresarray/handlefeatures/setfeatureenables from
671// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
672// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
673fn handlefeatures(m: *const module, f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
674    // c:3392 — the name-keyed variant in src/ported/module.rs holds the
675    // per-feature ADDED bit; this module ships no `Features` descriptor
676    // table for it to live on (see MODULE_FEATURE_ENABLES there).
677    let set = enables.as_ref().map(|e| e.to_vec());
678    let ret = crate::ported::module::handlefeatures("zsh/mathfunc", &featuresarray(m, f), enables);
679    // c:3395 — the SET arm must also COMMIT the bits through this module's
680    // own `setfeatureenables` -> c:1374 `setmathfuncs`, which registers /
681    // deregisters the mftab entries in the global MATHFUNCS list. Without
682    // it `zmodload zsh/mathfunc` never populated MATHFUNCS, so getmathfunc's
683    // post-autoload re-query (c:1298) always missed and `zmodload -af
684    // zsh/mathfunc sin; $(( sin(0) ))` errored.
685    match set {
686        Some(e) => setfeatureenables(m, f, Some(&e)),
687        None => ret,
688    }
689}
690
691// WARNING: NOT IN MATHFUNC.C — Rust-only module-framework shim.
692// C uses generic featuresarray/handlefeatures/setfeatureenables from
693// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
694// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
695fn setfeatureenables(_m: *const module, _f: &Mutex<features>, e: Option<&[i32]>) -> i32 {
696    // c:Src/module.c:3445-3460 setfeatureenables → c:1374 setmathfuncs:
697    // walk mftab against the positional enables bitmap; 1 → addmathfunc
698    // into the global MATHFUNCS list (+MFF_ADDED), 0/None → remove.
699    //
700    // MFTAB get_or_init — port of `static struct mathfunc mftab[]`
701    // from Src/Modules/mathfunc.c:114-167. C macro per entry:
702    // `NUMMATHFUNC(name, math_func, min, max, id)` = `{ NULL, name, 0,
703    // func, NULL, NULL, min, max, id }` (zsh.h:133) — flags 0, module
704    // NULL. Entry order MUST match `featuresarray` above — enables
705    // bitmaps are positional (module.c:3284 featuresarray ↔ c:3319
706    // getfeatureenables). The one STRMATHFUNC entry, `rand48` (c:154),
707    // is built by `strf` below; `math.rs`'s own name-keyed shortcut
708    // (math.rs:3153) evaluates rand48 without consulting MATHFUNCS, so
709    // the table entry is what `zmodload -F zsh/mathfunc f:rand48` and
710    // `zmodload -lF` read, not what `$(( rand48() ))` dispatches on.
711    let tab_mutex = MFTAB.get_or_init(|| {
712        // NUMMATHFUNC expansion — zsh.h:133.
713        let num = |name: &str, min: i32, max: i32, id: i32| mathfunc {
714            next: None,
715            name: name.to_string(),
716            flags: 0,
717            nfunc: Some(math_func as crate::ported::zsh_h::NumMathFunc),
718            sfunc: None,
719            module: None,
720            minargs: min,
721            maxargs: max,
722            funcid: id,
723        };
724        // STRMATHFUNC expansion — zsh.h:135: `{ NULL, name, MFF_STR,
725        // NULL, func, NULL, 0, 0, id }`, already ported in zsh_h.
726        let strf = crate::ported::zsh_h::STRMATHFUNC;
727        Mutex::new(vec![
728            num("abs", 1, 1, MF_ABS | tflag(TF_NOCONV | TF_NOASS)), // c:115
729            num("acos", 1, 1, MF_ACOS),                             // c:117
730            num("acosh", 1, 1, MF_ACOSH),                           // c:118
731            num("asin", 1, 1, MF_ASIN),                             // c:119
732            num("asinh", 1, 1, MF_ASINH),                           // c:120
733            num("atan", 1, 2, MF_ATAN),                             // c:121
734            num("atanh", 1, 1, MF_ATANH),                           // c:122
735            num("cbrt", 1, 1, MF_CBRT),                             // c:123
736            num("ceil", 1, 1, MF_CEIL),                             // c:124
737            num("copysign", 2, 2, MF_COPYSIGN),                     // c:125
738            num("cos", 1, 1, MF_COS),                               // c:126
739            num("cosh", 1, 1, MF_COSH),                             // c:127
740            num("erf", 1, 1, MF_ERF),                               // c:128
741            num("erfc", 1, 1, MF_ERFC),                             // c:129
742            num("exp", 1, 1, MF_EXP),                               // c:130
743            num("expm1", 1, 1, MF_EXPM1),                           // c:131
744            num("fabs", 1, 1, MF_FABS),                             // c:132
745            num("float", 1, 1, MF_FLOAT),                           // c:133
746            num("floor", 1, 1, MF_FLOOR),                           // c:134
747            num("fmod", 2, 2, MF_FMOD),                             // c:135
748            num("gamma", 1, 1, MF_GAMMA),                           // c:136
749            num("hypot", 2, 2, MF_HYPOT),                           // c:137
750            num("ilogb", 1, 1, MF_ILOGB | tflag(TF_NOASS)),         // c:138
751            num("int", 1, 1, MF_INT | tflag(TF_NOASS)),             // c:139
752            num("isinf", 1, 1, MF_ISINF | tflag(TF_NOASS)),         // c:140
753            num("isnan", 1, 1, MF_ISNAN | tflag(TF_NOASS)),         // c:141
754            num("j0", 1, 1, MF_J0),                                 // c:142
755            num("j1", 1, 1, MF_J1),                                 // c:143
756            num("jn", 2, 2, MF_JN | tflag(TF_INT1)),                // c:144
757            num("ldexp", 2, 2, MF_LDEXP | tflag(TF_INT2)),          // c:145
758            num("lgamma", 1, 1, MF_LGAMMA),                         // c:146
759            num("log", 1, 1, MF_LOG),                               // c:147
760            num("log10", 1, 1, MF_LOG10),                           // c:148
761            num("log1p", 1, 1, MF_LOG1P),                           // c:149
762            num("log2", 1, 1, MF_LOG2),                             // c:150
763            num("logb", 1, 1, MF_LOGB),                             // c:151
764            num("nextafter", 2, 2, MF_NEXTAFTER),                   // c:152
765            strf("rand48", math_string, MS_RAND48),                 // c:154
766            num("rint", 1, 1, MF_RINT),                             // c:156
767            num("scalb", 2, 2, MF_SCALB | tflag(TF_INT2)),          // c:157
768            num("signgam", 0, 0, MF_SIGNGAM | tflag(TF_NOASS)),     // c:159
769            num("sin", 1, 1, MF_SIN),                               // c:161
770            num("sinh", 1, 1, MF_SINH),                             // c:162
771            num("sqrt", 1, 1, MF_SQRT),                             // c:163
772            num("tan", 1, 1, MF_TAN),                               // c:164
773            num("tanh", 1, 1, MF_TANH),                             // c:165
774            num("y0", 1, 1, MF_Y0),                                 // c:166
775            num("y1", 1, 1, MF_Y1),                                 // c:167
776            num("yn", 2, 2, MF_YN | tflag(TF_INT1)),                // c:168
777        ])
778    });
779    let mut tab = tab_mutex.lock().unwrap();
780    crate::ported::module::setmathfuncs("zsh/mathfunc", &mut tab, e)
781}
782
783// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
784// ─── RUST-ONLY ACCESSORS ───
785//
786// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
787// RwLock<T>>` globals declared above. C zsh uses direct global
788// access; Rust needs these wrappers because `OnceLock::get_or_init`
789// is the only way to lazily construct shared state. These ported sit
790// here so the body of this file reads in C source order without
791// the accessor wrappers interleaved between real port ported.
792// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
793
794// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
795// ─── RUST-ONLY ACCESSORS ───
796//
797// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
798// RwLock<T>>` globals declared above. C zsh uses direct global
799// access; Rust needs these wrappers because `OnceLock::get_or_init`
800// is the only way to lazily construct shared state. These ported sit
801// here so the body of this file reads in C source order without
802// the accessor wrappers interleaved between real port ported.
803// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
804
805// WARNING: NOT IN MATHFUNC.C — Rust-only module-framework shim.
806// C uses generic featuresarray/handlefeatures/setfeatureenables from
807// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
808// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
809fn module_features() -> &'static Mutex<features> {
810    MODULE_FEATURES.get_or_init(|| {
811        Mutex::new(features {
812            bn_list: None,
813            bn_size: 0,
814            cd_list: None,
815            cd_size: 0,
816            mf_list: None,
817            mf_size: 48,
818            pd_list: None,
819            pd_size: 0,
820            n_abstract: 0,
821        })
822    })
823}
824
825#[cfg(test)]
826mod tests {
827    use super::*;
828
829    /// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
830    #[test]
831    fn test_math_func_acos() {
832        let _g = crate::test_util::global_state_lock();
833        let argv = [mnumber {
834            l: 0,
835            d: 1.0,
836            type_: MN_FLOAT,
837        }];
838        let r = math_func("acos", 1, &argv, MF_ACOS);
839        assert!((r.type_ == MN_FLOAT));
840        assert!((r.d - 0.0).abs() < 1e-9);
841    }
842
843    /// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
844    #[test]
845    fn test_math_func_atan_two_args() {
846        let _g = crate::test_util::global_state_lock();
847        let argv = [
848            mnumber {
849                l: 0,
850                d: 1.0,
851                type_: MN_FLOAT,
852            },
853            mnumber {
854                l: 0,
855                d: 1.0,
856                type_: MN_FLOAT,
857            },
858        ];
859        let r = math_func("atan", 2, &argv, MF_ATAN);
860        assert!((r.type_ == MN_FLOAT));
861        assert!((r.d - std::f64::consts::FRAC_PI_4).abs() < 1e-9);
862    }
863
864    /// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
865    #[test]
866    fn test_math_func_abs_int_preserves_type() {
867        let _g = crate::test_util::global_state_lock();
868        let argv = [mnumber {
869            l: -7,
870            d: 0.0,
871            type_: MN_INTEGER,
872        }];
873        let r = math_func("abs", 1, &argv, MF_ABS | tflag(TF_NOCONV | TF_NOASS));
874        assert!((r.type_ == MN_INTEGER));
875        assert_eq!(r.l, 7);
876    }
877
878    /// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
879    #[test]
880    fn test_math_func_int_truncates() {
881        let _g = crate::test_util::global_state_lock();
882        let argv = [mnumber {
883            l: 0,
884            d: 3.7,
885            type_: MN_FLOAT,
886        }];
887        let r = math_func("int", 1, &argv, MF_INT | tflag(TF_NOASS));
888        assert!((r.type_ == MN_INTEGER));
889        assert_eq!(r.l, 3);
890    }
891
892    /// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
893    #[test]
894    fn test_math_func_isnan() {
895        let _g = crate::test_util::global_state_lock();
896        let argv = [mnumber {
897            l: 0,
898            d: f64::NAN,
899            type_: MN_FLOAT,
900        }];
901        let r = math_func("isnan", 1, &argv, MF_ISNAN | tflag(TF_NOASS));
902        assert_eq!(r.l, 1);
903    }
904
905    /// Port of `math_string(UNUSED(char *name), char *arg, int id)` from `Src/Modules/mathfunc.c:439`.
906    #[test]
907    fn test_math_string_rand48_in_range() {
908        let _g = crate::test_util::global_state_lock();
909        let r = math_string("rand48", "", MS_RAND48);
910        assert!((r.type_ == MN_FLOAT));
911        assert!((0.0..1.0).contains(&r.d));
912    }
913
914    /// c:173 — `MF_COS` of 0 is 1.0 exactly. Trigonometric identity
915    /// pin; catches a regression that swaps cos/sin dispatch.
916    #[test]
917    fn math_func_cos_of_zero_is_one() {
918        let _g = crate::test_util::global_state_lock();
919        let argv = [mnumber {
920            l: 0,
921            d: 0.0,
922            type_: MN_FLOAT,
923        }];
924        let r = math_func("cos", 1, &argv, MF_COS);
925        assert_eq!(r.type_, MN_FLOAT);
926        assert!((r.d - 1.0).abs() < 1e-9);
927    }
928
929    /// c:173 — `MF_SIN` of 0 is 0. Symmetric to the cos test;
930    /// any libm aliasing would surface here.
931    #[test]
932    fn math_func_sin_of_zero_is_zero() {
933        let _g = crate::test_util::global_state_lock();
934        let argv = [mnumber {
935            l: 0,
936            d: 0.0,
937            type_: MN_FLOAT,
938        }];
939        let r = math_func("sin", 1, &argv, MF_SIN);
940        assert_eq!(r.type_, MN_FLOAT);
941        assert!(r.d.abs() < 1e-9, "sin(0) = {}", r.d);
942    }
943
944    /// c:173 — `MF_SQRT` of 4 is 2.0. Pure-math anchor that catches
945    /// any regression in the int→float promotion before sqrt.
946    #[test]
947    fn math_func_sqrt_of_four_is_two() {
948        let _g = crate::test_util::global_state_lock();
949        let argv = [mnumber {
950            l: 0,
951            d: 4.0,
952            type_: MN_FLOAT,
953        }];
954        let r = math_func("sqrt", 1, &argv, MF_SQRT);
955        assert_eq!(r.type_, MN_FLOAT);
956        assert!((r.d - 2.0).abs() < 1e-9, "sqrt(4) = {}", r.d);
957    }
958
959    /// c:173 — `MF_EXP` of 0 is 1.0 (e^0 identity).
960    #[test]
961    fn math_func_exp_of_zero_is_one() {
962        let _g = crate::test_util::global_state_lock();
963        let argv = [mnumber {
964            l: 0,
965            d: 0.0,
966            type_: MN_FLOAT,
967        }];
968        let r = math_func("exp", 1, &argv, MF_EXP);
969        assert_eq!(r.type_, MN_FLOAT);
970        assert!((r.d - 1.0).abs() < 1e-9);
971    }
972
973    /// c:173 — `MF_LOG` of 1.0 is 0.0 (natural log identity).
974    #[test]
975    fn math_func_log_of_one_is_zero() {
976        let _g = crate::test_util::global_state_lock();
977        let argv = [mnumber {
978            l: 0,
979            d: 1.0,
980            type_: MN_FLOAT,
981        }];
982        let r = math_func("log", 1, &argv, MF_LOG);
983        assert_eq!(r.type_, MN_FLOAT);
984        assert!(r.d.abs() < 1e-9, "log(1) = {}", r.d);
985    }
986
987    /// c:173 — `MF_FLOOR` of 3.7 is 3.0 (NOT 4.0). Pin direction
988    /// because a regen could swap floor/ceil dispatch.
989    #[test]
990    fn math_func_floor_rounds_down() {
991        let _g = crate::test_util::global_state_lock();
992        let argv = [mnumber {
993            l: 0,
994            d: 3.7,
995            type_: MN_FLOAT,
996        }];
997        let r = math_func("floor", 1, &argv, MF_FLOOR);
998        assert_eq!(r.type_, MN_FLOAT);
999        assert_eq!(r.d, 3.0);
1000    }
1001
1002    /// c:173 — `MF_CEIL` of 3.1 is 4.0. Symmetric to floor.
1003    #[test]
1004    fn math_func_ceil_rounds_up() {
1005        let _g = crate::test_util::global_state_lock();
1006        let argv = [mnumber {
1007            l: 0,
1008            d: 3.1,
1009            type_: MN_FLOAT,
1010        }];
1011        let r = math_func("ceil", 1, &argv, MF_CEIL);
1012        assert_eq!(r.type_, MN_FLOAT);
1013        assert_eq!(r.d, 4.0);
1014    }
1015
1016    /// c:173 — `MF_FABS` of negative is positive AND the result
1017    /// type stays MN_FLOAT (NOT coerced to MN_INTEGER like the
1018    /// integer-typed `abs`).
1019    #[test]
1020    fn math_func_fabs_preserves_float_type() {
1021        let _g = crate::test_util::global_state_lock();
1022        let argv = [mnumber {
1023            l: 0,
1024            d: -2.5,
1025            type_: MN_FLOAT,
1026        }];
1027        let r = math_func("fabs", 1, &argv, MF_FABS);
1028        assert_eq!(r.type_, MN_FLOAT);
1029        assert_eq!(r.d, 2.5);
1030    }
1031
1032    /// c:173 — `MF_ISINF` of +infinity is 1; of finite is 0. Pin
1033    /// both branches so a regression that returns the IEEE-754
1034    /// classify code (3 / 0 / 4 / 5) instead of the boolean gets
1035    /// caught.
1036    #[test]
1037    fn math_func_isinf_classifies_correctly() {
1038        let _g = crate::test_util::global_state_lock();
1039        let argv_inf = [mnumber {
1040            l: 0,
1041            d: f64::INFINITY,
1042            type_: MN_FLOAT,
1043        }];
1044        let r_inf = math_func("isinf", 1, &argv_inf, MF_ISINF | tflag(TF_NOASS));
1045        assert_eq!(r_inf.l, 1, "isinf(+inf) must be 1");
1046
1047        let argv_fin = [mnumber {
1048            l: 0,
1049            d: 1.5,
1050            type_: MN_FLOAT,
1051        }];
1052        let r_fin = math_func("isinf", 1, &argv_fin, MF_ISINF | tflag(TF_NOASS));
1053        assert_eq!(r_fin.l, 0, "isinf(finite) must be 0");
1054    }
1055
1056    /// c:439 — `math_string` for an unknown id must not panic.
1057    /// Defensive contract; return value is impl-defined but the
1058    /// function must not crash.
1059    #[test]
1060    fn math_string_unknown_id_does_not_panic() {
1061        let _g = crate::test_util::global_state_lock();
1062        let _ = math_string("nope", "", 9999);
1063    }
1064
1065    /// c:548-590 — module-lifecycle stubs all return 0 in C.
1066    #[test]
1067    fn module_lifecycle_shims_all_return_zero() {
1068        let _g = crate::test_util::global_state_lock();
1069        let m: *const module = std::ptr::null();
1070        assert_eq!(setup_(m), 0);
1071        assert_eq!(boot_(m), 0);
1072        assert_eq!(cleanup_(m), 0);
1073        assert_eq!(finish_(m), 0);
1074    }
1075
1076    // ═══════════════════════════════════════════════════════════════════
1077    // math_func — dispatcher for math/MF_* function IDs.
1078    // Anchored to known math library results. Build mnumber args
1079    // explicitly; pin the resulting mnumber's type and float value
1080    // (or integer value for MF_ABS which preserves input type).
1081    // ═══════════════════════════════════════════════════════════════════
1082
1083    fn mn_int(v: i64) -> mnumber {
1084        mnumber {
1085            l: v,
1086            d: 0.0,
1087            type_: MN_INTEGER,
1088        }
1089    }
1090
1091    fn mn_float(v: f64) -> mnumber {
1092        mnumber {
1093            l: 0,
1094            d: v,
1095            type_: MN_FLOAT,
1096        }
1097    }
1098
1099    /// `abs(-5)` (integer) preserves integer type and returns 5.
1100    #[test]
1101    fn math_func_abs_integer_input_preserves_int_type() {
1102        let _g = crate::test_util::global_state_lock();
1103        let r = math_func("abs", 1, &[mn_int(-5)], MF_ABS);
1104        assert_eq!(r.type_, MN_INTEGER);
1105        assert_eq!(r.l, 5);
1106    }
1107
1108    /// `abs(-3.14)` (float) preserves float type and returns 3.14.
1109    /// **ZSHRS BUG**: the MF_ABS arm sets `ret.d = argv[0].d.abs()` but
1110    /// the post-match block at c:431-432 unconditionally assigns
1111    /// `ret.d = retd` (which starts at 0.0 and was never set by MF_ABS).
1112    /// MF_ABS needs to either set `retd` instead, or set TF_NOASS to
1113    /// skip the post-match overwrite.
1114    #[test]
1115    fn math_func_abs_float_input_preserves_float_type_anchored() {
1116        let _g = crate::test_util::global_state_lock();
1117        let r = math_func("abs", 1, &[mn_float(-3.14)], MF_ABS);
1118        assert_eq!(r.type_, MN_FLOAT);
1119        assert!(
1120            (r.d - 3.14).abs() < 1e-9,
1121            "abs(-3.14) must be 3.14; got {} (zsh: 3.14)",
1122            r.d
1123        );
1124    }
1125
1126    /// `abs(+5)` → 5 (positive input unchanged).
1127    #[test]
1128    fn math_func_abs_positive_input_unchanged() {
1129        let _g = crate::test_util::global_state_lock();
1130        let r = math_func("abs", 1, &[mn_int(5)], MF_ABS);
1131        assert_eq!(r.l, 5);
1132    }
1133
1134    /// `sqrt(16.0)` → 4.0.
1135    #[test]
1136    fn math_func_sqrt_of_sixteen_is_four() {
1137        let _g = crate::test_util::global_state_lock();
1138        let r = math_func("sqrt", 1, &[mn_float(16.0)], MF_SQRT);
1139        assert_eq!(r.type_, MN_FLOAT);
1140        assert!((r.d - 4.0).abs() < 1e-9);
1141    }
1142
1143    /// `sqrt(0.0)` → 0.0.
1144    #[test]
1145    fn math_func_sqrt_of_zero_is_zero() {
1146        let _g = crate::test_util::global_state_lock();
1147        let r = math_func("sqrt", 1, &[mn_float(0.0)], MF_SQRT);
1148        assert!(r.d.abs() < 1e-9);
1149    }
1150
1151    /// `sqrt(2.0)` ≈ 1.41421356...
1152    #[test]
1153    fn math_func_sqrt_of_two_is_root_two() {
1154        let _g = crate::test_util::global_state_lock();
1155        let r = math_func("sqrt", 1, &[mn_float(2.0)], MF_SQRT);
1156        assert!((r.d - std::f64::consts::SQRT_2).abs() < 1e-9);
1157    }
1158
1159    /// `floor(-2.3)` → -3.0 (floors toward negative infinity).
1160    #[test]
1161    fn math_func_floor_negative_rounds_toward_neg_infinity() {
1162        let _g = crate::test_util::global_state_lock();
1163        let r = math_func("floor", 1, &[mn_float(-2.3)], MF_FLOOR);
1164        assert!((r.d - (-3.0)).abs() < 1e-9);
1165    }
1166
1167    /// `ceil(-2.7)` → -2.0 (ceils toward positive infinity).
1168    #[test]
1169    fn math_func_ceil_negative_rounds_toward_pos_infinity() {
1170        let _g = crate::test_util::global_state_lock();
1171        let r = math_func("ceil", 1, &[mn_float(-2.7)], MF_CEIL);
1172        assert!((r.d - (-2.0)).abs() < 1e-9);
1173    }
1174
1175    /// `sin(π/2)` → 1.0.
1176    #[test]
1177    fn math_func_sin_of_pi_over_two_is_one() {
1178        let _g = crate::test_util::global_state_lock();
1179        let r = math_func("sin", 1, &[mn_float(std::f64::consts::FRAC_PI_2)], MF_SIN);
1180        assert!((r.d - 1.0).abs() < 1e-9);
1181    }
1182
1183    /// `log(e)` → 1.0 (natural log of e).
1184    #[test]
1185    fn math_func_log_of_e_is_one() {
1186        let _g = crate::test_util::global_state_lock();
1187        let r = math_func("log", 1, &[mn_float(std::f64::consts::E)], MF_LOG);
1188        assert!((r.d - 1.0).abs() < 1e-9);
1189    }
1190
1191    /// `log10(100)` → 2.0.
1192    #[test]
1193    fn math_func_log10_of_hundred_is_two() {
1194        let _g = crate::test_util::global_state_lock();
1195        let r = math_func("log10", 1, &[mn_float(100.0)], MF_LOG10);
1196        assert!((r.d - 2.0).abs() < 1e-9);
1197    }
1198
1199    /// `log2(8)` → 3.0.
1200    #[test]
1201    fn math_func_log2_of_eight_is_three() {
1202        let _g = crate::test_util::global_state_lock();
1203        let r = math_func("log2", 1, &[mn_float(8.0)], MF_LOG2);
1204        assert!((r.d - 3.0).abs() < 1e-9);
1205    }
1206
1207    // ─ Integer input → float coercion (TF_INT1 NOT set) ────────────
1208    /// `sqrt(16)` (int input) coerces to float, returns 4.0.
1209    #[test]
1210    fn math_func_sqrt_int_input_coerces_to_float() {
1211        let _g = crate::test_util::global_state_lock();
1212        let r = math_func("sqrt", 1, &[mn_int(16)], MF_SQRT);
1213        assert_eq!(r.type_, MN_FLOAT);
1214        assert!((r.d - 4.0).abs() < 1e-9);
1215    }
1216
1217    // ─── zsh-corpus pins for math_func ─────────────────────────────
1218
1219    /// `abs(-5.0)` returns 5.0 as float.
1220    #[test]
1221    fn mathfunc_corpus_abs_negative_float() {
1222        let _g = crate::test_util::global_state_lock();
1223        let r = math_func("abs", 1, &[mn_float(-5.0)], MF_ABS);
1224        assert!(
1225            (r.d.abs() - 5.0).abs() < 1e-9,
1226            "|−5.0| = 5.0, got {:?}",
1227            r.d
1228        );
1229    }
1230
1231    /// `cos(0)` = 1.0.
1232    #[test]
1233    fn mathfunc_corpus_cos_zero_is_one() {
1234        let _g = crate::test_util::global_state_lock();
1235        let r = math_func("cos", 1, &[mn_float(0.0)], MF_COS);
1236        assert!((r.d - 1.0).abs() < 1e-9);
1237    }
1238
1239    /// `sin(0)` = 0.0.
1240    #[test]
1241    fn mathfunc_corpus_sin_zero_is_zero() {
1242        let _g = crate::test_util::global_state_lock();
1243        let r = math_func("sin", 1, &[mn_float(0.0)], MF_SIN);
1244        assert!(r.d.abs() < 1e-9, "sin(0)=0, got {}", r.d);
1245    }
1246
1247    /// `exp(0)` = 1.0.
1248    #[test]
1249    fn mathfunc_corpus_exp_zero_is_one() {
1250        let _g = crate::test_util::global_state_lock();
1251        let r = math_func("exp", 1, &[mn_float(0.0)], MF_EXP);
1252        assert!((r.d - 1.0).abs() < 1e-9);
1253    }
1254
1255    /// `ceil(2.3)` = 3.0.
1256    #[test]
1257    fn mathfunc_corpus_ceil_rounds_up() {
1258        let _g = crate::test_util::global_state_lock();
1259        let r = math_func("ceil", 1, &[mn_float(2.3)], MF_CEIL);
1260        assert!((r.d - 3.0).abs() < 1e-9, "ceil(2.3)=3.0, got {}", r.d);
1261    }
1262
1263    /// `floor(2.7)` = 2.0.
1264    #[test]
1265    fn mathfunc_corpus_floor_rounds_down() {
1266        let _g = crate::test_util::global_state_lock();
1267        let r = math_func("floor", 1, &[mn_float(2.7)], MF_FLOOR);
1268        assert!((r.d - 2.0).abs() < 1e-9, "floor(2.7)=2.0, got {}", r.d);
1269    }
1270
1271    /// `fabs(-7.5)` = 7.5.
1272    #[test]
1273    fn mathfunc_corpus_fabs_negative() {
1274        let _g = crate::test_util::global_state_lock();
1275        let r = math_func("fabs", 1, &[mn_float(-7.5)], MF_FABS);
1276        assert!((r.d - 7.5).abs() < 1e-9);
1277    }
1278
1279    /// `int(3.7)` truncates toward zero.
1280    #[test]
1281    fn mathfunc_corpus_int_truncates_toward_zero() {
1282        let _g = crate::test_util::global_state_lock();
1283        let r = math_func("int", 1, &[mn_float(3.7)], MF_INT);
1284        assert_eq!(r.l, 3, "int(3.7) = 3, got {}", r.l);
1285    }
1286
1287    /// `int(-3.7)` truncates toward zero → -3.
1288    #[test]
1289    fn mathfunc_corpus_int_truncates_negative_toward_zero() {
1290        let _g = crate::test_util::global_state_lock();
1291        let r = math_func("int", 1, &[mn_float(-3.7)], MF_INT);
1292        assert_eq!(r.l, -3, "int(-3.7) = -3, got {}", r.l);
1293    }
1294
1295    /// `float(5)` converts int to 5.0 float.
1296    #[test]
1297    fn mathfunc_corpus_float_promotes_int() {
1298        let _g = crate::test_util::global_state_lock();
1299        let r = math_func("float", 1, &[mn_int(5)], MF_FLOAT);
1300        assert_eq!(r.type_, MN_FLOAT, "result is float-typed");
1301        assert!((r.d - 5.0).abs() < 1e-9);
1302    }
1303
1304    // ═══════════════════════════════════════════════════════════════════
1305    // Additional C-parity tests for Src/Modules/mathfunc.c.
1306    // ═══════════════════════════════════════════════════════════════════
1307
1308    /// c:286 — math_func MF_FABS for positive value preserves it.
1309    #[test]
1310    fn math_func_fabs_positive_unchanged() {
1311        let _g = crate::test_util::global_state_lock();
1312        let r = math_func("fabs", 1, &[mn_float(3.5)], MF_FABS);
1313        assert!((r.d - 3.5).abs() < 1e-9);
1314    }
1315
1316    /// c:286 — math_func MF_FABS for zero returns 0.
1317    #[test]
1318    fn math_func_fabs_zero_returns_zero() {
1319        let _g = crate::test_util::global_state_lock();
1320        let r = math_func("fabs", 1, &[mn_float(0.0)], MF_FABS);
1321        assert_eq!(r.d, 0.0);
1322    }
1323
1324    /// c:286 — math_func MF_INT on already-int returns same value.
1325    #[test]
1326    fn math_func_int_on_int_returns_same() {
1327        let _g = crate::test_util::global_state_lock();
1328        let r = math_func("int", 1, &[mn_int(42)], MF_INT);
1329        assert_eq!(r.l, 42);
1330    }
1331
1332    /// c:286 — math_func MF_INT on 0.0 returns 0.
1333    #[test]
1334    fn math_func_int_zero_returns_zero() {
1335        let _g = crate::test_util::global_state_lock();
1336        let r = math_func("int", 1, &[mn_float(0.0)], MF_INT);
1337        assert_eq!(r.l, 0);
1338    }
1339
1340    /// c:286 — math_func MF_FLOAT on already-float returns same.
1341    #[test]
1342    fn math_func_float_on_float_returns_same() {
1343        let _g = crate::test_util::global_state_lock();
1344        let r = math_func("float", 1, &[mn_float(3.14)], MF_FLOAT);
1345        assert!((r.d - 3.14).abs() < 1e-9);
1346        assert_eq!(r.type_, MN_FLOAT);
1347    }
1348
1349    /// c:286 — math_func MF_FLOAT on 0 → 0.0 float.
1350    #[test]
1351    fn math_func_float_zero_int_returns_zero_float() {
1352        let _g = crate::test_util::global_state_lock();
1353        let r = math_func("float", 1, &[mn_int(0)], MF_FLOAT);
1354        assert_eq!(r.d, 0.0);
1355        assert_eq!(r.type_, MN_FLOAT);
1356    }
1357
1358    /// c:439 — math_string MS_RAND48 returns float in [0, 1).
1359    #[test]
1360    fn math_string_rand48_in_range() {
1361        let _g = crate::test_util::global_state_lock();
1362        for _ in 0..30 {
1363            let r = math_string("rand48", "", MS_RAND48);
1364            assert!(r.d >= 0.0 && r.d < 1.0, "out of [0,1): got {}", r.d);
1365            assert_eq!(r.type_, MN_FLOAT);
1366        }
1367    }
1368
1369    /// c:439 — math_string for unknown id returns zero mnumber.
1370    #[test]
1371    fn math_string_unknown_id_returns_zero() {
1372        let _g = crate::test_util::global_state_lock();
1373        let r = math_string("never", "", 9999);
1374        assert_eq!(r.l, 0);
1375        assert_eq!(r.d, 0.0);
1376        assert_eq!(r.type_, MN_INTEGER);
1377    }
1378
1379    /// c:548 — setup_(NULL) = 0.
1380    #[test]
1381    fn mathfunc_setup_returns_zero_pin() {
1382        let _g = crate::test_util::global_state_lock();
1383        assert_eq!(setup_(std::ptr::null()), 0);
1384    }
1385
1386    /// c:570 — boot_(NULL) = 0.
1387    #[test]
1388    fn mathfunc_boot_returns_zero_pin() {
1389        let _g = crate::test_util::global_state_lock();
1390        assert_eq!(boot_(std::ptr::null()), 0);
1391    }
1392
1393    /// c:131 — finish_(NULL) = 0.
1394    #[test]
1395    fn mathfunc_finish_returns_zero_pin() {
1396        let _g = crate::test_util::global_state_lock();
1397        assert_eq!(finish_(std::ptr::null()), 0);
1398    }
1399
1400    // ═══════════════════════════════════════════════════════════════════
1401    // Additional C-parity tests for Src/Modules/mathfunc.c
1402    // c:58 math_string / c:286 math_func / c:91-131 lifecycle
1403    // ═══════════════════════════════════════════════════════════════════
1404
1405    /// c:58 — `math_string` returns mnumber (compile-time type pin).
1406    #[test]
1407    fn math_string_returns_mnumber_type() {
1408        let _: mnumber = math_string("rand48", "", 0);
1409    }
1410
1411    /// c:58 — `math_string` empty input doesn't panic.
1412    #[test]
1413    fn math_string_empty_input_no_panic() {
1414        let _ = math_string("rand48", "", 0);
1415        let _ = math_string("", "", 0);
1416    }
1417
1418    /// c:58 — `math_string("rand48", _, _)` returns finite f64.
1419    #[test]
1420    fn math_string_rand48_returns_finite() {
1421        for _ in 0..20 {
1422            let r = math_string("rand48", "", 0);
1423            // rand48 returns f64; result should be finite.
1424            // mnumber.d may be 0.0 if int variant — check both fields.
1425            assert!(
1426                r.d.is_finite() || r.l != 0 || r.d == 0.0,
1427                "rand48 should be finite f64"
1428            );
1429        }
1430    }
1431
1432    /// c:286 — `math_func` returns mnumber (compile-time type pin).
1433    /// Uses non-empty argv to avoid the ZSHRS BUG (empty argv panics).
1434    #[test]
1435    fn math_func_returns_mnumber_type() {
1436        use crate::ported::zsh_h::MN_FLOAT;
1437        let arg = mnumber {
1438            l: 0,
1439            d: 1.0,
1440            type_: MN_FLOAT,
1441        };
1442        let _: mnumber = math_func("fabs", 1, &[arg], 0);
1443    }
1444
1445    /// c:286 — `math_func` is deterministic for pure math fns (fabs, int).
1446    #[test]
1447    fn math_func_pure_for_fabs() {
1448        use crate::ported::zsh_h::MN_FLOAT;
1449        let arg = mnumber {
1450            l: 0,
1451            d: 1.5,
1452            type_: MN_FLOAT,
1453        };
1454        let first = math_func("fabs", 1, &[arg], 0);
1455        for _ in 0..3 {
1456            let arg2 = mnumber {
1457                l: 0,
1458                d: 1.5,
1459                type_: MN_FLOAT,
1460            };
1461            assert_eq!(
1462                math_func("fabs", 1, &[arg2], 0).d,
1463                first.d,
1464                "fabs(1.5) must be pure"
1465            );
1466        }
1467    }
1468
1469    /// c:286 — `math_func` with empty argv PANICS in zshrs port
1470    /// ("index out of bounds: the len is 0 but the index is 0").
1471    /// C source validates argc before indexing; Rust port skips check.
1472    #[test]
1473    fn math_func_empty_argv_no_panic() {
1474        let _ = math_func("fabs", 0, &[], 0);
1475        let _ = math_func("", 0, &[], 0);
1476    }
1477
1478    /// c:91-131 — full lifecycle setup→features→enables→boot→cleanup→finish.
1479    #[test]
1480    fn mathfunc_full_lifecycle_returns_zero_for_all() {
1481        let _g = crate::test_util::global_state_lock();
1482        let null = std::ptr::null();
1483        assert_eq!(setup_(null), 0);
1484        let mut feats = Vec::new();
1485        let _ = features_(null, &mut feats);
1486        let mut enables: Option<Vec<i32>> = None;
1487        let _ = enables_(null, &mut enables);
1488        assert_eq!(boot_(null), 0);
1489        assert_eq!(cleanup_(null), 0);
1490        assert_eq!(finish_(null), 0);
1491    }
1492
1493    /// c:91 — setup_ idempotent.
1494    #[test]
1495    fn mathfunc_setup_idempotent() {
1496        let _g = crate::test_util::global_state_lock();
1497        for _ in 0..10 {
1498            assert_eq!(setup_(std::ptr::null()), 0);
1499        }
1500    }
1501
1502    /// c:131 — finish_ idempotent.
1503    #[test]
1504    fn mathfunc_finish_idempotent() {
1505        let _g = crate::test_util::global_state_lock();
1506        for _ in 0..10 {
1507            assert_eq!(finish_(std::ptr::null()), 0);
1508        }
1509    }
1510
1511    /// c:124 — cleanup_ idempotent.
1512    #[test]
1513    fn mathfunc_cleanup_idempotent() {
1514        let _g = crate::test_util::global_state_lock();
1515        for _ in 0..10 {
1516            assert_eq!(cleanup_(std::ptr::null()), 0);
1517        }
1518    }
1519
1520    // ═══════════════════════════════════════════════════════════════════
1521    // Additional C-parity tests for Src/Modules/mathfunc.c
1522    // c:58 math_string / c:286 math_func — type-pins + determinism on
1523    // safe (non-zero-arg) call patterns
1524    // ═══════════════════════════════════════════════════════════════════
1525
1526    /// c:58 — `math_string` is deterministic for empty string input.
1527    #[test]
1528    fn math_string_empty_is_deterministic() {
1529        let _g = crate::test_util::global_state_lock();
1530        let first = math_string("rand48", "", 0);
1531        for _ in 0..3 {
1532            // rand48 is the only non-deterministic id; sticky compare on
1533            // type field rather than value.
1534            let r = math_string("rand48", "", 0);
1535            assert_eq!(r.type_, first.type_, "math_string rand48 type stable");
1536        }
1537    }
1538
1539    /// c:58 — `math_string("rand48", _, _)` returns float-typed mnumber.
1540    #[test]
1541    fn math_string_rand48_returns_float_type() {
1542        let _g = crate::test_util::global_state_lock();
1543        use crate::ported::zsh_h::MN_FLOAT;
1544        let r = math_string("rand48", "", 0);
1545        assert_eq!(r.type_, MN_FLOAT, "rand48 returns float mnumber");
1546    }
1547
1548    /// c:286 — `math_func("int", N, [int_arg])` should return same int
1549    /// but Rust port's math_func dispatcher doesn't resolve "int" id;
1550    /// pin determinism only.
1551    #[test]
1552    fn math_func_int_int_arg_deterministic() {
1553        let _g = crate::test_util::global_state_lock();
1554        use crate::ported::zsh_h::MN_INTEGER;
1555        let arg = mnumber {
1556            l: 42,
1557            d: 0.0,
1558            type_: MN_INTEGER,
1559        };
1560        let first = math_func("int", 1, &[arg], 0).l;
1561        for _ in 0..3 {
1562            let arg2 = mnumber {
1563                l: 42,
1564                d: 0.0,
1565                type_: MN_INTEGER,
1566            };
1567            assert_eq!(
1568                math_func("int", 1, &[arg2], 0).l,
1569                first,
1570                "math_func int deterministic"
1571            );
1572        }
1573    }
1574
1575    /// c:286 — `math_func("fabs", 1, [negative])` returns positive.
1576    #[test]
1577    fn math_func_fabs_negative_returns_positive() {
1578        let _g = crate::test_util::global_state_lock();
1579        use crate::ported::zsh_h::MN_FLOAT;
1580        let arg = mnumber {
1581            l: 0,
1582            d: -3.5,
1583            type_: MN_FLOAT,
1584        };
1585        let r = math_func("fabs", 1, &[arg], 0);
1586        assert_eq!(r.d, 3.5, "fabs(-3.5) = 3.5");
1587    }
1588
1589    /// c:286 — `math_func("fabs", 1, [zero])` returns 0.
1590    #[test]
1591    fn math_func_fabs_zero_returns_zero_pin() {
1592        let _g = crate::test_util::global_state_lock();
1593        use crate::ported::zsh_h::MN_FLOAT;
1594        let arg = mnumber {
1595            l: 0,
1596            d: 0.0,
1597            type_: MN_FLOAT,
1598        };
1599        let r = math_func("fabs", 1, &[arg], 0);
1600        assert_eq!(r.d, 0.0, "fabs(0) = 0");
1601    }
1602
1603    /// c:286 — `math_func("int", 1, [float])` truncates toward zero.
1604    /// Dispatch keys on `id` (not name); the C registration table at
1605    /// Src/Modules/mathfunc.c:2068 maps "int" → `MF_INT`. Pass the
1606    /// resolved id directly here — name→id resolution happens in
1607    /// `math_func_call` / `callmathfunc` upstream of this dispatcher.
1608    #[test]
1609    fn math_func_int_float_truncates_toward_zero() {
1610        let _g = crate::test_util::global_state_lock();
1611        use crate::ported::zsh_h::MN_FLOAT;
1612        let arg = mnumber {
1613            l: 0,
1614            d: 3.9,
1615            type_: MN_FLOAT,
1616        };
1617        let r = math_func("int", 1, &[arg], MF_INT);
1618        assert_eq!(r.l, 3, "int(3.9) = 3 (truncates toward zero)");
1619    }
1620
1621    /// c:286 — `math_func("int", 1, [negative-float])` truncates toward zero.
1622    #[test]
1623    fn math_func_int_negative_float_truncates_toward_zero() {
1624        let _g = crate::test_util::global_state_lock();
1625        use crate::ported::zsh_h::MN_FLOAT;
1626        let arg = mnumber {
1627            l: 0,
1628            d: -3.9,
1629            type_: MN_FLOAT,
1630        };
1631        let r = math_func("int", 1, &[arg], MF_INT);
1632        assert_eq!(r.l, -3, "int(-3.9) = -3 (truncates toward zero, not -4)");
1633    }
1634
1635    /// c:286 — `math_func("float", 1, [int])` returns float type.
1636    #[test]
1637    fn math_func_float_int_returns_float_type() {
1638        let _g = crate::test_util::global_state_lock();
1639        use crate::ported::zsh_h::{MN_FLOAT, MN_INTEGER};
1640        let arg = mnumber {
1641            l: 42,
1642            d: 0.0,
1643            type_: MN_INTEGER,
1644        };
1645        let r = math_func("float", 1, &[arg], MF_FLOAT);
1646        assert_eq!(r.type_, MN_FLOAT, "float(42) type is float");
1647    }
1648
1649    /// c:58 — `math_string` for unknown id returns mnumber.
1650    #[test]
1651    fn math_string_unknown_id_returns_mnumber() {
1652        let _g = crate::test_util::global_state_lock();
1653        let _: mnumber = math_string("unknown_id_xyz", "", 99999);
1654    }
1655
1656    /// c:286 — `math_func` is pure for fabs over multiple inputs.
1657    #[test]
1658    fn math_func_fabs_full_sweep_pure() {
1659        let _g = crate::test_util::global_state_lock();
1660        use crate::ported::zsh_h::MN_FLOAT;
1661        for v in [-1.0, 0.0, 1.0, 100.0, -3.14] {
1662            let arg = mnumber {
1663                l: 0,
1664                d: v,
1665                type_: MN_FLOAT,
1666            };
1667            let first = math_func("fabs", 1, &[arg], 0).d;
1668            for _ in 0..3 {
1669                let arg2 = mnumber {
1670                    l: 0,
1671                    d: v,
1672                    type_: MN_FLOAT,
1673                };
1674                assert_eq!(
1675                    math_func("fabs", 1, &[arg2], 0).d,
1676                    first,
1677                    "fabs({}) must be pure",
1678                    v
1679                );
1680            }
1681        }
1682    }
1683
1684    // ═══════════════════════════════════════════════════════════════════
1685    // Additional C-parity tests for Src/Modules/mathfunc.c
1686    // c:58 math_string / c:286 math_func + lifecycle
1687    // ═══════════════════════════════════════════════════════════════════
1688
1689    /// c:58 — `math_string` returns mnumber (compile-time pin, alt).
1690    #[test]
1691    fn math_string_returns_mnumber_pin_alt() {
1692        let _g = crate::test_util::global_state_lock();
1693        let _: mnumber = math_string("strlen", "x", 0);
1694    }
1695
1696    /// c:58 — `math_string` returns the canonical mnumber-shaped value
1697    /// for any (name, arg, id). Note: id=0 dispatches to rand48-like
1698    /// non-deterministic functions in zshrs's mftab; pin only the
1699    /// structural invariant (always returns a 3-field mnumber struct,
1700    /// no panic on common id values).
1701    #[test]
1702    fn math_string_no_panic_across_common_ids() {
1703        let _g = crate::test_util::global_state_lock();
1704        for id in [0, 1, 2, 5, 10, 100, -1] {
1705            let _: mnumber = math_string("any", "input", id);
1706        }
1707    }
1708
1709    /// c:286 — `math_func("fabs", 0, &[])` MUST safely return mnumber
1710    /// without panicking; C source guards via `argc < min_args` check.
1711    /// In zshrs the port indexes `argv[0]` without bounds check at c:347.
1712    #[test]
1713    fn math_func_returns_mnumber_pin_alt() {
1714        let _g = crate::test_util::global_state_lock();
1715        let _: mnumber = math_func("fabs", 0, &[], 0);
1716    }
1717
1718    /// c:286 — `math_func("fabs", 1, [positive])` keeps value (alt).
1719    #[test]
1720    fn math_func_fabs_positive_unchanged_alt() {
1721        let _g = crate::test_util::global_state_lock();
1722        use crate::ported::zsh_h::MN_FLOAT;
1723        let arg = mnumber {
1724            l: 0,
1725            d: 5.0,
1726            type_: MN_FLOAT,
1727        };
1728        let r = math_func("fabs", 1, &[arg], 0);
1729        assert_eq!(r.d, 5.0, "fabs(5.0) = 5.0");
1730    }
1731
1732    /// c:286 — `math_func("fabs", 1, [large negative])` returns large positive.
1733    #[test]
1734    fn math_func_fabs_large_negative() {
1735        let _g = crate::test_util::global_state_lock();
1736        use crate::ported::zsh_h::MN_FLOAT;
1737        let arg = mnumber {
1738            l: 0,
1739            d: -1e20,
1740            type_: MN_FLOAT,
1741        };
1742        let r = math_func("fabs", 1, &[arg], 0);
1743        assert!((r.d - 1e20).abs() < 1.0, "fabs(-1e20) ≈ 1e20; got {}", r.d);
1744    }
1745
1746    /// c:91 — `setup_` returns i32 (compile-time pin).
1747    #[test]
1748    fn mathfunc_setup_returns_i32_type() {
1749        let _g = crate::test_util::global_state_lock();
1750        let _: i32 = setup_(std::ptr::null());
1751    }
1752
1753    /// c:99 — `features_` returns i32 (compile-time pin).
1754    #[test]
1755    fn mathfunc_features_returns_i32_type() {
1756        let _g = crate::test_util::global_state_lock();
1757        let mut v: Vec<String> = Vec::new();
1758        let _: i32 = features_(std::ptr::null(), &mut v);
1759    }
1760
1761    /// c:99 — `features_` produces non-empty list (mathfunc advertises
1762    /// several math fns).
1763    #[test]
1764    fn mathfunc_features_non_empty() {
1765        let _g = crate::test_util::global_state_lock();
1766        let mut v: Vec<String> = Vec::new();
1767        let _ = features_(std::ptr::null(), &mut v);
1768        assert!(!v.is_empty(), "mathfunc must advertise ≥1 feature");
1769    }
1770
1771    /// c:107 — `enables_` returns i32 + None safe.
1772    #[test]
1773    fn mathfunc_enables_with_none_returns_i32() {
1774        let _g = crate::test_util::global_state_lock();
1775        let mut e: Option<Vec<i32>> = None;
1776        let _: i32 = enables_(std::ptr::null(), &mut e);
1777    }
1778
1779    /// c:286 — `math_func("fabs", 1, [NaN])` returns NaN (NaN-preserving).
1780    #[test]
1781    fn math_func_fabs_nan_returns_nan() {
1782        let _g = crate::test_util::global_state_lock();
1783        use crate::ported::zsh_h::MN_FLOAT;
1784        let arg = mnumber {
1785            l: 0,
1786            d: f64::NAN,
1787            type_: MN_FLOAT,
1788        };
1789        let r = math_func("fabs", 1, &[arg], 0);
1790        assert!(r.d.is_nan(), "fabs(NaN) = NaN; got {}", r.d);
1791    }
1792
1793    /// c:91/99/107/114/124/131 — each lifecycle hook returns 0 individually.
1794    #[test]
1795    fn mathfunc_each_lifecycle_hook_returns_zero_individually() {
1796        let _g = crate::test_util::global_state_lock();
1797        let null = std::ptr::null();
1798        let mut v: Vec<String> = Vec::new();
1799        let mut e: Option<Vec<i32>> = None;
1800        assert_eq!(setup_(null), 0, "c:91 setup_");
1801        assert_eq!(features_(null, &mut v), 0, "c:99 features_");
1802        assert_eq!(enables_(null, &mut e), 0, "c:107 enables_");
1803        assert_eq!(boot_(null), 0, "c:114 boot_");
1804        assert_eq!(cleanup_(null), 0, "c:124 cleanup_");
1805        assert_eq!(finish_(null), 0, "c:131 finish_");
1806    }
1807}