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