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