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};
15
16// libm bindings used by the math-function dispatcher. Direct port
17// of the calls C's `math_func()` (Src/Modules/mathfunc.c:172-436)
18// makes via `<math.h>`. Bessel functions and `erf` aren't in
19// Rust's `std`, so we declare the C ABI bindings here.
20#[cfg(unix)]
21extern "C" {
22    fn j0(x: f64) -> f64;
23    fn j1(x: f64) -> f64;
24    fn jn(n: i32, x: f64) -> f64;
25    fn y0(x: f64) -> f64;
26    fn y1(x: f64) -> f64;
27    fn yn(n: i32, x: f64) -> f64;
28    fn erf(x: f64) -> f64;
29    fn erfc(x: f64) -> f64;
30    fn lgamma(x: f64) -> f64;
31    fn tgamma(x: f64) -> f64;
32    fn ilogb(x: f64) -> i32;
33    fn logb(x: f64) -> f64;
34    fn nextafter(x: f64, y: f64) -> f64;
35    fn rint(x: f64) -> f64;
36    fn scalbn(x: f64, n: i32) -> f64;
37    fn ldexp(x: f64, exp: i32) -> f64;
38    fn copysign(x: f64, y: f64) -> f64;
39    fn expm1(x: f64) -> f64;
40    fn log1p(x: f64) -> f64;
41    fn cbrt(x: f64) -> f64;
42}
43
44/// Port of `math_string(UNUSED(char *name), char *arg, int id)` from `Src/Modules/mathfunc.c:439`. The
45/// string-arg math-fn dispatcher behind `rand48("seedvar")` and
46/// future string-takers. C signature:
47///   `static mnumber math_string(char *name, char *arg, int id)`
48///
49/// Strips leading/trailing iblank from `arg` (mathfunc.c:447-451)
50/// then switches on `id`. Currently only `MS_RAND48` exists; the
51/// random-bit production lives in `crate::ported::random` and
52/// `crate::ported::modules::random_real`. Returns `zero_mnumber`
53/// for unrecognised ids (matching C's pre-init `ret = zero_mnumber`).
54#[allow(unused_variables)]
55pub fn math_string(name: &str, arg: &str, id: i32) -> mnumber {         // c:439
56    let trimmed = arg.trim_matches(|c: char| c == ' ' || c == '\t');     // c:439-451 iblank
57    match id {
58        MS_RAND48 => {                                                   // c:457
59            // C decodes optional 12-hex seedstr from $seedvar then
60            // calls erand48(). zshrs uses `random_real()` which
61            // already produces uniform doubles via the OS-entropy
62            // path; the seed-from-param wiring is pending param-
63            // table integration.
64            let _ = trimmed;
65            mnumber {
66                l: 0,
67                d: crate::ported::modules::random_real::random_real(),
68                type_: MN_FLOAT,
69            }
70        }
71        _ => mnumber { l: 0, d: 0.0, type_: MN_INTEGER },                                         // zero_mnumber
72    }
73}
74
75// `mftab` — port of `static struct mathfunc mftab[]` (mathfunc.c:497).
76
77
78// `module_features` — port of `static struct features module_features`
79// from mathfunc.c:540.
80
81
82
83/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/mathfunc.c:548`.
84#[allow(unused_variables)]
85pub fn setup_(m: *const module) -> i32 {                                    // c:548
86    // C body c:550-551 — `return 0`. Faithful empty-body port.
87    0
88}
89
90/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/mathfunc.c:555`.
91/// C body: `*features = featuresarray(m, &module_features); return 0;`
92pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {     // c:555
93    *features = featuresarray(m, module_features());
94    0                                                                    // c:570
95}
96
97/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/mathfunc.c:563`.
98/// C body: `return handlefeatures(m, &module_features, enables);`
99pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {  // c:563
100    handlefeatures(m, module_features(), enables) // c:570
101}
102
103/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/mathfunc.c:570`.
104#[allow(unused_variables)]
105pub fn boot_(m: *const module) -> i32 {                                     // c:570
106    // C body c:572-573 — `return 0`. Faithful empty-body port; the
107    //                    math functions are registered via the mf_list
108    //                    feature dispatch, no extra boot work needed.
109    0
110}
111
112/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/mathfunc.c:577`.
113/// C body: `return setfeatureenables(m, &module_features, NULL);`
114pub fn cleanup_(m: *const module) -> i32 {                                  // c:577
115    setfeatureenables(m, module_features(), None) // c:584
116}
117
118/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/mathfunc.c:584`.
119#[allow(unused_variables)]
120pub fn finish_(m: *const module) -> i32 {                                   // c:584
121    // C body c:586-587 — `return 0`. Faithful empty-body port; the
122    //                    math functions are unregistered via cleanup_.
123    0
124}
125
126// ============================================================
127// MF_* — port of the anonymous `enum {}` at mathfunc.c:34-84.
128// C `enum {}` with no typedef → untyped int constants. Rust
129// mirrors as `pub const ... : i32` (no Rust-only enum type).
130// ============================================================
131pub const MF_ABS:       i32 = 0;                                         // c:35
132pub const MF_ACOS:      i32 = 1;                                         // c:36
133pub const MF_ACOSH:     i32 = 2;
134pub const MF_ASIN:      i32 = 3;
135pub const MF_ASINH:     i32 = 4;
136pub const MF_ATAN:      i32 = 5;
137pub const MF_ATANH:     i32 = 6;
138pub const MF_CBRT:      i32 = 7;
139pub const MF_CEIL:      i32 = 8;
140pub const MF_COPYSIGN:  i32 = 9;
141pub const MF_COS:       i32 = 10;
142pub const MF_COSH:      i32 = 11;
143pub const MF_ERF:       i32 = 12;
144pub const MF_ERFC:      i32 = 13;
145pub const MF_EXP:       i32 = 14;
146pub const MF_EXPM1:     i32 = 15;
147pub const MF_FABS:      i32 = 16;
148pub const MF_FLOAT:     i32 = 17;
149pub const MF_FLOOR:     i32 = 18;
150pub const MF_FMOD:      i32 = 19;
151pub const MF_GAMMA:     i32 = 20;
152pub const MF_HYPOT:     i32 = 21;
153pub const MF_ILOGB:     i32 = 22;
154pub const MF_INT:       i32 = 23;
155pub const MF_ISINF:     i32 = 24;
156pub const MF_ISNAN:     i32 = 25;
157pub const MF_J0:        i32 = 26;
158pub const MF_J1:        i32 = 27;
159pub const MF_JN:        i32 = 28;
160pub const MF_LDEXP:     i32 = 29;
161pub const MF_LGAMMA:    i32 = 30;
162pub const MF_LOG:       i32 = 31;
163pub const MF_LOG10:     i32 = 32;
164pub const MF_LOG1P:     i32 = 33;
165pub const MF_LOG2:      i32 = 34;
166pub const MF_LOGB:      i32 = 35;
167pub const MF_NEXTAFTER: i32 = 36;
168pub const MF_RINT:      i32 = 37;
169pub const MF_SCALB:     i32 = 38;
170pub const MF_SIGNGAM:   i32 = 39;                                        // c:75 #ifdef HAVE_SIGNGAM
171pub const MF_SIN:       i32 = 40;
172pub const MF_SINH:      i32 = 41;
173pub const MF_SQRT:      i32 = 42;
174pub const MF_TAN:       i32 = 43;
175pub const MF_TANH:      i32 = 44;
176pub const MF_Y0:        i32 = 45;
177pub const MF_Y1:        i32 = 46;
178pub const MF_YN:        i32 = 47;                                        // c:84
179
180
181
182// =====================================================================
183// static struct mathfunc mftab[]                                    c:497
184// static struct features module_features                            c:540
185// =====================================================================
186
187use crate::ported::zsh_h::module;
188
189// ============================================================
190// MS_* — port of the anonymous `enum {}` at mathfunc.c:90.
191// String-arg math-fn ids.
192// ============================================================
193pub const MS_RAND48: i32 = 0;                                            // c:91
194
195// ============================================================
196// TF_* — port of the anonymous `enum {}` at mathfunc.c:104.
197// Type-flag bits, individually testable.
198// ============================================================
199pub const TF_NOCONV: i32 = 1;                                            // c:106 don't convert to float
200pub const TF_INT1:   i32 = 2;                                            // c:107 first arg is integer
201pub const TF_INT2:   i32 = 4;                                            // c:108 second arg is integer
202pub const TF_NOASS:  i32 = 8;                                            // c:109 don't assign result as double
203
204/// Port of the `TFLAG(x)` macro from `mathfunc.c:113`.
205/// `#define TFLAG(x) ((x) << 8)`. Shifts the type-flag bits into
206/// the high byte of the `id` arg passed to `math_func()` so the
207/// MF_* numeric ids can occupy the low byte.
208pub const fn tflag(x: i32) -> i32 { x << 8 }                             // c:113
209
210/// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`. The
211/// dispatcher behind every numeric math fn registered via
212/// `NUMMATHFUNC` in `mftab[]` (mathfunc.c:115-167).
213///
214/// C signature:
215///   `static mnumber math_func(char *name, int argc, mnumber *argv, int id)`
216///
217/// Matches that signature exactly: `name` is unused (UNUSED in C);
218/// `argc` is the actual argument count; `argv` is the slice of
219/// argument values; `id` is the MF_* function id ORed with TFLAG()
220/// type flags in its high byte.
221#[allow(non_snake_case)]
222/// WARNING: param names don't match C — Rust=(_name, argc, argv, id) vs C=(name, argc, argv, id)
223pub fn math_func(_name: &str, argc: i32, argv: &[mnumber], id: i32) -> mnumber {  // c:173
224    let mut ret = mnumber { l: 0, d: 0.0, type_: MN_FLOAT };             // c:173,193
225    let mut argd: f64 = 0.0;                                             // c:175
226    let mut argd2: f64 = 0.0;                                            // c:175
227    let mut argi: i32 = 0;                                               // c:176
228
229    // Type-coerce argv[0] (and argv[1]) per the TF_INT1/TF_INT2/
230    // TF_NOCONV flag bits — c:178-191.
231    if argc > 0 && (id & tflag(TF_NOCONV)) == 0 {                        // c:178
232        if (id & tflag(TF_INT1)) != 0 {                                  // c:179
233            argi = if argv[0].type_ == MN_FLOAT {
234                argv[0].d as i32                                         // c:180
235            } else {
236                argv[0].l as i32
237            };
238        } else {                                                         // c:181
239            argd = if argv[0].type_ == MN_INTEGER {
240                argv[0].l as f64                                         // c:182
241            } else {
242                argv[0].d
243            };
244        }
245        if argc > 1 {                                                    // c:183
246            if (id & tflag(TF_INT2)) != 0 {                              // c:184
247                argi = if argv[1].type_ == MN_FLOAT {
248                    argv[1].d as i32                                     // c:185
249                } else {
250                    argv[1].l as i32
251                };
252            } else {                                                     // c:187
253                argd2 = if argv[1].type_ == MN_INTEGER {
254                    argv[1].l as f64                                     // c:188
255                } else {
256                    argv[1].d
257                };
258            }
259        }
260    }
261
262    // C: `if (errflag) return ret;` — c:196. zshrs's errflag is on
263    // the executor; this dispatcher is invoked from the math
264    // evaluator which already short-circuits on error, so the
265    // explicit check is redundant here.
266
267    let mut retd: f64 = 0.0;                                             // c:175
268
269    match id & 0xff {                                                    // c:198
270        MF_ABS => {                                                      // c:199
271            ret.type_ = argv[0].type_;
272            if argv[0].type_ == MN_INTEGER {
273                ret.l = if argv[0].l < 0 { -argv[0].l } else { argv[0].l };
274            } else {
275                ret.d = argv[0].d.abs();
276            }
277        }
278        MF_ACOS  => retd = argd.acos(),                                  // c:208
279        MF_ACOSH => retd = argd.acosh(),                                 // c:212
280        MF_ASIN  => retd = argd.asin(),                                  // c:216
281        MF_ASINH => retd = argd.asinh(),                                 // c:220
282        MF_ATAN  => {                                                    // c:224
283            retd = if argc == 2 { argd.atan2(argd2) } else { argd.atan() };
284        }
285        MF_ATANH => retd = argd.atanh(),                                 // c:233
286        MF_CBRT  => retd = unsafe { cbrt(argd) },                        // c:237
287        MF_CEIL  => retd = argd.ceil(),                                  // c:241
288        MF_COPYSIGN => retd = unsafe { copysign(argd, argd2) },          // c:245
289        MF_COS   => retd = argd.cos(),                                   // c:249
290        MF_COSH  => retd = argd.cosh(),                                  // c:253
291        MF_ERF   => retd = unsafe { erf(argd) },                         // c:257
292        MF_ERFC  => retd = unsafe { erfc(argd) },                        // c:261
293        MF_EXP   => retd = argd.exp(),                                   // c:265
294        MF_EXPM1 => retd = unsafe { expm1(argd) },                       // c:269
295        MF_FABS  => retd = argd.abs(),                                   // c:273
296        MF_FLOAT => retd = argd,                                         // c:277
297        MF_FLOOR => retd = argd.floor(),                                 // c:281
298        MF_FMOD  => retd = argd % argd2,                                 // c:285
299        MF_GAMMA => retd = unsafe { tgamma(argd) },                      // c:289
300        MF_HYPOT => retd = argd.hypot(argd2),                            // c:300
301        MF_ILOGB => {                                                    // c:304
302            ret.type_ = MN_INTEGER;
303            ret.l = unsafe { ilogb(argd) } as i64;
304        }
305        MF_INT => {                                                      // c:309
306            ret.type_ = MN_INTEGER;
307            ret.l = argd as i64;
308        }
309        MF_ISINF => {                                                    // c:314
310            ret.type_ = MN_INTEGER;
311            ret.l = argd.is_infinite() as i64;
312        }
313        MF_ISNAN => {                                                    // c:319
314            ret.type_ = MN_INTEGER;
315            ret.l = argd.is_nan() as i64;
316        }
317        MF_J0    => retd = unsafe { j0(argd) },                          // c:325
318        MF_J1    => retd = unsafe { j1(argd) },                          // c:329
319        MF_JN    => retd = unsafe { jn(argi, argd2) },                   // c:333
320        MF_LDEXP => retd = unsafe { ldexp(argd, argi) },                 // c:337
321        MF_LGAMMA => retd = unsafe { lgamma(argd) },                     // c:341
322        MF_LOG   => retd = argd.ln(),                                    // c:345
323        MF_LOG10 => retd = argd.log10(),                                 // c:349
324        MF_LOG1P => retd = unsafe { log1p(argd) },                       // c:353
325        MF_LOG2  => retd = argd.log2(),                                  // c:357
326        MF_LOGB  => retd = unsafe { logb(argd) },                        // c:365
327        MF_NEXTAFTER => retd = unsafe { nextafter(argd, argd2) },        // c:369
328        MF_RINT  => retd = unsafe { rint(argd) },                        // c:373
329        MF_SCALB => retd = unsafe { scalbn(argd, argi) },                // c:377
330        MF_SIGNGAM => {                                                  // c:386
331            ret.type_ = MN_INTEGER;
332            ret.l = 0;  // signgam is libm-internal; not portably exposed.
333        }
334        MF_SIN   => retd = argd.sin(),                                   // c:392
335        MF_SINH  => retd = argd.sinh(),                                  // c:396
336        MF_SQRT  => retd = argd.sqrt(),                                  // c:400
337        MF_TAN   => retd = argd.tan(),                                   // c:404
338        MF_TANH  => retd = argd.tanh(),                                  // c:408
339        MF_Y0    => retd = unsafe { y0(argd) },                          // c:412
340        MF_Y1    => retd = unsafe { y1(argd) },                          // c:416
341        MF_YN    => retd = unsafe { yn(argi, argd2) },                   // c:420
342        _ => {                                                           // c:425
343            // BUG: mathfunc type not handled. C prints to stderr
344            // under DEBUG; production zsh silently returns 0.
345        }
346    }
347
348    if (id & tflag(TF_NOASS)) == 0 {                                     // c:431
349        ret.d = retd;                                                    // c:432
350    }
351
352    ret                                                                  // c:434
353}
354
355use crate::ported::zsh_h::features as features_t;
356use std::sync::{Mutex, OnceLock};
357
358static MODULE_FEATURES: OnceLock<Mutex<features_t>> = OnceLock::new();
359
360
361// Local stubs for the per-module entry points. C uses generic
362// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
363// 3275/3370/3445) but those take `Builtin` + `Features` pointer
364// fields the Rust port doesn't carry. The hardcoded descriptor
365// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
366/// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
367fn featuresarray(_m: *const module, _f: &Mutex<features_t>) -> Vec<String> {
368    vec!["f:abs".to_string(), "f:acos".to_string(), "f:acosh".to_string(), "f:asin".to_string(), "f:asinh".to_string(), "f:atan".to_string(), "f:atanh".to_string(), "f:cbrt".to_string(), "f:ceil".to_string(), "f:copysign".to_string(), "f:cos".to_string(), "f:cosh".to_string(), "f:erf".to_string(), "f:erfc".to_string(), "f:exp".to_string(), "f:expm1".to_string(), "f:fabs".to_string(), "f:float".to_string(), "f:floor".to_string(), "f:fmod".to_string(), "f:gamma".to_string(), "f:hypot".to_string(), "f:ilogb".to_string(), "f:int".to_string(), "f:isinf".to_string(), "f:isnan".to_string(), "f:j0".to_string(), "f:j1".to_string(), "f:jn".to_string(), "f:ldexp".to_string(), "f:lgamma".to_string(), "f:log".to_string(), "f:log10".to_string(), "f:log1p".to_string(), "f:log2".to_string(), "f:logb".to_string(), "f:nextafter".to_string(), "f:rint".to_string(), "f:scalb".to_string(), "f:signgam".to_string(), "f:sin".to_string(), "f:sinh".to_string(), "f:sqrt".to_string(), "f:tan".to_string(), "f:tanh".to_string(), "f:y0".to_string(), "f:y1".to_string(), "f:yn".to_string()]
369}
370
371// WARNING: NOT IN MATHFUNC.C — Rust-only module-framework shim.
372// C uses generic featuresarray/handlefeatures/setfeatureenables from
373// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
374// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
375fn handlefeatures(
376    _m: *const module,
377    _f: &Mutex<features_t>,
378    enables: &mut Option<Vec<i32>>,
379) -> i32 {
380    if enables.is_none() {
381        *enables = Some(vec![1; 48]);
382    }
383    0
384}
385
386// WARNING: NOT IN MATHFUNC.C — Rust-only module-framework shim.
387// C uses generic featuresarray/handlefeatures/setfeatureenables from
388// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
389// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
390fn setfeatureenables(
391    _m: *const module,
392    _f: &Mutex<features_t>,
393    _e: Option<&[i32]>,
394) -> i32 {
395    0
396}
397
398// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
399// ─── RUST-ONLY ACCESSORS ───
400//
401// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
402// RwLock<T>>` globals declared above. C zsh uses direct global
403// access; Rust needs these wrappers because `OnceLock::get_or_init`
404// is the only way to lazily construct shared state. These fns sit
405// here so the body of this file reads in C source order without
406// the accessor wrappers interleaved between real port fns.
407// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
408
409// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
410// ─── RUST-ONLY ACCESSORS ───
411//
412// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
413// RwLock<T>>` globals declared above. C zsh uses direct global
414// access; Rust needs these wrappers because `OnceLock::get_or_init`
415// is the only way to lazily construct shared state. These fns sit
416// here so the body of this file reads in C source order without
417// the accessor wrappers interleaved between real port fns.
418// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
419
420// WARNING: NOT IN MATHFUNC.C — Rust-only module-framework shim.
421// C uses generic featuresarray/handlefeatures/setfeatureenables from
422// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
423// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
424fn module_features() -> &'static Mutex<features_t> {
425    MODULE_FEATURES.get_or_init(|| Mutex::new(features_t {
426        bn_list: None,
427        bn_size: 0,
428        cd_list: None,
429        cd_size: 0,
430        mf_list: None,
431        mf_size: 48,
432        pd_list: None,
433        pd_size: 0,
434        n_abstract: 0,
435    }))
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    /// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
443    #[test]
444    fn test_math_func_acos() {
445        let argv = [mnumber { l: 0, d: 1.0, type_: MN_FLOAT }];
446        let r = math_func("acos", 1, &argv, MF_ACOS);
447        assert!((r.type_ == MN_FLOAT));
448        assert!((r.d - 0.0).abs() < 1e-9);
449    }
450
451    /// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
452    #[test]
453    fn test_math_func_atan_two_args() {
454        let argv = [mnumber { l: 0, d: 1.0, type_: MN_FLOAT }, mnumber { l: 0, d: 1.0, type_: MN_FLOAT }];
455        let r = math_func("atan", 2, &argv, MF_ATAN);
456        assert!((r.type_ == MN_FLOAT));
457        assert!((r.d - std::f64::consts::FRAC_PI_4).abs() < 1e-9);
458    }
459
460    /// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
461    #[test]
462    fn test_math_func_abs_int_preserves_type() {
463        let argv = [mnumber { l: -7, d: 0.0, type_: MN_INTEGER }];
464        let r = math_func("abs", 1, &argv, MF_ABS | tflag(TF_NOCONV | TF_NOASS));
465        assert!((r.type_ == MN_INTEGER));
466        assert_eq!(r.l, 7);
467    }
468
469    /// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
470    #[test]
471    fn test_math_func_int_truncates() {
472        let argv = [mnumber { l: 0, d: 3.7, type_: MN_FLOAT }];
473        let r = math_func("int", 1, &argv, MF_INT | tflag(TF_NOASS));
474        assert!((r.type_ == MN_INTEGER));
475        assert_eq!(r.l, 3);
476    }
477
478    /// Port of `math_func(UNUSED(char *name), int argc, mnumber *argv, int id)` from `Src/Modules/mathfunc.c:173`.
479    #[test]
480    fn test_math_func_isnan() {
481        let argv = [mnumber { l: 0, d: f64::NAN, type_: MN_FLOAT }];
482        let r = math_func("isnan", 1, &argv, MF_ISNAN | tflag(TF_NOASS));
483        assert_eq!(r.l, 1);
484    }
485
486    /// Port of `math_string(UNUSED(char *name), char *arg, int id)` from `Src/Modules/mathfunc.c:439`.
487    #[test]
488    fn test_math_string_rand48_in_range() {
489        let r = math_string("rand48", "", MS_RAND48);
490        assert!((r.type_ == MN_FLOAT));
491        assert!((0.0..1.0).contains(&r.d));
492    }
493
494    /// c:173 — `MF_COS` of 0 is 1.0 exactly. Trigonometric identity
495    /// pin; catches a regression that swaps cos/sin dispatch.
496    #[test]
497    fn math_func_cos_of_zero_is_one() {
498        let argv = [mnumber { l: 0, d: 0.0, type_: MN_FLOAT }];
499        let r = math_func("cos", 1, &argv, MF_COS);
500        assert_eq!(r.type_, MN_FLOAT);
501        assert!((r.d - 1.0).abs() < 1e-9);
502    }
503
504    /// c:173 — `MF_SIN` of 0 is 0. Symmetric to the cos test;
505    /// any libm aliasing would surface here.
506    #[test]
507    fn math_func_sin_of_zero_is_zero() {
508        let argv = [mnumber { l: 0, d: 0.0, type_: MN_FLOAT }];
509        let r = math_func("sin", 1, &argv, MF_SIN);
510        assert_eq!(r.type_, MN_FLOAT);
511        assert!(r.d.abs() < 1e-9, "sin(0) = {}", r.d);
512    }
513
514    /// c:173 — `MF_SQRT` of 4 is 2.0. Pure-math anchor that catches
515    /// any regression in the int→float promotion before sqrt.
516    #[test]
517    fn math_func_sqrt_of_four_is_two() {
518        let argv = [mnumber { l: 0, d: 4.0, type_: MN_FLOAT }];
519        let r = math_func("sqrt", 1, &argv, MF_SQRT);
520        assert_eq!(r.type_, MN_FLOAT);
521        assert!((r.d - 2.0).abs() < 1e-9, "sqrt(4) = {}", r.d);
522    }
523
524    /// c:173 — `MF_EXP` of 0 is 1.0 (e^0 identity).
525    #[test]
526    fn math_func_exp_of_zero_is_one() {
527        let argv = [mnumber { l: 0, d: 0.0, type_: MN_FLOAT }];
528        let r = math_func("exp", 1, &argv, MF_EXP);
529        assert_eq!(r.type_, MN_FLOAT);
530        assert!((r.d - 1.0).abs() < 1e-9);
531    }
532
533    /// c:173 — `MF_LOG` of 1.0 is 0.0 (natural log identity).
534    #[test]
535    fn math_func_log_of_one_is_zero() {
536        let argv = [mnumber { l: 0, d: 1.0, type_: MN_FLOAT }];
537        let r = math_func("log", 1, &argv, MF_LOG);
538        assert_eq!(r.type_, MN_FLOAT);
539        assert!(r.d.abs() < 1e-9, "log(1) = {}", r.d);
540    }
541
542    /// c:173 — `MF_FLOOR` of 3.7 is 3.0 (NOT 4.0). Pin direction
543    /// because a regen could swap floor/ceil dispatch.
544    #[test]
545    fn math_func_floor_rounds_down() {
546        let argv = [mnumber { l: 0, d: 3.7, type_: MN_FLOAT }];
547        let r = math_func("floor", 1, &argv, MF_FLOOR);
548        assert_eq!(r.type_, MN_FLOAT);
549        assert_eq!(r.d, 3.0);
550    }
551
552    /// c:173 — `MF_CEIL` of 3.1 is 4.0. Symmetric to floor.
553    #[test]
554    fn math_func_ceil_rounds_up() {
555        let argv = [mnumber { l: 0, d: 3.1, type_: MN_FLOAT }];
556        let r = math_func("ceil", 1, &argv, MF_CEIL);
557        assert_eq!(r.type_, MN_FLOAT);
558        assert_eq!(r.d, 4.0);
559    }
560
561    /// c:173 — `MF_FABS` of negative is positive AND the result
562    /// type stays MN_FLOAT (NOT coerced to MN_INTEGER like the
563    /// integer-typed `abs`).
564    #[test]
565    fn math_func_fabs_preserves_float_type() {
566        let argv = [mnumber { l: 0, d: -2.5, type_: MN_FLOAT }];
567        let r = math_func("fabs", 1, &argv, MF_FABS);
568        assert_eq!(r.type_, MN_FLOAT);
569        assert_eq!(r.d, 2.5);
570    }
571
572    /// c:173 — `MF_ISINF` of +infinity is 1; of finite is 0. Pin
573    /// both branches so a regression that returns the IEEE-754
574    /// classify code (3 / 0 / 4 / 5) instead of the boolean gets
575    /// caught.
576    #[test]
577    fn math_func_isinf_classifies_correctly() {
578        let argv_inf = [mnumber { l: 0, d: f64::INFINITY, type_: MN_FLOAT }];
579        let r_inf = math_func("isinf", 1, &argv_inf, MF_ISINF | tflag(TF_NOASS));
580        assert_eq!(r_inf.l, 1, "isinf(+inf) must be 1");
581
582        let argv_fin = [mnumber { l: 0, d: 1.5, type_: MN_FLOAT }];
583        let r_fin = math_func("isinf", 1, &argv_fin, MF_ISINF | tflag(TF_NOASS));
584        assert_eq!(r_fin.l, 0, "isinf(finite) must be 0");
585    }
586
587    /// c:439 — `math_string` for an unknown id must not panic.
588    /// Defensive contract; return value is impl-defined but the
589    /// function must not crash.
590    #[test]
591    fn math_string_unknown_id_does_not_panic() {
592        let _ = math_string("nope", "", 9999);
593    }
594
595    /// c:548-590 — module-lifecycle stubs all return 0 in C.
596    #[test]
597    fn module_lifecycle_shims_all_return_zero() {
598        let m: *const module = std::ptr::null();
599        assert_eq!(setup_(m), 0);
600        assert_eq!(boot_(m), 0);
601        assert_eq!(cleanup_(m), 0);
602        assert_eq!(finish_(m), 0);
603    }
604}