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