zsh/ported/math.rs
1//! Mathematical expression evaluation for zshrs
2//!
3//! Direct port from zsh/Src/math.c
4//!
5//! Supports:
6//! - Integer and floating point arithmetic
7//! - All C operators (+, -, *, /, %, <<, >>, &, |, ^, etc.)
8//! - Zsh ** power operator
9//! - Comparison operators (<, >, <=, >=, ==, !=)
10//! - Logical operators (&&, ||, !)
11//! - Ternary operator (? :)
12//! - Assignment operators (=, +=, -=, *=, /=, etc.)
13//! - Pre/post increment/decrement (++, --)
14//! - Base conversion (`16#FF`, `2#1010`, `[16]FF`)
15//! - Special values (Inf, NaN)
16//! - Variable references and assignment
17
18use std::cell::{Cell, RefCell};
19use std::collections::HashMap;
20
21use crate::ported::options::opt_state_set;
22use crate::ported::params::{convbase, getsparam, unsetparam};
23use crate::ported::utils::zerr;
24/// Re-export of `mnumber` (defined in zsh_h.rs as the Src/zsh.h:95 port).
25pub use crate::ported::zsh_h::{mnumber, Nularg, MN_FLOAT, MN_INTEGER, MN_UNSET};
26use crate::zsh_h::{PM_ARRAY, PM_EFLOAT, PM_FFLOAT, PM_HASHED, PM_INTEGER, PM_TYPE};
27
28/// Re-export of `MN_FLOAT` (defined in zsh_h.rs as the Src/zsh.h:104 port).
29/// Re-export of `MN_INTEGER` (defined in zsh_h.rs as the Src/zsh.h:103 port).
30/// Re-export of `MN_UNSET` (defined in zsh_h.rs as the Src/zsh.h:105 port).
31
32/// Port of `struct mathvalue` from `Src/math.c`:
33///
34/// ```c
35/// struct mathvalue {
36/// char *lval; /* lvalue string for variable write-back */
37/// Value pval; /* resolved variable handle (or NULL) */
38/// mnumber val; /* current numeric value */
39/// };
40/// ```
41#[derive(Clone)]
42pub(crate) struct mathvalue {
43 pub val: mnumber,
44 pub lval: Option<String>,
45 /// `Value pval` slot from the C source. zsh uses it to cache the
46 /// resolved parameter handle so write-back doesn't re-parse the
47 /// `lval` string. Rust port leaves this as `()` for now — the
48 /// resolved variable lives in `crate::ported::exec::ShellExecutor`'s
49 /// `variables` map, looked up by `lval` on each access.
50 pub pval: (),
51}
52
53/// Operator associativity and type flags
54const LR: u16 = 0x0000; // left-to-right
55const RL: u16 = 0x0001; // right-to-left
56const BOOL: u16 = 0x0002; // short-circuit boolean
57
58const OP_A2: u16 = 0x0004; // 2 arguments
59const OP_A2IR: u16 = 0x0008; // 2 args, return int
60const OP_A2IO: u16 = 0x0010; // 2 args, must be int
61const OP_E2: u16 = 0x0020; // 2 args with assignment
62const OP_E2IO: u16 = 0x0040; // 2 args assign, must be int
63const OP_OP: u16 = 0x0080; // expecting operator position
64const OP_OPF: u16 = 0x0100; // followed by operator (after this, next is operator)
65
66/// Math tokens — direct port of Src/math.c:109-162. C uses bare
67/// `#define`s; the Rust port mirrors as `pub const` ints so
68/// `static int mtok` (math.c:305) can be a plain `i32` and the
69/// C precedence/type tables index by the literal numbers.
70pub const M_INPAR: i32 = 0; // c:109 '('
71/// `M_OUTPAR` constant.
72pub const M_OUTPAR: i32 = 1; // c:110 ')'
73/// `NOT` constant.
74pub const NOT: i32 = 2; // c:111 '!'
75/// `COMP` constant.
76pub const COMP: i32 = 3; // c:112 '~'
77/// `POSTPLUS` constant.
78pub const POSTPLUS: i32 = 4; // c:113 x++
79/// `POSTMINUS` constant.
80pub const POSTMINUS: i32 = 5; // c:114 x--
81/// `UPLUS` constant.
82pub const UPLUS: i32 = 6; // c:115 +x
83/// `UMINUS` constant.
84pub const UMINUS: i32 = 7; // c:116 -x
85/// `AND` constant.
86pub const AND: i32 = 8; // c:117 &
87/// `XOR` constant.
88pub const XOR: i32 = 9; // c:118 ^
89/// `OR` constant.
90pub const OR: i32 = 10; // c:119 |
91/// `MUL` constant.
92pub const MUL: i32 = 11; // c:120 *
93/// `DIV` constant.
94pub const DIV: i32 = 12; // c:121 /
95/// `MOD` constant.
96pub const MOD: i32 = 13; // c:122 %
97/// `PLUS` constant.
98pub const PLUS: i32 = 14; // c:123 +
99/// `MINUS` constant.
100pub const MINUS: i32 = 15; // c:124 -
101/// `SHLEFT` constant.
102pub const SHLEFT: i32 = 16; // c:125 <<
103/// `SHRIGHT` constant.
104pub const SHRIGHT: i32 = 17; // c:126 >>
105/// `LES` constant.
106pub const LES: i32 = 18; // c:127 <
107/// `LEQ` constant.
108pub const LEQ: i32 = 19; // c:128 <=
109/// `GRE` constant.
110pub const GRE: i32 = 20; // c:129 >
111/// `GEQ` constant.
112pub const GEQ: i32 = 21; // c:130 >=
113/// `DEQ` constant.
114pub const DEQ: i32 = 22; // c:131 ==
115/// `NEQ` constant.
116pub const NEQ: i32 = 23; // c:132 !=
117/// `DAND` constant.
118pub const DAND: i32 = 24; // c:133 &&
119/// `DOR` constant.
120pub const DOR: i32 = 25; // c:134 ||
121/// `DXOR` constant.
122pub const DXOR: i32 = 26; // c:135 ^^
123/// `QUEST` constant.
124pub const QUEST: i32 = 27; // c:136 ? (ternary)
125/// `COLON` constant.
126pub const COLON: i32 = 28; // c:137 :
127/// `EQ` constant.
128pub const EQ: i32 = 29; // c:138 =
129/// `PLUSEQ` constant.
130pub const PLUSEQ: i32 = 30; // c:139 +=
131/// `MINUSEQ` constant.
132pub const MINUSEQ: i32 = 31; // c:140 -=
133/// `MULEQ` constant.
134pub const MULEQ: i32 = 32; // c:141 *=
135/// `DIVEQ` constant.
136pub const DIVEQ: i32 = 33; // c:142 /=
137/// `MODEQ` constant.
138pub const MODEQ: i32 = 34; // c:143 %=
139/// `ANDEQ` constant.
140pub const ANDEQ: i32 = 35; // c:144 &=
141/// `XOREQ` constant.
142pub const XOREQ: i32 = 36; // c:145 ^=
143/// `OREQ` constant.
144pub const OREQ: i32 = 37; // c:146 |=
145/// `SHLEFTEQ` constant.
146pub const SHLEFTEQ: i32 = 38; // c:147 <<=
147/// `SHRIGHTEQ` constant.
148pub const SHRIGHTEQ: i32 = 39; // c:148 >>=
149/// `DANDEQ` constant.
150pub const DANDEQ: i32 = 40; // c:149 &&=
151/// `DOREQ` constant.
152pub const DOREQ: i32 = 41; // c:150 ||=
153/// `DXOREQ` constant.
154pub const DXOREQ: i32 = 42; // c:151 ^^=
155/// `COMMA` constant.
156pub const COMMA: i32 = 43; // c:152 ,
157/// `EOI` constant.
158pub const EOI: i32 = 44; // c:153 end of input
159/// `PREPLUS` constant.
160pub const PREPLUS: i32 = 45; // c:154 ++x
161/// `PREMINUS` constant.
162pub const PREMINUS: i32 = 46; // c:155 --x
163/// `NUM` constant.
164pub const NUM: i32 = 47; // c:156 number literal
165/// `ID` constant.
166pub const ID: i32 = 48; // c:157 identifier
167/// `POWER` constant.
168pub const POWER: i32 = 49; // c:158 **
169/// `CID` constant.
170pub const CID: i32 = 50; // c:159 #identifier (char value)
171/// `POWEREQ` constant.
172pub const POWEREQ: i32 = 51; // c:160 **=
173/// `FUNC` constant.
174pub const FUNC: i32 = 52; // c:161 function call
175/// Total token count — Src/math.c:162 `#define TOKCOUNT 53`. The
176/// `c_prec`/`z_prec`/`type` arrays are sized by this.
177pub const TOKCOUNT: usize = 53;
178
179/// Port of `enum prec_type` from `Src/math.c`. `mathevall()` (line
180/// 367) uses this to differentiate top-level expression evaluation
181/// (`(())`, `$(())`) from function-argument evaluation
182/// (`func(arg, arg, …)`) — argument-mode terminates parsing on
183/// the first comma encountered at the top level.
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185#[allow(non_camel_case_types)]
186pub enum prec_type {
187 MPREC_TOP,
188 MPREC_ARG,
189}
190
191/// Port of `getmathparam(struct mathvalue *mptr)` from `Src/math.c:337`.
192///
193/// Look up a parameter by name from inside math context. zsh
194/// auto-typesets a missing-but-referenced name (its mathparam
195/// flag), but the Rust port keeps the variables map separate from
196/// the param table so a miss returns `Integer(0)` and skips the
197/// type-coercion. Indirect-string mode (`a="3+2"; $((a))`) is
198/// handled by recursively evaluating the string value.
199/// WARNING: param names don't match C — Rust=() vs C=(mptr)
200pub(crate) fn getmathparam(name: &str) -> mnumber {
201 // c:Src/math.c:358-362 — after reading a parameter's value, FORCEFLOAT
202 // coerces an INTEGER result to float, so `integer a=3 b=4; setopt
203 // force_float; $((a/b))` does float division (0.75) not integer (0).
204 // The literal/operator paths already honor force_float; only this
205 // param-read path dropped it. The read logic is wrapped in an
206 // immediately-invoked closure (NOT a named inner fn — the name-parity
207 // build gate requires every `fn` to have a C counterpart, and there is
208 // only one C `getmathparam`).
209 let __raw = (|| -> mnumber {
210 // Strip array subscript if present
211 let base_name = if let Some(bracket) = name.find('[') {
212 &name[..bracket]
213 } else {
214 name
215 };
216 if let Some(v) = m_variables_get(base_name) {
217 return v;
218 }
219 // c:Src/math.c:337 getmathparam — falls back to `getvalue(s)`
220 // which parses the full subscript syntax (params.c:2180).
221 // The Rust port previously required callers to seed
222 // `with_string_variables` (a pre-populate pattern that
223 // diverged from C). Read paramtab + array subscripts here
224 // so matheval works without seeding.
225 if let Some(bracket) = name.find('[') {
226 let close = name.rfind(']').unwrap_or(name.len());
227 let arr_name = &name[..bracket];
228 let idx_str = &name[bracket + 1..close];
229
230 // c:Src/params.c::getarg — subscript-flag form `(i)pat` /
231 // `(I)pat` inside an arith subscript: search the array for
232 // `pat` and return the 1-based index (or len+1 / 0 for
233 // miss). Bug #341. The other flag arms (`r`/`R` returning
234 // strings, `n`/`b`/`e`/`w`/`s` etc.) don't yield arith
235 // values, so we only handle `i`/`I` here.
236 // Flag block `(flags)pat`: `i`/`I` make it an index search
237 // (forward / reverse), an optional `e` modifier forces EXACT
238 // (literal) compare instead of glob match. Accepts `(i)`,`(I)`,
239 // `(ie)`,`(Ie)`,`(ei)`,… so membership tests like
240 // `(( arr[(Ie)$x] ))` resolve. Other flag letters (r/R/n/b/w/s)
241 // don't yield an arith value and fall through.
242 if idx_str.starts_with('(') {
243 if let Some(close) = idx_str.find(')') {
244 let flags = &idx_str[1..close];
245 let pat = &idx_str[close + 1..];
246 let is_index = flags.contains('i') || flags.contains('I');
247 if is_index && flags.chars().all(|c| matches!(c, 'i' | 'I' | 'e' | 'n')) {
248 let reverse = flags.contains('I');
249 let exact = flags.contains('e');
250 let matches_elem = |e: &str| -> bool {
251 if exact {
252 e == pat
253 } else {
254 crate::ported::pattern::patcompile(
255 &{
256 let mut t = pat.to_string();
257 crate::ported::glob::tokenize(&mut t);
258 t
259 },
260 crate::ported::zsh_h::PAT_HEAPDUP as i32,
261 None,
262 )
263 .map_or(e == pat, |p| crate::ported::pattern::pattry(&p, e))
264 }
265 };
266 if let Ok(tab) = crate::ported::params::paramtab().read() {
267 if let Some(pm) = tab.get(arr_name) {
268 if let Some(arr) = &pm.u_arr {
269 let len = arr.len() as i64;
270 let mut found: i64 = if reverse { 0 } else { len + 1 };
271 let it: Vec<(usize, &String)> = if reverse {
272 arr.iter().enumerate().rev().collect()
273 } else {
274 arr.iter().enumerate().collect()
275 };
276 for (i, e) in it {
277 if matches_elem(e) {
278 found = (i + 1) as i64;
279 break;
280 }
281 }
282 return mnumber {
283 l: found,
284 d: 0.0,
285 type_: MN_INTEGER,
286 };
287 }
288 }
289 }
290 return mnumber {
291 l: 0,
292 d: 0.0,
293 type_: MN_INTEGER,
294 };
295 }
296 }
297 }
298
299 // Recursively eval the index (so a[i+1], h[$k], etc work).
300 // CRITICAL: save/restore evaluator state around the recursive
301 // matheval — without this, the inner call's `push(idx_value)`
302 // contaminates the OUTER expression's operand stack. Bug
303 // manifested as `$((1 + arr[1]))` returning 10 (just arr[1])
304 // because the outer NUM(1) got popped by the inner eval's
305 // op() during `op(PLUS)` (which sees [NUM(1), ID(arr[1]),
306 // NUM(1_from_idx_eval)] instead of [NUM(1), ID(arr[1])]).
307 // C mathevall at math.c:367 does the same xyy* save/restore
308 // around recursive entry.
309 let saved = save_state();
310 let idx_val = matheval(idx_str)
311 .map(|n| if n.type_ == MN_FLOAT { n.d as i64 } else { n.l })
312 .unwrap_or(0);
313 restore_state(saved);
314 // Read paramtab directly: PM_ARRAY → u_arr indexed by 1-based pos.
315 if let Ok(tab) = crate::ported::params::paramtab().read() {
316 if let Some(pm) = tab.get(arr_name) {
317 if let Some(arr) = &pm.u_arr {
318 let len = arr.len() as i64;
319 let pos = if idx_val < 0 {
320 len + idx_val
321 } else {
322 idx_val - 1
323 };
324 if pos >= 0 && (pos as usize) < arr.len() {
325 let raw = &arr[pos as usize];
326 if let Ok(n) = raw.parse::<i64>() {
327 return mnumber {
328 l: n,
329 d: 0.0,
330 type_: MN_INTEGER,
331 };
332 }
333 if let Ok(f) = raw.parse::<f64>() {
334 return mnumber {
335 l: 0,
336 d: f,
337 type_: MN_FLOAT,
338 };
339 }
340 }
341 }
342 }
343 }
344 // PM_HASHED via paramtab_hashed_storage.
345 if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
346 if let Some(map) = m.get(arr_name) {
347 if let Some(v) = map.get(idx_str) {
348 if let Ok(n) = v.parse::<i64>() {
349 return mnumber {
350 l: n,
351 d: 0.0,
352 type_: MN_INTEGER,
353 };
354 }
355 if let Ok(f) = v.parse::<f64>() {
356 return mnumber {
357 l: 0,
358 d: f,
359 type_: MN_FLOAT,
360 };
361 }
362 }
363 }
364 }
365 // c:Src/math.c:337 getmathparam → getvalue: magic-assoc special
366 // parameters (`sysparams`, `errnos`, `commands`, …) don't live in
367 // paramtab — their per-key value comes from a module getfn, the
368 // same PARTAB dispatch string context uses (subst.rs:8102). Route
369 // through it so `(( sysparams[pid] ))` yields the pid instead of 0.
370 // gitstatus_start relies on this (plugin line 649 / 593), so the
371 // whole zsh/system-backed init path (p10k's git prompt) was dead.
372 if let Some(e_) = crate::ported::modules::parameter::PARTAB
373 .iter()
374 .find(|e_| e_.name == arr_name)
375 {
376 let module_ok = match e_.module {
377 Some(m_) => crate::ported::module::MODULESTAB
378 .lock()
379 .map(|t| t.is_loaded(m_))
380 .unwrap_or(false),
381 None => true,
382 };
383 if module_ok {
384 if let Some(v) = (e_.getfn)(std::ptr::null_mut(), idx_str).and_then(|p_| p_.u_str)
385 {
386 if let Ok(n) = v.parse::<i64>() {
387 return mnumber {
388 l: n,
389 d: 0.0,
390 type_: MN_INTEGER,
391 };
392 }
393 if let Ok(f) = v.parse::<f64>() {
394 return mnumber {
395 l: 0,
396 d: f,
397 type_: MN_FLOAT,
398 };
399 }
400 }
401 }
402 }
403 return mnumber {
404 l: 0,
405 d: 0.0,
406 type_: MN_INTEGER,
407 };
408 }
409 if let Some(raw) = getsparam(base_name) {
410 if let Ok(n) = raw.parse::<i64>() {
411 return mnumber {
412 l: n,
413 d: 0.0,
414 type_: MN_INTEGER,
415 };
416 }
417 if let Ok(f) = raw.parse::<f64>() {
418 return mnumber {
419 l: 0,
420 d: f,
421 type_: MN_FLOAT,
422 };
423 }
424 // c:Src/math.c:337 getmathparam — falls back to recursively
425 // evaluating the raw string as an arith expression. zsh: a
426 // scalar holding `0xff` / `0b101` / `3+2` / `1e3` all evaluate
427 // when used in arith context. Direct path through `matheval`
428 // gives lexconstant + parser its full integer-base + float
429 // handling.
430 let saved = save_state();
431 let inherited_strs = saved.string_variables.clone();
432 new(&raw);
433 m_variables_set(saved.variables.clone());
434 let mut strs = inherited_strs;
435 strs.remove(base_name);
436 m_string_variables_set(strs);
437 m_prec_set(saved.prec);
438 m_c_precedences_set(saved.c_precedences);
439 let result = mathevall();
440 // c:Src/math.c::matheval — when the recursive eval errors
441 // (e.g. raw is "42xyz" with trailing junk), preserve the error
442 // message so it propagates to the outer arith caller instead
443 // of being clobbered by restore_state. zsh: `a="42xyz";
444 // $((a+1))` → "bad math expression: operator expected at
445 // `xyz'" rc=1. zshrs previously swallowed the error and
446 // returned 0 (then +1 = 1) silently. Bug #494.
447 let err_to_propagate = match &result {
448 Err(msg) => Some(msg.clone()),
449 Ok(_) => None,
450 };
451 restore_state(saved);
452 if let Ok(r) = result {
453 return r;
454 }
455 if let Some(msg) = err_to_propagate {
456 m_error_set(msg);
457 }
458 // Non-numeric and non-evaluable string: fall through.
459 }
460 // Recursive eval: if the var holds a non-numeric string, evaluate
461 // it AS an arith expression. zsh: `a="3+2"; $((a))` → 5. Bound
462 // to one level of indirection — fresh evaluator each call so we
463 // don't accidentally pollute s.variables.
464 if let Some(raw) = m_string_variables_get(base_name) {
465 // Save parent's eval state — `new(&raw)` resets thread_locals
466 // for the sub-eval, which would otherwise clobber the parent.
467 // Mirrors C `mathevall()` xyy* save/restore pattern (math.c:367).
468 let saved = save_state();
469 // Inherit caller's variables/string_variables/prec into the
470 // sub-eval, with `base_name` removed from the indirect map to
471 // prevent infinite recursion on `a="$a"`-style cycles.
472 let inherited_vars = saved.variables.clone();
473 let mut inherited_strs = saved.string_variables.clone();
474 inherited_strs.remove(base_name);
475 let inherited_prec = saved.prec;
476 let inherited_c_prec = saved.c_precedences;
477
478 new(&raw);
479 m_variables_set(inherited_vars);
480 m_string_variables_set(inherited_strs);
481 m_prec_set(inherited_prec);
482 m_c_precedences_set(inherited_c_prec);
483
484 let result = mathevall();
485 restore_state(saved);
486 if let Ok(r) = result {
487 return r;
488 }
489 }
490 // c:Src/math.c:345-346 — `if (unset(UNSET)) zerr("%s: parameter
491 // not set", mptr->lval);`. When `nounset` is set (i.e., the
492 // canonical `UNSET` option is OFF), referring to an unset
493 // parameter in arith context is an error. Bug #88 in
494 // docs/BUGS.md: zshrs silently used 0, masking typos and
495 // breaking defensive `set -u` scripts.
496 if !crate::ported::zsh_h::isset(crate::ported::zsh_h::UNSET) {
497 crate::ported::utils::zerr(&format!("{}: parameter not set", name));
498 }
499 mnumber {
500 l: 0,
501 d: 0.0,
502 type_: MN_INTEGER,
503 }
504 })();
505 if m_force_float() && __raw.type_ == MN_INTEGER {
506 // c:359-362 — coerce integer → float under FORCEFLOAT.
507 return mnumber {
508 l: 0,
509 d: __raw.l as f64,
510 type_: MN_FLOAT,
511 };
512 }
513 __raw
514}
515
516/// Evaluate the expression
517/// Port of `mathevall(char *s, enum prec_type prec_tp, char **ep)` from `Src/math.c:367`.
518/// WARNING: param names don't match C — Rust=() vs C=(s, prec_tp, ep)
519pub(crate) fn mathevall() -> Result<mnumber, String> {
520 // c:Src/math.c — matheval reads `isset(CPRECEDENCES)` / `isset(FORCEFLOAT)`
521 // / `isset(OCTALZEROES)` live at its use sites (e.g. c:348, 359, 482). The
522 // zshrs port caches them in per-eval thread-locals for speed but never
523 // populated them, so `setopt forcefloat` / `cprecedences` / `octalzeroes`
524 // had no effect inside arithmetic. Sync the caches from the live options at
525 // each eval entry (the option can't change mid-expression).
526 m_c_precedences_set(crate::ported::zsh_h::isset(
527 crate::ported::zsh_h::CPRECEDENCES,
528 ));
529 m_force_float_set(crate::ported::zsh_h::isset(
530 crate::ported::zsh_h::FORCEFLOAT,
531 ));
532 m_octal_zeroes_set(crate::ported::zsh_h::isset(
533 crate::ported::zsh_h::OCTALZEROES,
534 ));
535 m_prec_set(if m_c_precedences() { &C_PREC } else { &Z_PREC });
536
537 // c:386/446 — `if (mlevel++)` … `if (--mlevel)` bracket the evaluator.
538 // The output radix is deliberately NOT reset here: C clears it only in
539 // `matheval` and only when `mlevel` is 0 (c:1486), i.e. exactly once per
540 // TOP-LEVEL expression. `mathevall` is re-entered for every nested
541 // evaluation — including the recursive re-eval of a scalar parameter whose
542 // value is itself a math expression (`j=8#62; $(( [#36] j ))`, getmathparam
543 // below) — so resetting here wiped the outer `[#36]` and printed base 10.
544 // That is what the C comment at c:1485 means by "maintain outputradix and
545 // outputunderscore across levels of evaluation".
546 let _mlevel = MathLevel::enter();
547
548 // c:Src/math.c — bound recursive re-evaluation. A scalar whose
549 // value references itself in arithmetic — `x=x`, `x="1+x"`, or a
550 // cycle `x=y; y=x` — makes getmathparam re-enter mathevall on the
551 // same value without end (getsparam returns the self-referential
552 // string, mathevall re-parses it, getmathparam resolves the var
553 // again …). Unbounded, that recursion overruns the stack and the
554 // whole shell dies with SIGBUS/SIGABRT rather than erroring. zsh
555 // caps `mlevel` and bails with a diagnostic; match it so the eval
556 // fails cleanly (0 result) instead of crashing. thefuck's config
557 // tripped this the moment the cmd-subst deadlock that used to mask
558 // it was fixed.
559 const MAX_MLEVEL: i32 = 256; // c:Src/math.c MAX_MLEVEL
560 if M_LEVEL.with(|c| c.get()) > MAX_MLEVEL {
561 let expr = m_input_clone();
562 return Err(format!("math recursion limit exceeded: {}", expr.trim()));
563 }
564
565 // Skip leading whitespace and Nularg
566 while let Some(c) = peek() {
567 if c.is_whitespace() || c == '\u{a1}' {
568 advance();
569 } else {
570 break;
571 }
572 }
573
574 if m_pos() >= m_input_len() {
575 return Ok(mnumber {
576 l: 0,
577 d: 0.0,
578 type_: MN_INTEGER,
579 });
580 }
581
582 mathparse(top_prec());
583
584 if let Some(err) = m_error_take() {
585 return Err(err);
586 }
587
588 // Check for trailing characters
589 while let Some(c) = peek() {
590 if c.is_whitespace() {
591 advance();
592 } else if c == ')' {
593 // zsh's specific wording for the unmatched-close
594 // case: `bad math expression: unexpected ')'`.
595 return Err("bad math expression: unexpected ')'".to_string());
596 } else {
597 // c:1498-1499 — `if (*junk) zerr("bad math expression:
598 // illegal character: %c", *junk);`
599 return Err(format!("bad math expression: illegal character: {}", c));
600 }
601 }
602
603 if m_stack_is_empty() {
604 return Ok(mnumber {
605 l: 0,
606 d: 0.0,
607 type_: MN_INTEGER,
608 });
609 }
610
611 let mv = m_stack_pop().unwrap();
612 let result = if (mv.val.type_ == MN_UNSET) {
613 if let Some(ref name) = mv.lval {
614 getmathparam(name)
615 } else {
616 mnumber {
617 l: 0,
618 d: 0.0,
619 type_: MN_INTEGER,
620 }
621 }
622 } else {
623 mv.val
624 };
625
626 // c:Src/math.c:425-444 — `if (errflag) { ret = 0; }` and the
627 // caller checks `errflag` externally to detect failure. The Rust
628 // port carries the error in the Result instead of a side-channel
629 // errflag, so any m_error_set inside getmathparam (e.g. the
630 // recursive-eval arm at math.rs:384 for `a="/usr/bin"; (( a ))`)
631 // must surface as Err here rather than being swallowed by the
632 // unconditional Ok(result) below. Without this check, scalar
633 // params whose values fail recursive math parsing silently
634 // resolved to 0, breaking `${(t)assoc[NAME]}` parity where C's
635 // bracket-eval relies on the substituted name's value to
636 // trigger "bad math expression".
637 if let Some(err) = m_error_take() {
638 return Err(err);
639 }
640 Ok(result)
641}
642
643/// Port of `lexconstant()` from `Src/math.c:462`.
644///
645/// Lex a numeric constant — decimal/hex/binary/octal integer or
646/// floating-point literal. Sets `m_yyval()` and returns
647/// `NUM`. Recognises `0x`/`0b` prefixes, base-prefix
648/// (`16#FF`), trailing-dot float, scientific notation, and zsh's
649/// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART !!!
650/// C math.c:856 calls `getkeystring(ptr, NULL, GETKEYS_MATH, &v)` — the
651/// shared 200-line key-string decoder run in GETKEY_SINGLE_CHAR mode
652/// (decode exactly ONE char, report bytes consumed). zshrs's
653/// `getkeystring_with` loops the whole string and has no single-char
654/// mode, and the math lexer advances a char cursor (not a byte ptr), so
655/// this small adapter decodes just the one escaped char at the cursor
656/// and returns (code, chars-consumed). It mirrors the GETKEYS_MATH flag
657/// set (OCTAL_ESC | EMACS | CTRL). Allowlisted in fake_fn_allowlist.txt.
658fn decode_math_keychar(s: &str) -> Option<(i64, usize)> {
659 let cs: Vec<char> = s.chars().collect();
660 if cs.is_empty() {
661 return None;
662 }
663 if cs[0] != '\\' {
664 return Some((cs[0] as i64, 1));
665 }
666 // `\X` escape — `\` plus at least one more char.
667 let e = match cs.get(1) {
668 Some(c) => *c,
669 None => return Some(('\\' as i64, 1)),
670 };
671 let simple = |code: i64| Some((code, 2));
672 match e {
673 'n' => simple(10),
674 't' => simple(9),
675 'r' => simple(13),
676 'e' | 'E' => simple(27),
677 'a' => simple(7),
678 'b' => simple(8),
679 'f' => simple(12),
680 'v' => simple(11),
681 '\\' => simple(92),
682 '0'..='7' => {
683 // \NNN octal (GETKEY_OCTAL_ESC), up to 3 digits.
684 let mut val: i64 = 0;
685 let mut n = 0;
686 while n < 3 {
687 match cs.get(1 + n) {
688 Some(c @ '0'..='7') => {
689 val = val * 8 + (*c as i64 - '0' as i64);
690 n += 1;
691 }
692 _ => break,
693 }
694 }
695 Some((val, 1 + n))
696 }
697 'x' => {
698 // \xNN hex, up to 2 digits.
699 let mut val: i64 = 0;
700 let mut n = 0;
701 while n < 2 {
702 match cs.get(2 + n).and_then(|c| c.to_digit(16)) {
703 Some(d) => {
704 val = val * 16 + d as i64;
705 n += 1;
706 }
707 None => break,
708 }
709 }
710 if n == 0 {
711 Some(('x' as i64, 2))
712 } else {
713 Some((val, 2 + n))
714 }
715 }
716 'c' => {
717 // \cX control char (GETKEY_CTRL): code = X & 0x1f.
718 match cs.get(2) {
719 Some(c) => Some(((*c as i64) & 0x1f, 3)),
720 None => Some(('c' as i64, 2)),
721 }
722 }
723 'C' => {
724 // c:Src/utils.c:7041-7046 — `case 'C': if (how & GETKEY_EMACS) {
725 // if (s[1]=='-') s++; control=1; }`. `\C-X` / `\CX` → control
726 // char `X & 0x1f` (e.g. `##\C-a` → 1). GETKEYS_MATH sets
727 // GETKEY_EMACS, so the dash is optional and consumed when present.
728 let dash = cs.get(2) == Some(&'-');
729 let tidx = if dash { 3 } else { 2 };
730 match cs.get(tidx) {
731 Some(c) => Some(((*c as i64) & 0x1f, tidx + 1)),
732 None => Some(('C' as i64, 2)),
733 }
734 }
735 'M' => {
736 // c:Src/utils.c — GETKEY_EMACS meta: `\M-X` / `\MX` → `X | 0x80`.
737 let dash = cs.get(2) == Some(&'-');
738 let tidx = if dash { 3 } else { 2 };
739 match cs.get(tidx) {
740 Some(c) => Some(((*c as i64) | 0x80, tidx + 1)),
741 None => Some(('M' as i64, 2)),
742 }
743 }
744 other => simple(other as i64),
745 }
746}
747
748/// underscore digit-grouping. Mirrors C's `zstrtol_underscore()`
749/// for greedy base parsing (consume valid digits only, leave the
750/// rest as the next token).
751pub(crate) fn lexconstant() -> i32 {
752 let _start = m_pos();
753 let mut is_neg = false;
754
755 // Handle leading minus for unary context
756 if peek() == Some('-') {
757 is_neg = true;
758 advance();
759 }
760
761 // Check for hex/binary/octal
762 if peek() == Some('0') {
763 advance();
764 match peek().map(|c| c.to_ascii_lowercase()) {
765 Some('x') => {
766 // Hex: 0xFF
767 advance();
768 let hex_start = m_pos();
769 while let Some(c) = peek() {
770 if c.is_ascii_hexdigit() || c == '_' {
771 advance();
772 } else {
773 break;
774 }
775 }
776 let hex_str: String = m_input_clone()[hex_start..m_pos()]
777 .chars()
778 .filter(|&c| c != '_')
779 .collect();
780 // c:Src/math.c lexconstant — zsh parses every integer base via
781 // zstrtol, which truncates on overflow with a
782 // "number truncated after N digits" warning (utils.c:2511).
783 // i64::from_str_radix just errored to 0 on a >63-bit hex
784 // literal (0xFFFFFFFFFFFFFFFF). Route through the port.
785 let val = crate::ported::utils::zstrtol(&hex_str, 16).0;
786 m_lastbase_set(16);
787 m_yyval_set(if m_force_float() {
788 mnumber {
789 l: 0,
790 d: if is_neg { -(val as f64) } else { val as f64 },
791 type_: MN_FLOAT,
792 }
793 } else {
794 mnumber {
795 l: if is_neg { -val } else { val },
796 d: 0.0,
797 type_: MN_INTEGER,
798 }
799 });
800 return NUM;
801 }
802 Some('b') => {
803 // Binary: 0b1010
804 advance();
805 let bin_start = m_pos();
806 while let Some(c) = peek() {
807 if c == '0' || c == '1' || c == '_' {
808 advance();
809 } else {
810 break;
811 }
812 }
813 let bin_str: String = m_input_clone()[bin_start..m_pos()]
814 .chars()
815 .filter(|&c| c != '_')
816 .collect();
817 let val = crate::ported::utils::zstrtol(&bin_str, 2).0; // c:zstrtol base 2
818 m_lastbase_set(2);
819 m_yyval_set(if m_force_float() {
820 mnumber {
821 l: 0,
822 d: if is_neg { -(val as f64) } else { val as f64 },
823 type_: MN_FLOAT,
824 }
825 } else {
826 mnumber {
827 l: if is_neg { -val } else { val },
828 d: 0.0,
829 type_: MN_INTEGER,
830 }
831 });
832 return NUM;
833 }
834 Some('o') | Some('O') => {
835 // zsh rejects `0o…` octal-prefix (Rust/Python form).
836 // Only `0x` (hex), `0b` (binary), and bare-leading-0
837 // (with `setopt octalzeroes`) are recognized. Emit
838 // the same diagnostic zsh produces — set s.error
839 // and return a stub Num so the caller's
840 // error-propagation path picks up the failure.
841 m_error_set(format!(
842 "bad math expression: operator expected at `{}'",
843 &m_input_clone()[m_pos()..]
844 ));
845 m_yyval_set(mnumber {
846 l: 0,
847 d: 0.0,
848 type_: MN_INTEGER,
849 });
850 return NUM;
851 }
852 _ => {
853 // Could be octal or just 0
854 if m_octal_zeroes() {
855 // c:Src/math.c:489-512 — OCTALZEROES enabled.
856 // C scans all digits then calls
857 // `zstrtol_underscore(ptr, &ptr, 0, 1)` with base 0;
858 // strtol's base-0 octal mode stops at the first
859 // invalid octal digit (8 or 9), so the leftover
860 // digit is seen by the outer parser and produces
861 // "operator expected at `N'".
862 //
863 // To match: scan VALID octal digits (0-7) +
864 // underscores, STOP at 8/9, then emit NUM. Do NOT
865 // roll back over the 8/9 — it stays in the input
866 // for the outer parser.
867 //
868 // `.`/`e`/`E`/`#` (c:501) disqualify the whole
869 // number — fall through to decimal/float by
870 // rewinding to before the leading 0.
871 let oct_start = m_pos();
872 let mut is_float_or_base = false;
873 let mut hit_invalid_octal = false;
874 // First peek-ahead: scan all digits to detect the
875 // terminator type. This matches C's `for (ptr2 = nptr;
876 // idigit(*ptr2) || *ptr2 == '_'; ptr2++)` peek.
877 let mut probe = oct_start;
878 let input = m_input_clone();
879 while let Some(&b) = input.as_bytes().get(probe) {
880 if (b as char).is_ascii_digit() || b == b'_' {
881 if b == b'8' || b == b'9' {
882 hit_invalid_octal = true;
883 }
884 probe += 1;
885 } else {
886 if b == b'.' || b == b'e' || b == b'E' || b == b'#' {
887 is_float_or_base = true;
888 }
889 break;
890 }
891 }
892 if is_float_or_base {
893 // c:501 — `.`/`e`/`E`/`#` after digits means
894 // float/base-notation; treat as decimal/float.
895 m_pos_sub(1); // rewind over leading 0
896 } else {
897 // Octal path. Advance over valid octal digits
898 // only (0-7) + underscores; stop at 8/9.
899 while let Some(c) = peek() {
900 if ('0'..='7').contains(&c) || c == '_' {
901 advance();
902 } else {
903 break;
904 }
905 }
906 let oct_str: String = m_input_clone()[oct_start..m_pos()]
907 .chars()
908 .filter(|&c| c != '_')
909 .collect();
910 let val = if oct_str.is_empty() {
911 0 // c:zstrtol leading-0-only → value 0
912 } else {
913 crate::ported::utils::zstrtol(&oct_str, 8).0 // c:zstrtol base 8
914 };
915 let _ = hit_invalid_octal; // implicit via leftover digit
916 m_lastbase_set(8);
917 m_yyval_set(if m_force_float() {
918 mnumber {
919 l: 0,
920 d: if is_neg { -(val as f64) } else { val as f64 },
921 type_: MN_FLOAT,
922 }
923 } else {
924 mnumber {
925 l: if is_neg { -val } else { val },
926 d: 0.0,
927 type_: MN_INTEGER,
928 }
929 });
930 return NUM;
931 }
932 } else {
933 // Put back the 0 — fall through to decimal parser.
934 m_pos_sub(1);
935 }
936 }
937 }
938 }
939
940 // Parse decimal integer or float
941 let num_start = m_pos();
942 while let Some(c) = peek() {
943 if is_digit(c) || c == '_' {
944 advance();
945 } else {
946 break;
947 }
948 }
949
950 // Check for float
951 if peek() == Some('.') || peek() == Some('e') || peek() == Some('E') {
952 // Float
953 if peek() == Some('.') {
954 advance();
955 while let Some(c) = peek() {
956 if is_digit(c) || c == '_' {
957 advance();
958 } else {
959 break;
960 }
961 }
962 }
963 if peek() == Some('e') || peek() == Some('E') {
964 advance();
965 if peek() == Some('+') || peek() == Some('-') {
966 advance();
967 }
968 while let Some(c) = peek() {
969 if is_digit(c) || c == '_' {
970 advance();
971 } else {
972 break;
973 }
974 }
975 }
976 let float_str: String = m_input_clone()[num_start..m_pos()]
977 .chars()
978 .filter(|&c| c != '_')
979 .collect();
980 // c:552-559 — right after strtod:
981 // yyval.u.d = strtod(ptr, &nptr);
982 // if (ptr == nptr || *nptr == '.') {
983 // zerr("bad floating point constant");
984 // return EOI;
985 // }
986 // The `*nptr == '.'` half is the interesting one: a SECOND dot
987 // immediately after the constant strtod just consumed is fatal at LEX
988 // time. Without it `1.2.3` lexed as the float 1.2 followed by `.3` and
989 // the failure surfaced from the PARSER as "bad math expression:
990 // operator expected at `.3 '" — a different diagnostic for what zsh
991 // calls a malformed constant. (`ptr == nptr`, strtod consuming nothing,
992 // cannot happen here: this branch is only entered having already seen a
993 // digit or a dot.)
994 if peek() == Some('.') {
995 m_error_set("bad floating point constant".to_string()); // c:557
996 return EOI; // c:558
997 }
998 let val: f64 = float_str.parse().unwrap_or(0.0);
999 m_yyval_set(mnumber {
1000 l: 0,
1001 d: if is_neg { -val } else { val },
1002 type_: MN_FLOAT,
1003 });
1004 return NUM;
1005 }
1006
1007 // Check for base#value syntax (e.g., 16#FF)
1008 if peek() == Some('#') {
1009 advance();
1010 let base_str: String = m_input_clone()[num_start..m_pos() - 1]
1011 .chars()
1012 .filter(|&c| c != '_')
1013 .collect();
1014 let base: u32 = base_str.parse().unwrap_or(10);
1015 // zsh: `1#X` errors with "invalid base (must be 2 to 36 inclusive)".
1016 // i64::from_str_radix panics on out-of-range base; reject early.
1017 if !(2..=36).contains(&base) {
1018 m_error_set(format!(
1019 "invalid base (must be 2 to 36 inclusive): {}",
1020 base
1021 ));
1022 m_yyval_set(mnumber {
1023 l: 0,
1024 d: 0.0,
1025 type_: MN_INTEGER,
1026 });
1027 return NUM;
1028 }
1029 m_lastbase_set(base as i32);
1030
1031 // Mirror zsh's `zstrtol_underscore(ptr, &ptr, base, 1)`
1032 // semantics: consume ONLY chars valid for the base
1033 // (greedy), stopping at the first invalid digit.
1034 // Underscore-as-thousands-separator is allowed
1035 // mid-number. The remaining input becomes the next
1036 // token, which the parser will then trip on as
1037 // "operator expected at `<rest>'" via the regular
1038 // checkunary/parser path.
1039 //
1040 // Earlier version used Rust's `from_str_radix` which
1041 // is all-or-nothing — a single bad digit nuked the
1042 // entire literal. For `2#1011x` zsh consumes the
1043 // valid `1011` (= 11) and errors on the trailing `x`;
1044 // ours errored on the whole `1011x` as one chunk.
1045 // Same for `2#10112` (zsh: at `2`, ours: at `10112`).
1046 //
1047 // Empty-digit-sequence case (`10#`, `2#`) silently
1048 // yields 0, matching zsh's `zstrtol` returning 0 when
1049 // no valid digits follow.
1050 let mut val: i64 = 0;
1051 let base_i64 = base as i64;
1052 while let Some(c) = peek() {
1053 if c == '_' {
1054 advance();
1055 continue;
1056 }
1057 let digit_val: Option<u32> = if c.is_ascii_digit() {
1058 Some(c as u32 - '0' as u32)
1059 } else if c.is_ascii_alphabetic() {
1060 Some(c.to_ascii_lowercase() as u32 - 'a' as u32 + 10)
1061 } else {
1062 None
1063 };
1064 let Some(d) = digit_val else {
1065 break;
1066 };
1067 if d >= base {
1068 break;
1069 }
1070 val = val.saturating_mul(base_i64).saturating_add(d as i64);
1071 advance();
1072 }
1073 m_yyval_set(if m_force_float() {
1074 mnumber {
1075 l: 0,
1076 d: if is_neg { -(val as f64) } else { val as f64 },
1077 type_: MN_FLOAT,
1078 }
1079 } else {
1080 mnumber {
1081 l: if is_neg { -val } else { val },
1082 d: 0.0,
1083 type_: MN_INTEGER,
1084 }
1085 });
1086 return NUM;
1087 }
1088
1089 // Plain integer
1090 let int_str: String = m_input_clone()[num_start..m_pos()]
1091 .chars()
1092 .filter(|&c| c != '_')
1093 .collect();
1094 // c:Src/utils.c:2466-2515 zstrtol — accept overflow with truncation and a
1095 // `"number truncated after N digits"` warning rather than silently
1096 // producing 0. The fast i64 path covers everything up to i64::MAX; past
1097 // it, DELEGATE to the faithful zstrtol port (same as the hex branch above
1098 // at c:785) instead of a hardcoded 18-digit cut. zstrtol accumulates the
1099 // magnitude in a u64 and truncates ONLY when the unsigned multiply
1100 // overflows (19 digits for a 20+-digit run), then reinterprets the retained
1101 // u64 as signed — so `99999999999999999999` wraps to `-8446744073709551617`
1102 // (19 digits), while the fit-in-u64-but-not-i64 band (`9999999999999999999`)
1103 // hits the signed-overflow special case at 18 digits. The old `[..18]` slice
1104 // was one digit short on both counts. Bug #350; mirrors builtin.rs
1105 // parse_int_arg for #258.
1106 let val: i64 = match int_str.parse::<i64>() {
1107 Ok(n) => n,
1108 Err(_) if !int_str.is_empty() && int_str.chars().all(|c| c.is_ascii_digit()) => {
1109 // zstrtol emits the "number truncated after N digits" warning itself.
1110 crate::ported::utils::zstrtol_underscore(&int_str, 10, false).0
1111 }
1112 Err(_) => 0,
1113 };
1114 m_yyval_set(if m_force_float() {
1115 mnumber {
1116 l: 0,
1117 d: if is_neg { -(val as f64) } else { val as f64 },
1118 type_: MN_FLOAT,
1119 }
1120 } else {
1121 mnumber {
1122 l: if is_neg { -val } else { val },
1123 d: 0.0,
1124 type_: MN_INTEGER,
1125 }
1126 });
1127 NUM
1128}
1129
1130// ===========================================================
1131// Remaining stubs from Src/math.c that don't yet have a faithful
1132// implementation in the migrated free-fn evaluator. The
1133// in-place implementations (mathevall, getmathparam, lexconstant,
1134// setmathvar, callmathfunc, checkunary) replaced their stubs;
1135// the names below correspond to C helpers the evaluator uses
1136// internally below — bodies wire to existing Rust idioms while
1137// preserving the C name + citation.
1138// ===========================================================
1139
1140/// Port of `isinf(double x)` from Src/math.c:588 — IEEE +/-Infinity test.
1141/// Wraps Rust's `f64::is_infinite`.
1142/// WARNING: param names don't match C — Rust=() vs C=(x)
1143pub(crate) fn isinf(x: f64) -> bool {
1144 x.is_infinite()
1145}
1146
1147/// Port of `isnan(double x)` from Src/math.c:608 — IEEE NaN test. C
1148/// implements it as `store(&x) != store(&x)` to defeat compiler
1149/// folding of the canonical `x != x` NaN test; we route through
1150/// `store` for parity, but Rust's `f64::is_nan` is the
1151/// correctness path.
1152/// WARNING: param names don't match C — Rust=() vs C=(x)
1153pub(crate) fn isnan(x: f64) -> bool {
1154 store(x) != store(x) || x.is_nan()
1155}
1156
1157/// Port of `notzero(mnumber a)` from Src/math.c:1142 — error-on-zero check
1158/// used by `/` and `%` operators. Returns true when `a` is non-
1159/// zero (caller continues), false when zero (caller raises
1160/// "division by zero"). Float zero is treated as non-zero per
1161/// IEEE 754 (1/0.0 → Inf, not an error) — only integer zero
1162/// trips the check, matching math.c's `if (!a.u.l) zerr(…)`.
1163/// WARNING: param names don't match C — Rust=() vs C=(a)
1164pub(crate) fn notzero(a: mnumber) -> bool {
1165 if (a.type_ == MN_UNSET) {
1166 return false;
1167 }
1168 if (a.type_ == MN_INTEGER) {
1169 return a.l != 0;
1170 }
1171 true
1172}
1173
1174// ============================================================
1175// Module-level math statics — direct port of Src/math.c globals.
1176//
1177// math.c declares each of these at file scope:
1178// int noeval; // line 40
1179// mnumber zero_mnumber; // line 45
1180// mnumber lastmathval; // line 53
1181// int lastbase; // line 58
1182// static char *ptr; // line 60
1183// static mnumber yyval; // line 62
1184// static char *yylval; // line 63
1185// static int mlevel = 0; // line 67
1186// static int unary = 1; // line 71
1187// static struct mathvalue *stack; // (math.c body)
1188// ... and a few derived from option flags (force_float, etc.).
1189//
1190// Rust port: thread_local!<Cell|RefCell<T>> per global. `mathevall`
1191// (math.c:367) saves these to its own locals (`xyyval`, `xyylval`,
1192// `xunary`, etc.) on entry and restores on exit so recursive math
1193// calls (function-arg eval, indirect string eval) don't clobber
1194// the outer evaluator's state.
1195//
1196// Cell for Copy types (i64/i32/usize/bool/mnumber/i32/&'static
1197// slice). RefCell for owned/non-Copy (String, Vec, HashMap, Option).
1198// ============================================================
1199
1200thread_local! {
1201 /// `mnumber lastmathval` (math.c:53) — result of the most recent
1202 /// top-level `matheval`. Read by callmathfunc's MFF_USERFUNC branch
1203 /// (math.c:1115 `return lastmathval`): a `functions -M` math function
1204 /// communicates its result via the last `(( ))` in its body.
1205 static M_LASTMATHVAL: Cell<mnumber> = const { Cell::new(mnumber { l: 0, d: 0.0, type_: MN_INTEGER }) };
1206 /// `static char *ptr` — current input cursor. Owned String in Rust
1207 /// (vs C's caller-owned char*) so the thread_local isn't a borrow.
1208 static M_INPUT: RefCell<String> = const { RefCell::new(String::new()) };
1209 /// Byte offset into `M_INPUT` of the next char to lex.
1210 static M_POS: Cell<usize> = const { Cell::new(0) };
1211 /// Byte offset where the current token began (post-whitespace).
1212 /// Used to format zsh-style "at `<remaining>'" error pointers.
1213 static M_TOK_START: Cell<usize> = const { Cell::new(0) };
1214 /// `static mnumber yyval` (math.c:62) — value lexed by zzlex.
1215 static M_YYVAL: Cell<mnumber> = const { Cell::new(mnumber { l: 0, d: 0.0, type_: MN_INTEGER }) };
1216 /// `static char *yylval` (math.c:63) — identifier or function-call
1217 /// text lexed by zzlex (caller side reads via `M_YYLVAL.with(...)`).
1218 static M_YYLVAL: RefCell<String> = const { RefCell::new(String::new()) };
1219 /// `static struct mathvalue *stack` — operand stack for the
1220 /// shunting-yard evaluator. Mirrors C's heap-grown array.
1221 static M_STACK: RefCell<Vec<mathvalue >> = const { RefCell::new(Vec::new()) };
1222 /// `int mtok` — current token tag set by zzlex.
1223 static M_MTOK: Cell<i32> = const { Cell::new(EOI) };
1224 /// `static int unary` (math.c:71) — 1 when the parser is expecting
1225 /// an operand (so `+`/`-` mean unary plus/minus).
1226 static M_UNARY: Cell<bool> = const { Cell::new(true) };
1227 // nonzero means we are not evaluating, just parsing // c:37
1228 /// `int noeval` (math.c:40) — non-zero when in the parse-only side
1229 /// of `&&`/`||`/ternary; suppresses side-effects.
1230 static M_NOEVAL: Cell<i32> = const { Cell::new(0) }; // c:40
1231 // last input base we used // c:55
1232 /// `int lastbase` (math.c:58) — base of the last numeric literal
1233 /// (set by lexconstant, used by `$((…))` formatting).
1234 static M_LASTBASE: Cell<i32> = const { Cell::new(-1) }; // c:58
1235 /// `int *prec` — active precedence table (Z_PREC or C_PREC).
1236 static M_PREC: Cell<&'static [u8; TOKCOUNT]> = const { Cell::new(&Z_PREC) };
1237 /// `setopt CPRECEDENCES` mirror.
1238 static M_C_PRECEDENCES: Cell<bool> = const { Cell::new(false) };
1239 /// `setopt FORCEFLOAT` mirror.
1240 static M_FORCE_FLOAT: Cell<bool> = const { Cell::new(false) };
1241 /// `setopt OCTALZEROES` mirror.
1242 static M_OCTAL_ZEROES: Cell<bool> = const { Cell::new(false) };
1243 /// In-memory params table (zshrs uses this instead of the C param
1244 /// table). Carries float/integer mnumber results.
1245 static M_VARIABLES: RefCell<HashMap<String, mnumber>> = RefCell::new(HashMap::new());
1246 /// Raw string values for variables whose contents aren't a plain
1247 /// number — recursively re-eval'd by `getmathparam` for
1248 /// `a="3+2"; $((a))` semantics.
1249 static M_STRING_VARIABLES: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
1250 /// `$?` — last command exit status, used by the `?` token in
1251 /// unary position.
1252 static M_LASTVAL: Cell<i32> = const { Cell::new(0) };
1253 /// `$$` — current process ID, lexed for the `$` token.
1254 static M_PID: Cell<i64> = const { Cell::new(0) };
1255 /// Error message accumulator. zsh C uses `setjmp`/`longjmp`; the
1256 /// Rust port returns errors via this Option then `mathevall`
1257 /// surfaces it as `Result::Err`.
1258 static M_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
1259 /// `int outputradix` (math.c:580) — output base for the result.
1260 /// Set by `[#N]` (positive N, with `N#` prefix) or `[##N]`
1261 /// (negative N, bare digits). Read by subst.rs's `$((…))`
1262 /// formatter at math.c:4493-4498.
1263 static M_OUTPUTRADIX: Cell<i32> = const { Cell::new(0) }; // c:580
1264 /// `int outputunderscore` (math.c:583) — group every N digits with
1265 /// `_` for readable hex/decimal output. Set by `[#N_M]` /
1266 /// `[##N_M]` / `[#_M]`. Read alongside outputradix.
1267 static M_OUTPUTUNDERSCORE: Cell<i32> = const { Cell::new(0) }; // c:583
1268 /// `static int mlevel` (math.c:67) — count of `mathevall` frames
1269 /// currently on the stack. C brackets the evaluator body with
1270 /// `if (mlevel++)` (c:386) / `if (--mlevel)` (c:446), so mlevel is 0
1271 /// exactly when no evaluation is in progress. `matheval` reads it to
1272 /// decide whether the output radix is a fresh one (c:1486).
1273 static M_LEVEL: Cell<i32> = const { Cell::new(0) }; // c:67
1274}
1275
1276/// RAII bracket for C's `mlevel++` (math.c:386) / `--mlevel` (math.c:446).
1277///
1278/// C can pair the increment and decrement by hand because `mathevall` has a
1279/// single exit. The Rust evaluator returns `Result` from many points inside the
1280/// parse loop, so the decrement rides on `Drop` — otherwise an `Err` path would
1281/// leave `mlevel` permanently raised and every later `matheval` would skip its
1282/// reset, treating an unrelated expression as a nested one.
1283struct MathLevel;
1284
1285impl MathLevel {
1286 /// c:386 — `if (mlevel++)`.
1287 fn enter() -> Self {
1288 M_LEVEL.with(|c| c.set(c.get() + 1));
1289 MathLevel
1290 }
1291}
1292
1293impl Drop for MathLevel {
1294 /// c:446 — `if (--mlevel)`.
1295 fn drop(&mut self) {
1296 M_LEVEL.with(|c| c.set(c.get() - 1));
1297 }
1298}
1299
1300/// `outputradix` accessor for subst.rs's `$((…))` formatter.
1301/// Returns 0 if no `[#…]` directive was seen during the most
1302/// recent matheval. Caller is responsible for clearing via
1303/// `set_output_format(0, 0)` if it wants per-call state.
1304pub fn outputradix() -> i32 {
1305 M_OUTPUTRADIX.with(|c| c.get())
1306}
1307
1308/// `outputunderscore` accessor — see [`outputradix`].
1309pub fn outputunderscore() -> i32 {
1310 M_OUTPUTUNDERSCORE.with(|c| c.get())
1311}
1312
1313/// Reset the output-format state. Called by `mathevall` before
1314/// each evaluation so `[#16]` from a prior `$((…))` doesn't leak
1315/// into the next call.
1316pub fn reset_output_format() {
1317 M_OUTPUTRADIX.with(|c| c.set(0));
1318 M_OUTPUTUNDERSCORE.with(|c| c.set(0));
1319}
1320
1321fn m_outputradix_set(v: i32) {
1322 M_OUTPUTRADIX.with(|c| c.set(v));
1323}
1324
1325fn m_outputunderscore_set(v: i32) {
1326 M_OUTPUTUNDERSCORE.with(|c| c.set(v));
1327}
1328
1329// ============================================================
1330// WARNING: NOT IN MATH.C — every `m_*` fn below is a Rust-only
1331// thread_local accessor. C dereferences the corresponding module
1332// global directly (`yyval.u.l`, `*ptr++`, etc.) without an
1333// fn-shaped wrapper. The wrappers exist solely because Rust's
1334// `thread_local!` requires `.with(|c| ...)` for any access, and
1335// scattering 600 such closures throughout the evaluator would be
1336// unreadable. Allowlisted in tests/data/fake_fn_allowlist.txt.
1337// ============================================================
1338// Accessor helpers — each thread_local reads/writes via these so the
1339// migration from `s.X` → free-fn-only access is mechanical.
1340
1341#[inline]
1342fn m_input_clone() -> String {
1343 M_INPUT.with(|c| c.borrow().clone())
1344}
1345#[inline]
1346fn m_input_set(v: String) {
1347 M_INPUT.with(|c| *c.borrow_mut() = v)
1348}
1349#[inline]
1350fn m_input_len() -> usize {
1351 M_INPUT.with(|c| c.borrow().len())
1352}
1353#[inline]
1354fn m_input_byte(i: usize) -> u8 {
1355 M_INPUT.with(|c| c.borrow().as_bytes().get(i).copied().unwrap_or(0))
1356}
1357#[inline]
1358fn m_input_slice_from(start: usize) -> String {
1359 M_INPUT.with(|c| c.borrow()[start..].to_string())
1360}
1361#[inline]
1362fn m_input_slice(start: usize, end: usize) -> String {
1363 M_INPUT.with(|c| c.borrow()[start..end].to_string())
1364}
1365
1366#[inline]
1367fn m_pos() -> usize {
1368 M_POS.with(|c| c.get())
1369}
1370#[inline]
1371fn m_pos_set(v: usize) {
1372 M_POS.with(|c| c.set(v))
1373}
1374#[inline]
1375fn m_pos_sub(n: usize) {
1376 M_POS.with(|c| c.set(c.get() - n))
1377}
1378#[inline]
1379fn m_pos_add(n: usize) {
1380 M_POS.with(|c| c.set(c.get() + n))
1381}
1382
1383#[inline]
1384fn m_tok_start() -> usize {
1385 M_TOK_START.with(|c| c.get())
1386}
1387#[inline]
1388fn m_tok_start_set(v: usize) {
1389 M_TOK_START.with(|c| c.set(v))
1390}
1391
1392#[inline]
1393fn m_yyval() -> mnumber {
1394 M_YYVAL.with(|c| c.get())
1395}
1396#[inline]
1397fn m_yyval_set(v: mnumber) {
1398 M_YYVAL.with(|c| c.set(v))
1399}
1400
1401#[inline]
1402fn m_yylval_clone() -> String {
1403 M_YYLVAL.with(|c| c.borrow().clone())
1404}
1405#[inline]
1406fn m_yylval_set(v: String) {
1407 M_YYLVAL.with(|c| *c.borrow_mut() = v)
1408}
1409
1410#[inline]
1411fn m_mtok() -> i32 {
1412 M_MTOK.with(|c| c.get())
1413}
1414#[inline]
1415fn m_mtok_set(t: i32) {
1416 M_MTOK.with(|c| c.set(t))
1417}
1418
1419#[inline]
1420fn m_unary() -> bool {
1421 M_UNARY.with(|c| c.get())
1422}
1423#[inline]
1424fn m_unary_set(v: bool) {
1425 M_UNARY.with(|c| c.set(v))
1426}
1427
1428/// Accessor for the math-evaluator `noeval` counter (`Src/math.c:40`
1429/// `int noeval`). C reads/writes the global directly — Rust ports
1430/// the global as a thread-local `M_NOEVAL` (so nested evaluators
1431/// stay isolated) and exposes the read via this `pub` accessor.
1432/// Used by exec.c's `execsave` / `execrestore` save/restore frame
1433/// (`Src/exec.c:6450,6486`) — the math state must round-trip across
1434/// nested sublist evaluation so a ternary-arm `noeval++/--` inside
1435/// one expression doesn't leak into outer evaluations.
1436#[inline]
1437pub fn m_noeval() -> i32 {
1438 M_NOEVAL.with(|c| c.get())
1439}
1440/// Setter paired with `m_noeval` — assigns the math-evaluator
1441/// `noeval` counter. C does plain `noeval = en->noeval;`; this is
1442/// the Rust thread-local equivalent.
1443#[inline]
1444pub fn m_noeval_set(v: i32) {
1445 M_NOEVAL.with(|c| c.set(v))
1446}
1447#[inline]
1448fn m_noeval_inc() {
1449 M_NOEVAL.with(|c| c.set(c.get() + 1))
1450}
1451#[inline]
1452fn m_noeval_dec() {
1453 M_NOEVAL.with(|c| c.set(c.get() - 1))
1454}
1455
1456#[inline]
1457fn m_lastbase_set(v: i32) {
1458 M_LASTBASE.with(|c| c.set(v))
1459}
1460
1461/// Public getter for `lastbase` — used by `assignstrvalue` in
1462/// params.rs to inherit the input numeric base when a freshly
1463/// assigned integer parameter has none of its own.
1464pub fn lastbase() -> i32 {
1465 M_LASTBASE.with(|c| c.get())
1466}
1467
1468/// Public setter for `lastbase` — used by the bytecode arith
1469/// compiler (extensions/arith_compiler.rs) to communicate the
1470/// source numeric base when a `N#NNN` or `0x..` literal is
1471/// consumed inside `(( … ))`. The canonical math.c port at
1472/// `Src/math.c::lexconstant` sets this internally; bypassing
1473/// the canonical lexer (as `arith_compiler` does) requires
1474/// poking the TLS slot directly so assignsparam's `pm.base ==
1475/// 0 ? lastbase()` inheritance path fires.
1476pub fn set_lastbase(base: i32) {
1477 m_lastbase_set(base)
1478}
1479
1480#[inline]
1481fn m_prec() -> &'static [u8; TOKCOUNT] {
1482 M_PREC.with(|c| c.get())
1483}
1484#[inline]
1485fn m_prec_set(p: &'static [u8; TOKCOUNT]) {
1486 M_PREC.with(|c| c.set(p))
1487}
1488
1489#[inline]
1490fn m_c_precedences() -> bool {
1491 M_C_PRECEDENCES.with(|c| c.get())
1492}
1493#[inline]
1494fn m_c_precedences_set(v: bool) {
1495 M_C_PRECEDENCES.with(|c| c.set(v))
1496}
1497#[inline]
1498fn m_force_float() -> bool {
1499 M_FORCE_FLOAT.with(|c| c.get())
1500}
1501#[inline]
1502fn m_force_float_set(v: bool) {
1503 M_FORCE_FLOAT.with(|c| c.set(v))
1504}
1505#[inline]
1506fn m_octal_zeroes() -> bool {
1507 // c:Src/math.c:489 — `isset(OCTALZEROES)` is read directly at
1508 // each integer-literal parse site, not snapshotted at math-eval
1509 // entry. The thread-local cache here only honored a snapshot
1510 // pushed by arith_compile (line 1205); freshly toggled
1511 // `setopt octalzeroes` inside the same script never reached it.
1512 // Mirror C by reading the option live.
1513 if crate::ported::zsh_h::isset(crate::ported::zsh_h::OCTALZEROES) {
1514 return true;
1515 }
1516 M_OCTAL_ZEROES.with(|c| c.get())
1517}
1518#[inline]
1519fn m_octal_zeroes_set(v: bool) {
1520 M_OCTAL_ZEROES.with(|c| c.set(v))
1521}
1522
1523#[inline]
1524fn m_lastval_set(v: i32) {
1525 M_LASTVAL.with(|c| c.set(v))
1526}
1527#[inline]
1528fn m_lastval() -> i32 {
1529 M_LASTVAL.with(|c| c.get())
1530}
1531#[inline]
1532fn m_pid() -> i64 {
1533 M_PID.with(|c| c.get())
1534}
1535#[inline]
1536fn m_pid_set(v: i64) {
1537 M_PID.with(|c| c.set(v))
1538}
1539
1540#[inline]
1541fn m_error_take() -> Option<String> {
1542 M_ERROR.with(|c| c.borrow_mut().take())
1543}
1544#[inline]
1545fn m_error_some() -> bool {
1546 M_ERROR.with(|c| c.borrow().is_some())
1547}
1548#[inline]
1549fn m_error_set(msg: String) {
1550 M_ERROR.with(|c| {
1551 if c.borrow().is_none() {
1552 *c.borrow_mut() = Some(msg);
1553 }
1554 })
1555}
1556#[inline]
1557fn m_error_set_force(msg: String) {
1558 M_ERROR.with(|c| *c.borrow_mut() = Some(msg))
1559}
1560#[inline]
1561fn m_error_clear() {
1562 M_ERROR.with(|c| *c.borrow_mut() = None)
1563}
1564
1565// Stack helpers — mathvalue stack operations.
1566#[inline]
1567fn m_stack_push(v: mathvalue) {
1568 M_STACK.with(|c| c.borrow_mut().push(v))
1569}
1570#[inline]
1571fn m_stack_pop() -> Option<mathvalue> {
1572 M_STACK.with(|c| c.borrow_mut().pop())
1573}
1574#[inline]
1575fn m_stack_len() -> usize {
1576 M_STACK.with(|c| c.borrow().len())
1577}
1578#[inline]
1579fn m_stack_is_empty() -> bool {
1580 M_STACK.with(|c| c.borrow().is_empty())
1581}
1582#[inline]
1583fn m_stack_top_clone() -> Option<mathvalue> {
1584 M_STACK.with(|c| c.borrow().last().cloned())
1585}
1586
1587// Variable map helpers.
1588#[inline]
1589fn m_variables_get(name: &str) -> Option<mnumber> {
1590 M_VARIABLES.with(|c| c.borrow().get(name).copied())
1591}
1592#[inline]
1593fn m_variables_insert(k: String, v: mnumber) {
1594 M_VARIABLES.with(|c| {
1595 c.borrow_mut().insert(k, v);
1596 })
1597}
1598#[inline]
1599fn m_variables_clone() -> HashMap<String, mnumber> {
1600 M_VARIABLES.with(|c| c.borrow().clone())
1601}
1602#[inline]
1603fn m_variables_set(map: HashMap<String, mnumber>) {
1604 M_VARIABLES.with(|c| *c.borrow_mut() = map)
1605}
1606
1607#[inline]
1608fn m_string_variables_get(name: &str) -> Option<String> {
1609 M_STRING_VARIABLES.with(|c| c.borrow().get(name).cloned())
1610}
1611#[inline]
1612fn m_string_variables_remove(name: &str) {
1613 M_STRING_VARIABLES.with(|c| {
1614 c.borrow_mut().remove(name);
1615 })
1616}
1617#[inline]
1618fn m_string_variables_clone() -> HashMap<String, String> {
1619 M_STRING_VARIABLES.with(|c| c.borrow().clone())
1620}
1621#[inline]
1622fn m_string_variables_set(map: HashMap<String, String>) {
1623 M_STRING_VARIABLES.with(|c| *c.borrow_mut() = map)
1624}
1625#[inline]
1626fn m_string_variables_insert(k: String, v: String) {
1627 M_STRING_VARIABLES.with(|c| {
1628 c.borrow_mut().insert(k, v);
1629 })
1630}
1631
1632/// Save/restore container — mirrors C `mathevall()` (Src/math.c:367)'s
1633/// stack locals (`xyyval`, `xyylval`, `xunary`, `xnoeval`, `xptr`,
1634/// etc.). Wrap recursive math eval (`callmathfunc` arg parsing,
1635/// `getmathparam` indirect-string eval) with `save_state()` /
1636/// `restore_state()` so the parent's evaluator state survives the
1637/// inner call's thread_local mutations.
1638#[allow(non_camel_case_types)]
1639struct xyy_locals {
1640 input: String,
1641 pos: usize,
1642 tok_start: usize,
1643 yyval: mnumber,
1644 yylval: String,
1645 stack: Vec<mathvalue>,
1646 mtok: i32,
1647 unary: bool,
1648 noeval: i32,
1649 error: Option<String>,
1650 variables: HashMap<String, mnumber>,
1651 string_variables: HashMap<String, String>,
1652 prec: &'static [u8; TOKCOUNT],
1653 c_precedences: bool,
1654 force_float: bool,
1655 octal_zeroes: bool,
1656 lastbase: i32,
1657}
1658
1659// WARNING: NOT IN MATH.C — Rust-only helper. C inlines the
1660// xyy* save/restore directly inside `mathevall()`'s body
1661// (math.c:367 onward); the Rust port factors it out because two
1662// callsites (callmathfunc arg parsing, getmathparam indirect-string
1663// eval) would each duplicate ~17 lines of save/restore code.
1664fn save_state() -> xyy_locals {
1665 xyy_locals {
1666 input: m_input_clone(),
1667 pos: m_pos(),
1668 tok_start: m_tok_start(),
1669 yyval: m_yyval(),
1670 yylval: m_yylval_clone(),
1671 stack: M_STACK.with(|c| c.borrow().clone()),
1672 mtok: m_mtok(),
1673 unary: m_unary(),
1674 noeval: m_noeval(),
1675 error: M_ERROR.with(|c| c.borrow().clone()),
1676 variables: m_variables_clone(),
1677 string_variables: m_string_variables_clone(),
1678 prec: m_prec(),
1679 c_precedences: m_c_precedences(),
1680 force_float: m_force_float(),
1681 octal_zeroes: m_octal_zeroes(),
1682 lastbase: M_LASTBASE.with(|c| c.get()),
1683 }
1684}
1685
1686/// Port of `store(double *x)` from Src/math.c:601 — load/store a double
1687/// via a pointer to defeat compilers that mis-optimize the
1688/// canonical `x != x` NaN test. zsh only compiles this path when
1689/// `HAVE_ISNAN` is undefined; we keep it as a name-parity shim
1690/// so `isnan()` can route through it (matching the C source's
1691/// `store(&x) != store(&x)` idiom).
1692/// WARNING: param names don't match C — Rust=() vs C=(x)
1693pub(crate) fn store(x: f64) -> f64 {
1694 x
1695}
1696
1697/// Port of `getcvar(char *s)` from Src/math.c:943 — character-constant
1698/// lookup. Reads the named shell variable and returns the
1699/// codepoint of its first character. Used for `#varname` token
1700/// (CId): `x="hello"; (( y = #x ))` puts 104 (`'h'`) into y.
1701/// On miss or empty value, returns 0 (matches zsh's `*s ? *s : 0`).
1702/// WARNING: param names don't match C — Rust=() vs C=(s)
1703pub(crate) fn getcvar(name: &str) -> mnumber {
1704 if let Some(raw) = m_string_variables_get(name) {
1705 return mnumber {
1706 l: raw.chars().next().map(|c| c as i64).unwrap_or(0),
1707 d: 0.0,
1708 type_: MN_INTEGER,
1709 };
1710 }
1711 // c:Src/math.c:943 — `getcvar` falls back to `getsparam` for
1712 // scalar params not already cached in math-local tables. Without
1713 // this, `a=A; (( #a ))` returned 0 instead of 65 — `m_string_
1714 // variables_get` only sees variables explicitly seeded into the
1715 // math frame.
1716 if let Some(raw) = getsparam(name) {
1717 return mnumber {
1718 l: raw.chars().next().map(|c| c as i64).unwrap_or(0),
1719 d: 0.0,
1720 type_: MN_INTEGER,
1721 };
1722 }
1723 if let Some(v) = m_variables_get(name) {
1724 let s = match v.type_ {
1725 MN_INTEGER => v.l.to_string(),
1726 MN_FLOAT => {
1727 let f = v.d;
1728 if isnan(f) {
1729 "NaN".to_string()
1730 } else if isinf(f) {
1731 if f > 0.0 {
1732 "Inf".to_string()
1733 } else {
1734 "-Inf".to_string()
1735 }
1736 } else {
1737 format!("{:.10}", f)
1738 }
1739 }
1740 _ => "0".to_string(),
1741 };
1742 return mnumber {
1743 l: s.chars().next().map(|c| c as i64).unwrap_or(0),
1744 d: 0.0,
1745 type_: MN_INTEGER,
1746 };
1747 }
1748 mnumber {
1749 l: 0,
1750 d: 0.0,
1751 type_: MN_INTEGER,
1752 }
1753}
1754
1755/// Port of `zzlex()` from `Src/math.c:617`.
1756///
1757/// Main math-expression lexer — returns the next token, advancing
1758/// `m_pos()` and updating `m_yyval()` / `m_yylval_clone()` as side-effects.
1759/// Handles all operators, ident lookahead for `Func` vs `Id`,
1760/// `[base]value` / `[#base]EXPR` output-radix prefixes, char
1761/// constants (`#x`, `##varname`), and dispatches numeric literals
1762/// to `lexconstant()`.
1763pub(crate) fn zzlex() -> i32 {
1764 m_yyval_set(mnumber {
1765 l: 0,
1766 d: 0.0,
1767 type_: MN_INTEGER,
1768 });
1769
1770 loop {
1771 let pre_pos = m_pos();
1772 let c = match advance() {
1773 Some(c) => c,
1774 None => {
1775 m_tok_start_set(pre_pos);
1776 return EOI;
1777 }
1778 };
1779
1780 if matches!(c, ' ' | '\t' | '\n' | '"') {
1781 continue;
1782 }
1783 // Record where this token began (post-whitespace) so error
1784 // formatters can produce zsh-style "at `<remaining>`" messages.
1785 m_tok_start_set(pre_pos);
1786
1787 match c {
1788 '+' => {
1789 if peek() == Some('+') {
1790 advance();
1791 return if m_unary() { PREPLUS } else { POSTPLUS };
1792 }
1793 if peek() == Some('=') {
1794 advance();
1795 return PLUSEQ;
1796 }
1797 return if m_unary() { UPLUS } else { PLUS };
1798 }
1799
1800 '-' => {
1801 if peek() == Some('-') {
1802 advance();
1803 return if m_unary() { PREMINUS } else { POSTMINUS };
1804 }
1805 if peek() == Some('=') {
1806 advance();
1807 return MINUSEQ;
1808 }
1809 if m_unary() {
1810 // Check if followed by digit for negative number
1811 if let Some(next) = peek() {
1812 if is_digit(next) || next == '.' {
1813 m_pos_sub(1); // Put back the -
1814 return lexconstant();
1815 }
1816 }
1817 return UMINUS;
1818 }
1819 return MINUS;
1820 }
1821
1822 '(' => return M_INPAR,
1823 ')' => return M_OUTPAR,
1824
1825 '!' => {
1826 if peek() == Some('=') {
1827 advance();
1828 return NEQ;
1829 }
1830 return NOT;
1831 }
1832
1833 '~' => return COMP,
1834
1835 '&' => {
1836 if peek() == Some('&') {
1837 advance();
1838 if peek() == Some('=') {
1839 advance();
1840 return DANDEQ;
1841 }
1842 return DAND;
1843 }
1844 if peek() == Some('=') {
1845 advance();
1846 return ANDEQ;
1847 }
1848 return AND;
1849 }
1850
1851 '|' => {
1852 if peek() == Some('|') {
1853 advance();
1854 if peek() == Some('=') {
1855 advance();
1856 return DOREQ;
1857 }
1858 return DOR;
1859 }
1860 if peek() == Some('=') {
1861 advance();
1862 return OREQ;
1863 }
1864 return OR;
1865 }
1866
1867 '^' => {
1868 if peek() == Some('^') {
1869 advance();
1870 if peek() == Some('=') {
1871 advance();
1872 return DXOREQ;
1873 }
1874 return DXOR;
1875 }
1876 if peek() == Some('=') {
1877 advance();
1878 return XOREQ;
1879 }
1880 return XOR;
1881 }
1882
1883 '*' => {
1884 if peek() == Some('*') {
1885 advance();
1886 if peek() == Some('=') {
1887 advance();
1888 return POWEREQ;
1889 }
1890 return POWER;
1891 }
1892 if peek() == Some('=') {
1893 advance();
1894 return MULEQ;
1895 }
1896 return MUL;
1897 }
1898
1899 '/' => {
1900 if peek() == Some('=') {
1901 advance();
1902 return DIVEQ;
1903 }
1904 return DIV;
1905 }
1906
1907 '%' => {
1908 if peek() == Some('=') {
1909 advance();
1910 return MODEQ;
1911 }
1912 return MOD;
1913 }
1914
1915 '<' => {
1916 if peek() == Some('<') {
1917 advance();
1918 if peek() == Some('=') {
1919 advance();
1920 return SHLEFTEQ;
1921 }
1922 return SHLEFT;
1923 }
1924 if peek() == Some('=') {
1925 advance();
1926 return LEQ;
1927 }
1928 return LES;
1929 }
1930
1931 '>' => {
1932 if peek() == Some('>') {
1933 advance();
1934 if peek() == Some('=') {
1935 advance();
1936 return SHRIGHTEQ;
1937 }
1938 return SHRIGHT;
1939 }
1940 if peek() == Some('=') {
1941 advance();
1942 return GEQ;
1943 }
1944 return GRE;
1945 }
1946
1947 '=' => {
1948 if peek() == Some('=') {
1949 advance();
1950 return DEQ;
1951 }
1952 return EQ;
1953 }
1954
1955 '$' => {
1956 // $$ = pid
1957 m_yyval_set(mnumber {
1958 l: m_pid(),
1959 d: 0.0,
1960 type_: MN_INTEGER,
1961 });
1962 return NUM;
1963 }
1964
1965 '?' => {
1966 if m_unary() {
1967 // c:Src/math.c:772-776 — `case '?': if (unary) { yyval.u.l
1968 // = lastval; return NUM; } return QUEST;`. Read the live
1969 // `LASTVAL` atomic (zsh C's `lastval` global). The local
1970 // `m_lastval()` cache is unset by any matheval caller —
1971 // it would always be 0. Bug #367.
1972 let lv =
1973 crate::ported::builtin::LASTVAL.load(std::sync::atomic::Ordering::Relaxed);
1974 m_yyval_set(mnumber {
1975 l: lv as i64,
1976 d: 0.0,
1977 type_: MN_INTEGER,
1978 });
1979 return NUM;
1980 }
1981 return QUEST;
1982 }
1983
1984 ':' => return COLON,
1985 ',' => return COMMA,
1986
1987 '[' => {
1988 // [base]value or output format [#base]
1989 if is_digit(peek().unwrap_or('\0')) {
1990 // [base]value
1991 let base_start = m_pos();
1992 while let Some(c) = peek() {
1993 if is_digit(c) {
1994 advance();
1995 } else {
1996 break;
1997 }
1998 }
1999 if peek() != Some(']') {
2000 m_error_set("bad base syntax".to_string());
2001 return EOI;
2002 }
2003 let base_str: String = m_input_clone()[base_start..m_pos()].to_string();
2004 let base: u32 = base_str.parse().unwrap_or(10);
2005 advance(); // skip ]
2006
2007 if !is_digit(peek().unwrap_or('\0')) && !is_ident_start(peek().unwrap_or('\0'))
2008 {
2009 m_error_set("bad base syntax".to_string());
2010 return EOI;
2011 }
2012 // Reject out-of-range bases; from_str_radix panics
2013 // on bases outside [2, 36].
2014 if !(2..=36).contains(&base) {
2015 m_error_set(format!(
2016 "invalid base (must be 2 to 36 inclusive): {}",
2017 base
2018 ));
2019 m_yyval_set(mnumber {
2020 l: 0,
2021 d: 0.0,
2022 type_: MN_INTEGER,
2023 });
2024 return NUM;
2025 }
2026
2027 let val_start = m_pos();
2028 while let Some(c) = peek() {
2029 if c.is_ascii_alphanumeric() {
2030 advance();
2031 } else {
2032 break;
2033 }
2034 }
2035 let val_str = &m_input_clone()[val_start..m_pos()];
2036 let val = crate::ported::utils::zstrtol(val_str, base as i32).0; // c:zstrtol base#N
2037 m_lastbase_set(base as i32);
2038 m_yyval_set(mnumber {
2039 l: val,
2040 d: 0.0,
2041 type_: MN_INTEGER,
2042 });
2043 return NUM;
2044 }
2045 // c:Src/math.c:798-832 — `[#N]` / `[##N]` / `[#_M]`
2046 // output format specifier. Set outputradix to ±N and
2047 // outputunderscore to M (digit-grouping width).
2048 //
2049 // `[#N]` outputradix = +N (emit `N#` prefix)
2050 // `[##N]` outputradix = -N (bare digits, no prefix)
2051 // `[#N_M]` ... plus underscore every M digits
2052 // `[#_M]` outputradix unchanged, underscore = M
2053 // `[#_]` outputradix unchanged, underscore = 3 (default)
2054 //
2055 // Previous Rust port matched `[#…]` and SILENTLY DROPPED
2056 // the directive, so `$(([##16] 255))` returned `255` (decimal)
2057 // instead of `FF`. p10k uses `[##16]` in glyph-code emitters
2058 // and `[#16]` in icon-byte formatting; both were broken.
2059 if peek() == Some('#') {
2060 advance(); // c:798 — skip first `#`
2061 let mut n: i32 = 1; // c:799
2062 if peek() == Some('#') {
2063 // c:800 — second `#` flips sign for "no prefix"
2064 n = -1; // c:801
2065 advance(); // c:802
2066 }
2067 let p_now = peek().unwrap_or('\0');
2068 if !is_digit(p_now) && p_now != '_' {
2069 // c:804-805
2070 m_error_set("bad output format specification".to_string());
2071 return EOI;
2072 }
2073 let mut checkradix = false;
2074 if is_digit(p_now) {
2075 // c:806-809 — `outputradix = n * zstrtol(ptr, &ptr, 10);`
2076 let rstart = m_pos();
2077 while let Some(c) = peek() {
2078 if is_digit(c) {
2079 advance();
2080 } else {
2081 break;
2082 }
2083 }
2084 let radix_str: String = m_input_clone()[rstart..m_pos()].to_string();
2085 let radix: i32 = radix_str.parse().unwrap_or(10);
2086 m_outputradix_set(n * radix); // c:807
2087 checkradix = true; // c:808
2088 }
2089 if peek() == Some('_') {
2090 // c:810-816 — `[…_M]` underscore digit-grouping width.
2091 advance(); // c:811
2092 let us_now = peek().unwrap_or('\0');
2093 if is_digit(us_now) {
2094 let ustart = m_pos();
2095 while let Some(c) = peek() {
2096 if is_digit(c) {
2097 advance();
2098 } else {
2099 break;
2100 }
2101 }
2102 let us_str: String = m_input_clone()[ustart..m_pos()].to_string();
2103 m_outputunderscore_set(us_str.parse().unwrap_or(3));
2104 // c:812-813
2105 } else {
2106 m_outputunderscore_set(3); // c:814-815 default
2107 }
2108 }
2109 if peek() != Some(']') {
2110 // c:822-823
2111 m_error_set("bad output format specification".to_string());
2112 return EOI;
2113 }
2114 advance(); // c:832 — skip `]`
2115 if checkradix {
2116 // c:824-831 — validate base ∈ [2, 36].
2117 let abs_n = M_OUTPUTRADIX.with(|c| c.get()).abs();
2118 if !(2..=36).contains(&abs_n) {
2119 m_error_set(format!(
2120 "invalid base (must be 2 to 36 inclusive): {}",
2121 M_OUTPUTRADIX.with(|c| c.get())
2122 ));
2123 return EOI;
2124 }
2125 }
2126 // c:833 — `break;` — fall through to the next token
2127 // (the format directive doesn't yield a NUM itself).
2128 continue;
2129 }
2130 m_error_set("bad output format specification".to_string());
2131 return EOI;
2132 }
2133
2134 '#' => {
2135 // Character code: #\x or ##string
2136 if peek() == Some('\\') || peek() == Some('#') {
2137 advance(); // consume the `\` / 2nd `#` marker
2138 // c:852-854 — `ptr++; if (!*ptr) { zerr("bad math
2139 // expression: character missing after ##"); return EOI; }`.
2140 // `$((##))` with nothing after the marker is an error, not 0.
2141 if peek().is_none() {
2142 crate::ported::utils::zerr(
2143 "bad math expression: character missing after ##",
2144 );
2145 return EOI;
2146 }
2147 // c:Src/math.c:856 — `getkeystring(ptr, NULL,
2148 // GETKEYS_MATH, &v)` decodes the char AFTER the
2149 // marker, honoring backslash escapes: `##\n` → 10,
2150 // `##\e` → 27, `##A` → 65. The previous port read a
2151 // single literal char, so `##\n` yielded 92 (`\`)
2152 // and left `n` dangling ("operator expected").
2153 let rest: String = m_input_clone()[m_pos()..].to_string();
2154 if let Some((code, consumed)) = decode_math_keychar(&rest) {
2155 for _ in 0..consumed {
2156 advance();
2157 }
2158 m_yyval_set(mnumber {
2159 l: code,
2160 d: 0.0,
2161 type_: MN_INTEGER,
2162 });
2163 return NUM;
2164 }
2165 if let Some(ch) = advance() {
2166 m_yyval_set(mnumber {
2167 l: ch as i64,
2168 d: 0.0,
2169 type_: MN_INTEGER,
2170 });
2171 return NUM;
2172 }
2173 }
2174 // #varname - get first char value
2175 let id_start = m_pos();
2176 while let Some(c) = peek() {
2177 if is_ident(c) {
2178 advance();
2179 } else {
2180 break;
2181 }
2182 }
2183 if m_pos() > id_start {
2184 m_yylval_set(m_input_clone()[id_start..m_pos()].to_string());
2185 return CID;
2186 }
2187 // c:Src/math.c:911-915 — bare `#` (followed by non-ident) is
2188 // `$#` (positional-parameter count): `yyval.u.l =
2189 // poundgetfn(NULL); return NUM;`. Bug #368.
2190 m_yyval_set(mnumber {
2191 l: crate::ported::params::poundgetfn(),
2192 d: 0.0,
2193 type_: MN_INTEGER,
2194 });
2195 return NUM;
2196 }
2197
2198 _ => {
2199 if is_digit(c) || (c == '.' && is_digit(peek().unwrap_or('\0'))) {
2200 m_pos_sub(c.len_utf8());
2201 return lexconstant();
2202 }
2203
2204 if is_ident_start(c) {
2205 let id_start = m_pos() - c.len_utf8();
2206 while let Some(c) = peek() {
2207 if is_ident(c) {
2208 advance();
2209 } else {
2210 break;
2211 }
2212 }
2213
2214 let id = &m_input_clone()[id_start..m_pos()];
2215
2216 // Check for Inf/NaN
2217 let id_lower = id.to_lowercase();
2218 if id_lower == "nan" {
2219 m_yyval_set(mnumber {
2220 l: 0,
2221 d: f64::NAN,
2222 type_: MN_FLOAT,
2223 });
2224 return NUM;
2225 }
2226 if id_lower == "inf" {
2227 m_yyval_set(mnumber {
2228 l: 0,
2229 d: f64::INFINITY,
2230 type_: MN_FLOAT,
2231 });
2232 return NUM;
2233 }
2234
2235 // Check for function call
2236 if peek() == Some('(') {
2237 // Skip to closing paren
2238 let func_start = id_start;
2239 advance(); // (
2240 let mut depth = 1;
2241 while let Some(c) = peek() {
2242 advance();
2243 if c == '(' {
2244 depth += 1;
2245 } else if c == ')' {
2246 depth -= 1;
2247 if depth == 0 {
2248 break;
2249 }
2250 }
2251 }
2252 m_yylval_set(m_input_clone()[func_start..m_pos()].to_string());
2253 return FUNC;
2254 }
2255
2256 // Check for array subscript
2257 if peek() == Some('[') {
2258 advance(); // [
2259 let mut depth = 1;
2260 while let Some(c) = peek() {
2261 advance();
2262 if c == '[' {
2263 depth += 1;
2264 } else if c == ']' {
2265 depth -= 1;
2266 if depth == 0 {
2267 break;
2268 }
2269 }
2270 }
2271 }
2272
2273 m_yylval_set(m_input_clone()[id_start..m_pos()].to_string());
2274 return ID;
2275 }
2276
2277 // c:842 — `default: if (idigit(*--ptr) ...` — the C
2278 // default case BACKS UP so an unrecognized char (e.g.
2279 // `'`) is left un-consumed; matheval's trailing-junk
2280 // check (c:1498-1499) then reports THAT char:
2281 // `$(( 'A' ))` → "illegal character: '" not ": A".
2282 m_pos_sub(c.len_utf8());
2283 return EOI;
2284 }
2285 }
2286 }
2287}
2288
2289impl Default for mathvalue {
2290 fn default() -> Self {
2291 mathvalue {
2292 val: mnumber {
2293 l: 0,
2294 d: 0.0,
2295 type_: MN_INTEGER,
2296 },
2297 lval: None,
2298 pval: (),
2299 }
2300 }
2301}
2302
2303/// Port of `push(mnumber val, char *lval, int getme)` from `Src/math.c:916`.
2304///
2305/// Push a value onto the evaluator's operand stack, with the
2306/// optional lvalue name (set when the value came from a variable
2307/// reference; needed for `++`/`--`/assignment-op write-back).
2308/// WARNING: param names don't match C — Rust=(lval) vs C=(val, lval, getme)
2309pub(crate) fn push(val: mnumber, lval: Option<String>) {
2310 m_stack_push(mathvalue {
2311 val,
2312 lval,
2313 pval: (),
2314 });
2315}
2316
2317/// Port of `pop(int noget)` from `Src/math.c:931`.
2318///
2319/// Pop the top operand from the stack, resolving any deferred
2320/// variable read (`mnumber { l: 0, d: 0.0, type_: MN_UNSET }` + lval set). The C source
2321/// passes a `noget` flag to skip the resolution; the Rust port
2322/// always resolves since callers that want the raw lvalue use
2323/// `pop_with_lval` instead.
2324/// WARNING: param names don't match C — Rust=() vs C=(noget)
2325pub(crate) fn pop() -> mnumber {
2326 if let Some(mv) = m_stack_pop() {
2327 if (mv.val.type_ == MN_UNSET) {
2328 if let Some(ref name) = mv.lval {
2329 return getmathparam(name);
2330 }
2331 }
2332 mv.val
2333 } else {
2334 m_error_set("stack underflow".to_string());
2335 mnumber {
2336 l: 0,
2337 d: 0.0,
2338 type_: MN_INTEGER,
2339 }
2340 }
2341}
2342
2343/// Port of `setmathvar(struct mathvalue *mvp, mnumber v)` from `Src/math.c:972`.
2344///
2345/// Write `val` to the named parameter from inside math context.
2346/// Port of `setmathvar(struct mathvalue *mvp, mnumber v)` from `Src/math.c:972`.
2347/// Calls `setnparam` (the canonical param-set) and returns the value
2348/// re-typed to match the parameter's type (C c:1014-1027).
2349pub(crate) fn setmathvar(name: &str, val: mnumber) -> mnumber {
2350 // c:972
2351 // c:996-1001 — bad-lvalue check (empty name).
2352 if name.is_empty() {
2353 zerr("bad math expression: lvalue required");
2354 return mnumber {
2355 l: 0,
2356 d: 0.0,
2357 type_: MN_INTEGER,
2358 };
2359 }
2360 // c:1002-1003 — `if (noeval) return v;`
2361 if M_NOEVAL.with(|n| n.get()) != 0 {
2362 return val;
2363 }
2364 // c:1004 — `setnparam(mvp->lval, v)`. C passes the FULL lval
2365 // (including any `[subscript]`) to setnparam → assignnparam,
2366 // which calls getvalue to resolve the subscript and routes the
2367 // write via setnumvalue on the resulting Value (whose v->pm for
2368 // a hash element is the hash-element scalar shim — see
2369 // params.c:640 `foundparam` set by scanparamvals at c:664). The
2370 // previous Rust port stripped the subscript here and called
2371 // setnparam("counts", val) for `counts[apple]++` — silently
2372 // wiping the assoc/array and replacing it with a scalar.
2373 //
2374 // Until the foundparam/PM_HASHELEM scalar-shim path lands in
2375 // assignnparam, route subscripted writes through `assignsparam`
2376 // (which already handles PM_HASHED + PM_ARRAY[idx] writes at
2377 // params.rs:4880-4914). For PM_ARRAY targets pre-evaluate the
2378 // subscript body via `matheval` so `arr[i + 1]` becomes
2379 // `arr[3]` before assignsparam parses the body as i64 — same
2380 // dispatch C's getarg (params.c:1367) performs internally via
2381 // mathevalarg.
2382 if let Some(bi) = name.find('[') {
2383 let close = name.rfind(']').unwrap_or(name.len());
2384 let base = &name[..bi];
2385 let body = if close > bi { &name[bi + 1..close] } else { "" };
2386 // PM_HASHED → literal-string subscript (no math eval).
2387 // PM_ARRAY / unset → math-eval the subscript body.
2388 let is_hashed = {
2389 let tab = crate::ported::params::paramtab().read();
2390 tab.ok()
2391 .and_then(|t| {
2392 t.get(base)
2393 .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_HASHED)
2394 })
2395 .unwrap_or(false)
2396 };
2397 let canonical = if is_hashed {
2398 name.to_string()
2399 } else {
2400 // Save/restore evaluator state around the recursive
2401 // matheval — mirrors getmathparam at math.rs:230 and
2402 // C mathevall's xyy* save/restore pattern (math.c:367).
2403 let saved = save_state();
2404 let idx_val = matheval(body)
2405 .map(|n| if n.type_ == MN_FLOAT { n.d as i64 } else { n.l })
2406 .unwrap_or(0);
2407 restore_state(saved);
2408 format!("{}[{}]", base, idx_val)
2409 };
2410 // Render mnumber as decimal string for assignsparam's
2411 // numeric subscript-write path. PM_ARRAY/PM_HASHED store
2412 // strings; assignsparam writes them straight through.
2413 let val_str = if val.type_ == MN_FLOAT {
2414 crate::ported::params::convfloat_underscore(val.d, 0)
2415 } else {
2416 crate::ported::params::convbase_underscore(val.l, 10, 0)
2417 };
2418 let _ = crate::ported::params::assignsparam(&canonical, &val_str, 0);
2419 // Cache the resolved (canonical) name so a subsequent read
2420 // of the same subscript in the SAME math expression sees
2421 // the new value without a paramtab round-trip.
2422 m_variables_insert(canonical, val);
2423 return val;
2424 }
2425
2426 // Unsubscripted path — cache by name and route through setnparam
2427 // as before. The canonical paramtab write inside setnparam is
2428 // what makes the value persist beyond the current $((…)).
2429 m_variables_insert(name.to_string(), val);
2430 // c:1005 — `pm = setnparam(mvp->lval, v);`
2431 let pm = crate::ported::params::setnparam(name, val);
2432 // c:1006-1027 — re-type the return per the param's type after setnparam.
2433 if let Some(pm) = pm {
2434 let flags = pm.node.flags as u32;
2435 if flags & PM_INTEGER != 0 {
2436 let l = if val.type_ == MN_FLOAT {
2437 val.d as i64
2438 } else {
2439 val.l
2440 };
2441 return mnumber {
2442 l,
2443 d: 0.0,
2444 type_: MN_INTEGER,
2445 };
2446 }
2447 if flags & (PM_EFLOAT | PM_FFLOAT) != 0 {
2448 let d = if val.type_ == MN_INTEGER {
2449 val.l as f64
2450 } else {
2451 val.d
2452 };
2453 return mnumber {
2454 l: 0,
2455 d,
2456 type_: MN_FLOAT,
2457 };
2458 }
2459 }
2460 val
2461}
2462
2463/// Call a math function
2464/// Port of `callmathfunc(char *o)` from `Src/math.c:1037`.
2465/// WARNING: param names don't match C — Rust=() vs C=(o)
2466pub(crate) fn callmathfunc(call: &str) -> mnumber {
2467 // Parse function name and args
2468 let paren = call.find('(').unwrap_or(call.len());
2469 let name = &call[..paren];
2470 // c:Src/math.c:1037 — `callmathfunc` looks up `name` in the
2471 // global `mathfuncs` table. The table is empty until
2472 // `zmodload zsh/mathfunc` (Src/Modules/mathfunc.c mtab[]) is
2473 // loaded. Without it, every named call fails with "unknown
2474 // function: NAME" (Src/math.c:1066). The previous Rust port
2475 // unconditionally dispatched against the built-in match arms,
2476 // auto-loading the module's contents — `zsh -fc 'echo
2477 // $((sqrt(4)))'` should exit 1, not silently return `2.`.
2478 let is_module_func = matches!(
2479 name,
2480 "abs"
2481 | "acos"
2482 | "acosh"
2483 | "asin"
2484 | "asinh"
2485 | "atan"
2486 | "atanh"
2487 | "cbrt"
2488 | "ceil"
2489 | "copysign"
2490 | "cos"
2491 | "cosh"
2492 | "erf"
2493 | "erfc"
2494 | "exp"
2495 | "expm1"
2496 | "fabs"
2497 | "float"
2498 | "floor"
2499 | "fmod"
2500 | "gamma"
2501 | "hypot"
2502 | "ilogb"
2503 | "int"
2504 | "j0"
2505 | "j1"
2506 | "jn"
2507 | "ldexp"
2508 | "lgamma"
2509 | "log"
2510 | "log10"
2511 | "log1p"
2512 | "log2"
2513 | "logb"
2514 | "nextafter"
2515 | "rand48"
2516 | "rint"
2517 | "scalb"
2518 | "sin"
2519 | "sinh"
2520 | "sqrt"
2521 | "tan"
2522 | "tanh"
2523 | "y0"
2524 | "y1"
2525 | "yn"
2526 );
2527 // c:Src/module.c:2206-2322 `load_module` — the post-init flag
2528 // signaling "this module's setup/boot ran" is MOD_INIT_B (set
2529 // at c:2322 after do_boot_module). MOD_LINKED alone is just
2530 // "statically linkable" and is pre-set for every builtin
2531 // module at registration time in modulestab::init_builtin
2532 // (zsh_h.rs:758) — so it's true even before any `zmodload`.
2533 // Gate on MOD_INIT_B to mirror C's "the module's mtab[] is
2534 // currently in the global mathfuncs table".
2535 let module_loaded = crate::ported::module::MODULESTAB
2536 .lock()
2537 .ok()
2538 .and_then(|tab| {
2539 tab.modules.get("zsh/mathfunc").map(|m| {
2540 let flags = m.node.flags;
2541 (flags & crate::ported::zsh_h::MOD_INIT_B) != 0
2542 && (flags & crate::ported::zsh_h::MOD_UNLOAD) == 0
2543 })
2544 })
2545 .unwrap_or(false);
2546 // c:Src/math.c:1108-1116 — MFF_USERFUNC branch: when the named
2547 // math function points at a user shfunc (registered via
2548 // `functions -M`), dispatch via doshfunc instead of looking it
2549 // up in mathfuncs. The body sets `lastmathval` and returns;
2550 // callmathfunc reads it back. Routed here BEFORE the module
2551 // arms below so a user-registered fn shadows a built-in name
2552 // (matching C lookup order).
2553 //
2554 // C zsh's `Src/math.c:1037-1116 callmathfunc` walks the
2555 // canonical `mathfuncs` table (Src/module.c:1258) — a shell
2556 // function with the same name as the math call is NOT
2557 // dispatched unless an MFF_USERFUNC entry was installed via
2558 // `functions -M`. Bug #360: previously zshrs dispatched ANY
2559 // matching shfunc, so unregistered `myadd() {…}; $((myadd(2,3)))`
2560 // entered doshfunc and produced a math-error rather than
2561 // "unknown function: myadd" (the zsh behavior).
2562 //
2563 // Gate the dispatch on a present MFF_USERFUNC entry whose
2564 // shfunc handler resolves to `name` (per C math.c:1108's
2565 // `if (f->flags & MFF_USERFUNC)` check).
2566 // c:1109 — `shfnam = f->module ? f->module : n`. A `functions -M`
2567 // entry can map the math name to a DIFFERENT implementing shell
2568 // function (the optional 4th arg): `functions -M addtwo 2 2 _addtwo`
2569 // dispatches to `_addtwo`. Resolve the impl name from the entry's
2570 // `module` field, falling back to the math name.
2571 // c:1108-1109 + c:1106-1107 — resolve the implementing shfunc name
2572 // AND the registered [minargs, maxargs] bounds together, so the
2573 // arg-count check below sees the same entry that doshfunc dispatches
2574 // to.
2575 let userfunc_impl: Option<(String, i32, i32)> = crate::ported::module::MATHFUNCS
2576 .lock()
2577 .ok()
2578 .and_then(|tab| {
2579 tab.iter()
2580 .find(|p| p.name == name && (p.flags & crate::ported::zsh_h::MFF_USERFUNC) != 0)
2581 .map(|p| {
2582 (
2583 p.module.clone().unwrap_or_else(|| p.name.clone()),
2584 p.minargs,
2585 p.maxargs,
2586 )
2587 })
2588 });
2589 if let Some((impl_name, minargs, maxargs)) = userfunc_impl {
2590 if let Some(mut shfunc) = crate::ported::utils::getshfunc(&impl_name) {
2591 // c:1059-1062 — `addlinknode(l, n)`: the FIRST positional ($0)
2592 // is the MATH function NAME (`max`/`min`), NOT the implementing
2593 // shfunc name. A shared impl (zmathfunc registers max/min/sum to
2594 // one function) switches on $0, so it must see the math name.
2595 // The body to RUN is still the impl shfunc.
2596 let mut largs: Vec<String> = vec![name.to_string()];
2597 let argv_str: Vec<String> = call[paren..]
2598 .trim_start_matches('(')
2599 .trim_end_matches(')')
2600 .split(',')
2601 .map(|s| s.trim().to_string())
2602 .filter(|s| !s.is_empty())
2603 .collect();
2604 // c:Src/math.c:1106-1107 — `if (argc >= f->minargs &&
2605 // (f->maxargs < 0 || argc <= f->maxargs))`. The actual arg count
2606 // (NOT counting the math-fn name pushed as $0 at c:1061) must be
2607 // within the registered bounds; `maxargs < 0` means unbounded.
2608 // On mismatch C falls to c:1127 `zerr("wrong number of
2609 // arguments: %s", o)` where `o` is the original `name(args)`
2610 // call text, and aborts the math eval. Without this check zshrs
2611 // dispatched the body anyway (e.g. a 0-arg `functions -M` fn
2612 // called as `cube(3)` ran the body instead of erroring).
2613 let argc = argv_str.len() as i32;
2614 if argc < minargs || (maxargs >= 0 && argc > maxargs) {
2615 crate::ported::utils::zerr(&format!("wrong number of arguments: {}", call)); // c:1127
2616 crate::ported::utils::errflag.fetch_or(
2617 crate::ported::zsh_h::ERRFLAG_ERROR,
2618 std::sync::atomic::Ordering::Relaxed,
2619 );
2620 return mnumber {
2621 l: 0,
2622 d: 0.0,
2623 type_: MN_INTEGER,
2624 };
2625 }
2626 largs.extend(argv_str.iter().cloned());
2627 let name_for_body = impl_name.clone();
2628 let body_args = argv_str.clone();
2629 let body_runner = move || -> i32 {
2630 crate::ported::exec::run_function_body(&name_for_body, &body_args).unwrap_or(0)
2631 };
2632 // c:1114 — `doshfunc(shfunc, l, 1)`. The body runs a nested
2633 // `(( ))` which RE-ENTERS this evaluator and clobbers the outer
2634 // parser's input/pos/stack thread-locals; save + restore them
2635 // around the call so the OUTER `$(( fn(x) ))` keeps parsing
2636 // (without this it errored "operand expected at end of string").
2637 // M_LASTMATHVAL is NOT part of save_state, so the body's last
2638 // `(( ))` result survives the restore.
2639 let saved = save_state();
2640 let _ = crate::ported::exec::doshfunc(&mut shfunc, largs, true, body_runner);
2641 restore_state(saved);
2642 // c:1115 — `return lastmathval`. The body's last arithmetic
2643 // evaluation (e.g. `(( REPLY = $1 + 2 ))`) is the function's value.
2644 return M_LASTMATHVAL.with(|c| c.get());
2645 } else {
2646 // c:Src/math.c:1110-1112 — the math function IS registered
2647 // (MFF_USERFUNC via `functions -M`), but its implementing shell
2648 // function doesn't exist: `zerr("no such function: %s", shfnam)`.
2649 // This is distinct from the `unknown function` of an
2650 // UN-registered math name (c:1131); zshrs previously fell
2651 // through to that generic message.
2652 crate::ported::utils::zerr(&format!("no such function: {}", impl_name)); // c:1112
2653 crate::ported::utils::errflag.fetch_or(
2654 crate::ported::zsh_h::ERRFLAG_ERROR,
2655 std::sync::atomic::Ordering::Relaxed,
2656 );
2657 return mnumber {
2658 l: 0,
2659 d: 0.0,
2660 type_: MN_INTEGER,
2661 };
2662 }
2663 } // close `if mathfunc_entry.is_some()`
2664
2665 if is_module_func && !module_loaded {
2666 // c:Src/math.c:1050 — `if ((f = getmathfunc(n, 1)))`: the
2667 // lookup with autol=1 IS the autoload fire. `zmodload -af
2668 // zsh/mathfunc sin` installs a MATHFUNCS stub (module.c:1410
2669 // add_automathfunc); getmathfunc removes the stub and
2670 // ensurefeature-loads the owning module (module.c:1289-1301).
2671 // On a hit, fall through to the evaluation arms below (the
2672 // module is now booted). Without this, the registered
2673 // autoload never fired and `$(( sin(0) ))` errored
2674 // `unknown function: sin` despite the -af registration.
2675 let autoloaded = crate::ported::module::MODULESTAB
2676 .lock()
2677 .ok()
2678 .map(|mut tab| crate::ported::module::getmathfunc(&mut tab, name, 1).is_some())
2679 .unwrap_or(false);
2680 if !autoloaded {
2681 crate::ported::utils::zerr(&format!("unknown function: {}", name));
2682 crate::ported::utils::errflag.fetch_or(
2683 crate::ported::zsh_h::ERRFLAG_ERROR,
2684 std::sync::atomic::Ordering::Relaxed,
2685 );
2686 return mnumber {
2687 l: 0,
2688 d: 0.0,
2689 type_: MN_INTEGER,
2690 };
2691 }
2692 }
2693 let args_str = if paren < call.len() {
2694 &call[paren + 1..call.len() - 1]
2695 } else {
2696 ""
2697 };
2698
2699 // c:Src/math.c:1051-1052 — `if ((f->flags & (MFF_STR|MFF_USERFUNC))
2700 // == MFF_STR) return f->sfunc(n, a, f->funcid);`. A pure string
2701 // math function receives the raw, UN-evaluated arg text (here the
2702 // name of the parameter holding the seed) and is dispatched before
2703 // the numeric arg-eval below. `rand48` (mathfunc.c:154
2704 // STRMATHFUNC("rand48", math_string, MS_RAND48)) is the only one.
2705 if name == "rand48" {
2706 return math_string(name, args_str, MS_RAND48);
2707 }
2708
2709 // Parse arguments. Keep both the float view (for trig) and the
2710 // original mnumber so int-preserving functions (abs/min/max/
2711 // int/floor/ceil/trunc) can return integer when all inputs
2712 // were integer.
2713 let arg_nums: Vec<mnumber> = if args_str.is_empty() {
2714 vec![]
2715 } else {
2716 args_str
2717 .split(',')
2718 .filter_map(|arg| {
2719 // Save caller's eval state, sub-eval each arg in a
2720 // fresh state inheriting caller's variables, restore.
2721 // C `mathevall()` xyy* save/restore (math.c:367).
2722 let saved = save_state();
2723 let inherited_vars = saved.variables.clone();
2724 new(arg.trim());
2725 m_variables_set(inherited_vars);
2726 let result = mathevall();
2727 restore_state(saved);
2728 // c:math.c::callmathfunc — when a function-arg subeval
2729 // fails, the C body's mathevall has already zerr'd the
2730 // parse error. Rust's mathevall captures the message
2731 // in Err; the previous .ok() discarded it silently,
2732 // so `$(( abs(1 2) ))` returned 0 instead of erroring.
2733 match result {
2734 Ok(n) => Some(n),
2735 Err(msg) => {
2736 crate::ported::utils::zerr(&msg);
2737 None
2738 }
2739 }
2740 })
2741 .collect()
2742 };
2743 let args: Vec<f64> = arg_nums
2744 .iter()
2745 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
2746 .collect();
2747 // c:Src/math.c:1106-1107 + c:1127 — every math function's arg count
2748 // must be within its registered [minargs, maxargs] bounds (maxargs<0
2749 // = unbounded); on mismatch C errors "wrong number of arguments:
2750 // NAME(args)" and aborts. The MFF_USERFUNC arity check above covers
2751 // shfunc-backed entries; this mirrors it for the NUMERIC built-in
2752 // dispatch so e.g. `atan(1,2,3)` errors (atan is registered 1..2)
2753 // instead of silently dropping the extra arg. Bounds come from the
2754 // ported NUMMATHFUNC table (modules/mathfunc.rs num()).
2755 if let Some((minargs, maxargs)) = crate::ported::module::MATHFUNCS
2756 .lock()
2757 .ok()
2758 .and_then(|tab| {
2759 tab.iter()
2760 .find(|p| p.name == name)
2761 .map(|p| (p.minargs, p.maxargs))
2762 })
2763 {
2764 let argc = args.len() as i32;
2765 if argc < minargs || (maxargs >= 0 && argc > maxargs) {
2766 crate::ported::utils::zerr(&format!("wrong number of arguments: {}", call)); // c:1127
2767 crate::ported::utils::errflag.fetch_or(
2768 crate::ported::zsh_h::ERRFLAG_ERROR,
2769 std::sync::atomic::Ordering::Relaxed,
2770 );
2771 return mnumber {
2772 l: 0,
2773 d: 0.0,
2774 type_: MN_INTEGER,
2775 };
2776 }
2777 }
2778 let all_int = !arg_nums.is_empty() && arg_nums.iter().all(|n| (n.type_ == MN_INTEGER));
2779
2780 // c:Src/Modules/mathfunc.c:139 — only `int` has TFLAG(TF_NOASS)
2781 // which collapses the result to MN_INTEGER. `ceil`/`floor` lack
2782 // TF_NOASS so they return float (rendered as `5.` for whole
2783 // values), and `trunc` doesn't exist in zsh's mathfunc table at
2784 // all — it must error "unknown function: trunc" like zsh.
2785 // The previous Rust port forced all four to integer, so
2786 // `$(( ceil(1.1) ))` printed `2` instead of zsh's `2.`.
2787 let always_int = matches!(name, "int");
2788 if always_int {
2789 let i = match name {
2790 "int" => arg_nums
2791 .first()
2792 .map(|n| (if n.type_ == MN_FLOAT { n.d as i64 } else { n.l }))
2793 .unwrap_or(0),
2794 _ => 0,
2795 };
2796 return mnumber {
2797 l: i,
2798 d: 0.0,
2799 type_: MN_INTEGER,
2800 };
2801 }
2802 // c:Src/Modules/mathfunc.c:115 — only `abs` is a real mathfunc that
2803 // returns an integer when fed integers. `min`/`max` are NOT mathfunc
2804 // entries (zsh provides them only via the `zmathfunc` autoload, which
2805 // registers them as `functions -M` shfuncs handled by the userfunc
2806 // path above) — calling them through zsh/mathfunc errors "unknown
2807 // function". Keeping them here let `zmodload zsh/mathfunc; min(1,2)`
2808 // wrongly return a value.
2809 let int_preserving = matches!(name, "abs");
2810 if all_int && int_preserving {
2811 let i = match name {
2812 "abs" => arg_nums
2813 .first()
2814 .map(|n| (if n.type_ == MN_FLOAT { n.d as i64 } else { n.l }).abs())
2815 .unwrap_or(0),
2816 _ => 0,
2817 };
2818 return mnumber {
2819 l: i,
2820 d: 0.0,
2821 type_: MN_INTEGER,
2822 };
2823 }
2824
2825 // c:Src/Modules/system.c:467/900 — zsh/system registers the
2826 // `systell` math function (NUMMATHFUNC("systell", math_systell)).
2827 // It returns lseek(fd, 0, SEEK_CUR). Dispatch to the ported
2828 // math_systell when zsh/system is loaded; otherwise fall through to
2829 // the "unknown function" error like any unregistered name (gated the
2830 // same way as the zsh/mathfunc functions above).
2831 if name == "systell" {
2832 let system_loaded = crate::ported::module::MODULESTAB
2833 .lock()
2834 .ok()
2835 .and_then(|tab| {
2836 tab.modules.get("zsh/system").map(|m| {
2837 let flags = m.node.flags;
2838 (flags & crate::ported::zsh_h::MOD_INIT_B) != 0
2839 && (flags & crate::ported::zsh_h::MOD_UNLOAD) == 0
2840 })
2841 })
2842 .unwrap_or(false);
2843 if system_loaded {
2844 let argv: Vec<mnumber> = args
2845 .iter()
2846 .map(|&x| mnumber {
2847 l: x as i64,
2848 d: x,
2849 type_: if x.fract() == 0.0 {
2850 MN_INTEGER
2851 } else {
2852 MN_FLOAT
2853 },
2854 })
2855 .collect();
2856 return crate::ported::modules::system::math_systell(
2857 "systell",
2858 argv.len() as i32,
2859 &argv,
2860 0,
2861 );
2862 }
2863 }
2864
2865 // c:Src/Modules/mathfunc.c:24-44 — extern math fns provided by
2866 // libc on every UNIX. Rust's `f64` exposes most directly
2867 // (acosh/asinh/atanh/sqrt/...). The libgm-only ones (erf/erfc/
2868 // tgamma/lgamma/j0/j1/y0/y1/ilogb/logb/cbrt/expm1/log1p/
2869 // copysign/nextafter/fmod) need an explicit C ABI binding.
2870 #[cfg(unix)]
2871 extern "C" {
2872 fn erf(x: f64) -> f64;
2873 fn erfc(x: f64) -> f64;
2874 fn lgamma(x: f64) -> f64;
2875 fn tgamma(x: f64) -> f64;
2876 fn ilogb(x: f64) -> i32;
2877 fn logb(x: f64) -> f64;
2878 fn j0(x: f64) -> f64;
2879 fn j1(x: f64) -> f64;
2880 // c:Src/Modules/mathfunc.c:334/421 — `jn(argi, argd2)` /
2881 // `yn(argi, argd2)`: the ORDER is an int (TFLAG(TF_INT1)),
2882 // the argument a double.
2883 fn jn(n: i32, x: f64) -> f64;
2884 fn y0(x: f64) -> f64;
2885 fn y1(x: f64) -> f64;
2886 fn yn(n: i32, x: f64) -> f64;
2887 fn cbrt(x: f64) -> f64;
2888 fn expm1(x: f64) -> f64;
2889 fn log1p(x: f64) -> f64;
2890 fn copysign(x: f64, y: f64) -> f64;
2891 fn nextafter(x: f64, y: f64) -> f64;
2892 fn rint(x: f64) -> f64;
2893 fn fmod(x: f64, y: f64) -> f64;
2894 fn ldexp(x: f64, exp: i32) -> f64;
2895 fn scalbn(x: f64, exp: i32) -> f64;
2896 }
2897 // Built-in math functions — mirrors `math_func()` dispatch table
2898 // at Src/Modules/mathfunc.c:198-432.
2899 let result = match name {
2900 "abs" => args.first().map(|x| x.abs()).unwrap_or(0.0),
2901 "acos" => args.first().map(|x| x.acos()).unwrap_or(0.0),
2902 "acosh" => args.first().map(|x| x.acosh()).unwrap_or(0.0), // c:212
2903 "asin" => args.first().map(|x| x.asin()).unwrap_or(0.0),
2904 "asinh" => args.first().map(|x| x.asinh()).unwrap_or(0.0), // c:220
2905 // c:Src/Modules/mathfunc.c:225-229 — `atan` takes 1 OR 2 args:
2906 // the 2-arg form is atan2(y, x) (NUMMATHFUNC("atan", …, 1, 2)).
2907 // The previous port ignored the second arg and returned
2908 // atan(arg1), so `atan(3,2)` gave 1.249 instead of atan2(3,2)
2909 // = 0.98279. (The 3+-arg "wrong number of arguments" error
2910 // requires built-in math-func arity validation — see catalog.)
2911 "atan" => {
2912 if args.len() >= 2 {
2913 args[0].atan2(args[1]) // c:227
2914 } else {
2915 args.first().map(|x| x.atan()).unwrap_or(0.0) // c:229
2916 }
2917 }
2918 "atanh" => args.first().map(|x| x.atanh()).unwrap_or(0.0), // c:233
2919 "cbrt" => unsafe { cbrt(args.first().copied().unwrap_or(0.0)) }, // c:237
2920 "ceil" => args.first().map(|x| x.ceil()).unwrap_or(0.0),
2921 "copysign" => {
2922 let x = args.first().copied().unwrap_or(0.0);
2923 let y = args.get(1).copied().unwrap_or(0.0);
2924 unsafe { copysign(x, y) } // c:245
2925 }
2926 "cos" => args.first().map(|x| x.cos()).unwrap_or(1.0),
2927 "cosh" => args.first().map(|x| x.cosh()).unwrap_or(1.0),
2928 "erf" => unsafe { erf(args.first().copied().unwrap_or(0.0)) }, // c:257
2929 "erfc" => unsafe { erfc(args.first().copied().unwrap_or(0.0)) }, // c:261
2930 "exp" => args.first().map(|x| x.exp()).unwrap_or(1.0),
2931 "expm1" => unsafe { expm1(args.first().copied().unwrap_or(0.0)) }, // c:269
2932 "fabs" => args.first().map(|x| x.abs()).unwrap_or(0.0), // c:273
2933 "floor" => args.first().map(|x| x.floor()).unwrap_or(0.0),
2934 "fmod" => {
2935 let x = args.first().copied().unwrap_or(0.0);
2936 let y = args.get(1).copied().unwrap_or(1.0);
2937 unsafe { fmod(x, y) } // c:285
2938 }
2939 "gamma" => unsafe { tgamma(args.first().copied().unwrap_or(0.0)) }, // c:289
2940 "hypot" => {
2941 let x = args.first().copied().unwrap_or(0.0);
2942 let y = args.get(1).copied().unwrap_or(0.0);
2943 x.hypot(y)
2944 }
2945 "ilogb" => unsafe { ilogb(args.first().copied().unwrap_or(0.0)) as f64 }, // c:304
2946 "int" => args.first().map(|x| x.trunc()).unwrap_or(0.0),
2947 "j0" => unsafe { j0(args.first().copied().unwrap_or(0.0)) }, // c:325
2948 "j1" => unsafe { j1(args.first().copied().unwrap_or(0.0)) }, // c:331
2949 // c:Src/Modules/mathfunc.c:144 `NUMMATHFUNC("jn", math_func, 2, 2,
2950 // MF_JN | TFLAG(TF_INT1))` + c:333-335 `retd = jn(argi, argd2);`.
2951 // TF_INT1 (c:106) means the FIRST argument is the integer one —
2952 // the mirror of ldexp/scalb's TF_INT2 below.
2953 "jn" => {
2954 let n = args.first().copied().unwrap_or(0.0) as i32;
2955 let x = args.get(1).copied().unwrap_or(0.0);
2956 unsafe { jn(n, x) } // c:334
2957 }
2958 "ldexp" => {
2959 // c:Src/Modules/mathfunc.c:337 MF_LDEXP — `ldexp(argd, argi)`,
2960 // 2nd arg coerced to int (TF_INT2). Returns x * 2^n.
2961 let x = args.first().copied().unwrap_or(0.0);
2962 let n = args.get(1).copied().unwrap_or(0.0) as i32;
2963 unsafe { ldexp(x, n) }
2964 }
2965 "scalb" => {
2966 // c:Src/Modules/mathfunc.c:378 MF_SCALB — `scalbn(argd, argi)`.
2967 let x = args.first().copied().unwrap_or(0.0);
2968 let n = args.get(1).copied().unwrap_or(0.0) as i32;
2969 unsafe { scalbn(x, n) }
2970 }
2971 "lgamma" => unsafe { lgamma(args.first().copied().unwrap_or(0.0)) }, // c:341
2972 "log" => args.first().map(|x| x.ln()).unwrap_or(0.0),
2973 "log10" => args.first().map(|x| x.log10()).unwrap_or(0.0),
2974 "log1p" => unsafe { log1p(args.first().copied().unwrap_or(0.0)) }, // c:357
2975 "log2" => args.first().map(|x| x.log2()).unwrap_or(0.0),
2976 "logb" => unsafe { logb(args.first().copied().unwrap_or(0.0)) }, // c:365
2977 "nextafter" => {
2978 let x = args.first().copied().unwrap_or(0.0);
2979 let y = args.get(1).copied().unwrap_or(0.0);
2980 unsafe { nextafter(x, y) } // c:373
2981 }
2982 // c:Src/Modules/mathfunc.c:374 — `retd = rint(argd)` (round to
2983 // nearest, ties to even). Note zsh has NO `round`/`pow`/`rand`
2984 // mathfunc — `**` is the power operator and `round` doesn't exist.
2985 "rint" => unsafe { rint(args.first().copied().unwrap_or(0.0)) },
2986 "sin" => args.first().map(|x| x.sin()).unwrap_or(0.0),
2987 "sinh" => args.first().map(|x| x.sinh()).unwrap_or(0.0),
2988 "sqrt" => args.first().map(|x| x.sqrt()).unwrap_or(0.0),
2989 "tan" => args.first().map(|x| x.tan()).unwrap_or(0.0),
2990 "tanh" => args.first().map(|x| x.tanh()).unwrap_or(0.0),
2991 "y0" => unsafe { y0(args.first().copied().unwrap_or(0.0)) }, // c:417
2992 "y1" => unsafe { y1(args.first().copied().unwrap_or(0.0)) }, // c:423
2993 // c:Src/Modules/mathfunc.c:168 `NUMMATHFUNC("yn", math_func, 2, 2,
2994 // MF_YN | TFLAG(TF_INT1))` + c:420-422 `retd = yn(argi, argd2);`.
2995 "yn" => {
2996 let n = args.first().copied().unwrap_or(0.0) as i32;
2997 let x = args.get(1).copied().unwrap_or(0.0);
2998 unsafe { yn(n, x) } // c:421
2999 }
3000 // `float(x)` — widen int/float to float. Identity on
3001 // floats; on ints, returns same value tagged as float so
3002 // `printf "%.4f"` prints "3.0000" instead of "3". Direct
3003 // port of mathfunc.c's `to_float()`.
3004 "float" => args.first().copied().unwrap_or(0.0),
3005 _ => {
3006 m_error_set(format!("unknown function: {}", name));
3007 0.0
3008 }
3009 };
3010
3011 // c:Src/Modules/mathfunc.c — MF_ILOGB / MF_INT / MF_ISINF / MF_ISNAN
3012 // set `ret.type = MN_INTEGER` (e.g. `ilogb(8)` → 3, not 3.). Tag the
3013 // integer-returning functions so the result prints as an int.
3014 if matches!(name, "ilogb" | "int") {
3015 return mnumber {
3016 l: result as i64,
3017 d: 0.0,
3018 type_: MN_INTEGER,
3019 };
3020 }
3021 mnumber {
3022 l: 0,
3023 d: result,
3024 type_: MN_FLOAT,
3025 }
3026}
3027
3028/// `MS_RAND48` — the only id in the string-mathfunc enum.
3029/// c:Src/Modules/mathfunc.c:90-92 `enum { MS_RAND48 };`.
3030const MS_RAND48: i32 = 0;
3031
3032/// Port of `math_string(name, arg, id)` (Src/Modules/mathfunc.c:438).
3033/// String math functions receive their argument VERBATIM (un-evaluated)
3034/// — for `rand48` the arg is the name of the parameter holding (and
3035/// receiving) the 48-bit seed state as 12 hex digits.
3036fn math_string(_name: &str, arg: &str, id: i32) -> mnumber {
3037 extern "C" {
3038 // c:erand48(xsubi[3]) — next double in [0,1), advances seed in place.
3039 fn erand48(xsubi: *mut u16) -> f64;
3040 fn seed48(seed16v: *mut u16) -> *mut u16;
3041 fn rand() -> i32;
3042 }
3043 let mut ret = mnumber {
3044 l: 0,
3045 d: 0.0,
3046 type_: MN_INTEGER,
3047 }; // c:440 zero_mnumber
3048 // c:446-453 — trim leading/trailing blanks from the verbatim arg.
3049 let arg = arg.trim_matches(|c: char| c == ' ' || c == '\t');
3050 match id {
3051 MS_RAND48 => {
3052 // c:460-461 — `static unsigned short seedbuf[3]; static int
3053 // seedbuf_init;` persist the default (no-arg) seed across calls.
3054 thread_local! {
3055 static SEEDBUF: std::cell::Cell<[u16; 3]> = const { std::cell::Cell::new([0; 3]) };
3056 static SEEDBUF_INIT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3057 }
3058 let mut tmp_seedbuf: [u16; 3] = [0; 3]; // c:462
3059 let use_static; // seedbufptr == seedbuf (the static default)
3060 let mut do_init = true; // c:463
3061 if !arg.is_empty() {
3062 // c:465-494 — seed comes from the named parameter.
3063 use_static = false; // c:467 seedbufptr = tmp_seedbuf
3064 // c:468 — `(seedstr = getsparam(arg)) && strlen(seedstr) >= 12`
3065 if let Some(seedstr) = crate::ported::params::getsparam(arg) {
3066 let sb = seedstr.as_bytes();
3067 if sb.len() >= 12 {
3068 do_init = false; // c:470
3069 let mut p = 0usize; // walks seedstr (c: seedstr++)
3070 // c:474-492 — decode three u16 from 12 hex digits.
3071 for i in 0..3 {
3072 if do_init {
3073 break;
3074 } // c:474 `i < 3 && !do_init`
3075 let mut val: u16 = 0; // c:476 *seedptr = 0
3076 for j in 0..4 {
3077 let b = sb[p];
3078 if b.is_ascii_digit() {
3079 val = val.wrapping_add((b - b'0') as u16); // c:480
3080 } else {
3081 let lc = (b as char).to_ascii_lowercase();
3082 if ('a'..='f').contains(&lc) {
3083 // c:482-483
3084 val = val.wrapping_add((lc as u8 - b'a' + 10) as u16);
3085 } else {
3086 do_init = true; // c:486
3087 break;
3088 }
3089 }
3090 p += 1; // c:489 seedstr++
3091 if j < 3 {
3092 val = val.wrapping_mul(16); // c:491
3093 }
3094 }
3095 tmp_seedbuf[i] = val;
3096 }
3097 }
3098 }
3099 } else {
3100 // c:497-506 — default static seed; initialise once, then
3101 // re-init on every later call (do_init stays/becomes true).
3102 use_static = true; // c:500 seedbufptr = seedbuf
3103 if !SEEDBUF_INIT.with(|c| c.get()) {
3104 SEEDBUF_INIT.with(|c| c.set(true)); // c:503
3105 } else {
3106 do_init = true; // c:505
3107 }
3108 }
3109 // Working seed in a local we can pass by mutable pointer.
3110 let mut seed: [u16; 3] = if use_static {
3111 SEEDBUF.with(|c| c.get())
3112 } else {
3113 tmp_seedbuf
3114 };
3115 if do_init {
3116 // c:507-518 — seed from rand(); seed48 for impls that need it.
3117 seed[0] = unsafe { rand() } as u16;
3118 seed[1] = unsafe { rand() } as u16;
3119 seed[2] = unsafe { rand() } as u16;
3120 unsafe {
3121 seed48(seed.as_mut_ptr());
3122 }
3123 }
3124 ret.type_ = MN_FLOAT; // c:520
3125 ret.d = unsafe { erand48(seed.as_mut_ptr()) }; // c:521
3126 if use_static {
3127 SEEDBUF.with(|c| c.set(seed)); // persist advanced static state
3128 }
3129 if !arg.is_empty() {
3130 // c:523-529 — write the advanced state back as 12 hex digits.
3131 let outbuf = format!("{:04x}{:04x}{:04x}", seed[0], seed[1], seed[2]);
3132 crate::ported::params::setsparam(arg, &outbuf);
3133 }
3134 }
3135 _ => {}
3136 }
3137 ret
3138}
3139
3140/// Port of `op(int what)` from `Src/math.c:1154`.
3141///
3142/// Apply a binary or unary operator to the operand stack. Pops
3143/// 1-2 values, applies the operation (with type coercion), and
3144/// pushes the result. Handles assignment (`OP_E2*` flag) by
3145/// writing through `setmathvar` and pushing the new value back
3146/// with the same lvalue so chained assigns work.
3147/// WARNING: param names don't match C — Rust=() vs C=(what)
3148pub(crate) fn op(what: i32) {
3149 if m_error_some() {
3150 return;
3151 }
3152
3153 let tp = OP_TYPE[what as usize];
3154
3155 // Binary operators
3156 if (tp & (OP_A2 | OP_A2IR | OP_A2IO | OP_E2 | OP_E2IO)) != 0 {
3157 if m_stack_len() < 2 {
3158 // zsh's exact wording for the same condition is
3159 // `bad math expression: operand expected at end of
3160 // string`. Matching it here means `let "1+"` and
3161 // `$((5+))` produce the same diagnostic shape that
3162 // scripts grep for.
3163 m_error_set("bad math expression: operand expected at end of string".to_string());
3164 return;
3165 }
3166
3167 let b = pop();
3168 let mv_a = pop_with_lval();
3169 let a = if (mv_a.val.type_ == MN_UNSET) {
3170 if let Some(ref name) = mv_a.lval {
3171 getmathparam(name)
3172 } else {
3173 mnumber {
3174 l: 0,
3175 d: 0.0,
3176 type_: MN_INTEGER,
3177 }
3178 }
3179 } else {
3180 mv_a.val
3181 };
3182
3183 // Coerce types
3184 let (a, b) = if (tp & (OP_A2IO | OP_E2IO)) != 0 {
3185 // Must be integers
3186 (
3187 mnumber {
3188 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l }),
3189 d: 0.0,
3190 type_: MN_INTEGER,
3191 },
3192 mnumber {
3193 l: (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }),
3194 d: 0.0,
3195 type_: MN_INTEGER,
3196 },
3197 )
3198 } else if (a.type_ == MN_FLOAT) != (b.type_ == MN_FLOAT) && what != COMMA {
3199 // Different types, coerce to float
3200 (
3201 mnumber {
3202 l: 0,
3203 d: (if a.type_ == MN_FLOAT { a.d } else { a.l as f64 }),
3204 type_: MN_FLOAT,
3205 },
3206 mnumber {
3207 l: 0,
3208 d: (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }),
3209 type_: MN_FLOAT,
3210 },
3211 )
3212 } else {
3213 (a, b)
3214 };
3215
3216 let result = if m_noeval() > 0 {
3217 mnumber {
3218 l: 0,
3219 d: 0.0,
3220 type_: MN_INTEGER,
3221 }
3222 } else {
3223 let is_float = (a.type_ == MN_FLOAT);
3224 match what {
3225 AND | ANDEQ => mnumber {
3226 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3227 & (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }),
3228 d: 0.0,
3229 type_: MN_INTEGER,
3230 },
3231 XOR | XOREQ => mnumber {
3232 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3233 ^ (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }),
3234 d: 0.0,
3235 type_: MN_INTEGER,
3236 },
3237 OR | OREQ => mnumber {
3238 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3239 | (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }),
3240 d: 0.0,
3241 type_: MN_INTEGER,
3242 },
3243
3244 MUL | MULEQ => {
3245 if is_float {
3246 mnumber {
3247 l: 0,
3248 d: (if a.type_ == MN_FLOAT { a.d } else { a.l as f64 })
3249 * (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }),
3250 type_: MN_FLOAT,
3251 }
3252 } else {
3253 mnumber {
3254 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3255 .wrapping_mul((if b.type_ == MN_FLOAT { b.d as i64 } else { b.l })),
3256 d: 0.0,
3257 type_: MN_INTEGER,
3258 }
3259 }
3260 }
3261
3262 DIV | DIVEQ => {
3263 // Float div-by-zero is NOT an error in zsh —
3264 // it produces IEEE Inf/-Inf/NaN per IEEE 754.
3265 // Only INTEGER div-by-zero raises the error.
3266 // Without this gate `1/0.0` errored out instead
3267 // of returning `Inf`.
3268 if is_float {
3269 // Let f64 semantics handle 0.0, -0.0, NaN.
3270 mnumber {
3271 l: 0,
3272 d: (if a.type_ == MN_FLOAT { a.d } else { a.l as f64 })
3273 / (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }),
3274 type_: MN_FLOAT,
3275 }
3276 } else {
3277 if !notzero(b) {
3278 m_error_set("division by zero".to_string());
3279 return;
3280 }
3281 let bi = (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l });
3282 if bi == -1 {
3283 mnumber {
3284 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3285 .wrapping_neg(),
3286 d: 0.0,
3287 type_: MN_INTEGER,
3288 }
3289 } else {
3290 mnumber {
3291 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l }) / bi,
3292 d: 0.0,
3293 type_: MN_INTEGER,
3294 }
3295 }
3296 }
3297 }
3298
3299 MOD | MODEQ => {
3300 if is_float {
3301 // float % 0.0 → NaN per IEEE; let it fall
3302 // through to f64 semantics rather than
3303 // raising the integer-only error.
3304 mnumber {
3305 l: 0,
3306 d: (if a.type_ == MN_FLOAT { a.d } else { a.l as f64 })
3307 % (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }),
3308 type_: MN_FLOAT,
3309 }
3310 } else if !notzero(b) {
3311 m_error_set("division by zero".to_string());
3312 return;
3313 } else {
3314 let bi = (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l });
3315 if bi == -1 {
3316 mnumber {
3317 l: 0,
3318 d: 0.0,
3319 type_: MN_INTEGER,
3320 }
3321 } else {
3322 mnumber {
3323 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l }) % bi,
3324 d: 0.0,
3325 type_: MN_INTEGER,
3326 }
3327 }
3328 }
3329 }
3330
3331 PLUS | PLUSEQ => {
3332 if is_float {
3333 mnumber {
3334 l: 0,
3335 d: (if a.type_ == MN_FLOAT { a.d } else { a.l as f64 })
3336 + (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }),
3337 type_: MN_FLOAT,
3338 }
3339 } else {
3340 mnumber {
3341 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3342 .wrapping_add((if b.type_ == MN_FLOAT { b.d as i64 } else { b.l })),
3343 d: 0.0,
3344 type_: MN_INTEGER,
3345 }
3346 }
3347 }
3348
3349 MINUS | MINUSEQ => {
3350 if is_float {
3351 mnumber {
3352 l: 0,
3353 d: (if a.type_ == MN_FLOAT { a.d } else { a.l as f64 })
3354 - (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }),
3355 type_: MN_FLOAT,
3356 }
3357 } else {
3358 mnumber {
3359 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3360 .wrapping_sub((if b.type_ == MN_FLOAT { b.d as i64 } else { b.l })),
3361 d: 0.0,
3362 type_: MN_INTEGER,
3363 }
3364 }
3365 }
3366
3367 SHLEFT | SHLEFTEQ => mnumber {
3368 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3369 << ((if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }) as u32 & 63),
3370 d: 0.0,
3371 type_: MN_INTEGER,
3372 },
3373 SHRIGHT | SHRIGHTEQ => mnumber {
3374 l: (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3375 >> ((if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }) as u32 & 63),
3376 d: 0.0,
3377 type_: MN_INTEGER,
3378 },
3379
3380 LES => mnumber {
3381 l: if is_float {
3382 ((if a.type_ == MN_FLOAT { a.d } else { a.l as f64 })
3383 < (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }))
3384 as i64
3385 } else {
3386 ((if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3387 < (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }))
3388 as i64
3389 },
3390 d: 0.0,
3391 type_: MN_INTEGER,
3392 },
3393 LEQ => mnumber {
3394 l: if is_float {
3395 ((if a.type_ == MN_FLOAT { a.d } else { a.l as f64 })
3396 <= (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }))
3397 as i64
3398 } else {
3399 ((if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3400 <= (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }))
3401 as i64
3402 },
3403 d: 0.0,
3404 type_: MN_INTEGER,
3405 },
3406 GRE => mnumber {
3407 l: if is_float {
3408 ((if a.type_ == MN_FLOAT { a.d } else { a.l as f64 })
3409 > (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }))
3410 as i64
3411 } else {
3412 ((if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3413 > (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }))
3414 as i64
3415 },
3416 d: 0.0,
3417 type_: MN_INTEGER,
3418 },
3419 GEQ => mnumber {
3420 l: if is_float {
3421 ((if a.type_ == MN_FLOAT { a.d } else { a.l as f64 })
3422 >= (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }))
3423 as i64
3424 } else {
3425 ((if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3426 >= (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }))
3427 as i64
3428 },
3429 d: 0.0,
3430 type_: MN_INTEGER,
3431 },
3432 DEQ => mnumber {
3433 l: if is_float {
3434 ((if a.type_ == MN_FLOAT { a.d } else { a.l as f64 })
3435 == (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }))
3436 as i64
3437 } else {
3438 ((if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3439 == (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }))
3440 as i64
3441 },
3442 d: 0.0,
3443 type_: MN_INTEGER,
3444 },
3445 NEQ => mnumber {
3446 l: if is_float {
3447 ((if a.type_ == MN_FLOAT { a.d } else { a.l as f64 })
3448 != (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 }))
3449 as i64
3450 } else {
3451 ((if a.type_ == MN_FLOAT { a.d as i64 } else { a.l })
3452 != (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }))
3453 as i64
3454 },
3455 d: 0.0,
3456 type_: MN_INTEGER,
3457 },
3458
3459 DAND | DANDEQ => mnumber {
3460 l: ((if a.type_ == MN_FLOAT { a.d as i64 } else { a.l }) != 0
3461 && (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }) != 0)
3462 as i64,
3463 d: 0.0,
3464 type_: MN_INTEGER,
3465 },
3466 DOR | DOREQ => mnumber {
3467 l: ((if a.type_ == MN_FLOAT { a.d as i64 } else { a.l }) != 0
3468 || (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }) != 0)
3469 as i64,
3470 d: 0.0,
3471 type_: MN_INTEGER,
3472 },
3473 DXOR | DXOREQ => {
3474 let ai = (if a.type_ == MN_FLOAT { a.d as i64 } else { a.l }) != 0;
3475 let bi = (if b.type_ == MN_FLOAT { b.d as i64 } else { b.l }) != 0;
3476 mnumber {
3477 l: (ai != bi) as i64,
3478 d: 0.0,
3479 type_: MN_INTEGER,
3480 }
3481 }
3482
3483 POWER | POWEREQ => {
3484 // c:1335 — POWER '**'
3485 let mut a = a;
3486 let mut b = b;
3487 let mut cf = is_float; // c.type == MN_FLOAT
3488 // c:1337 — integer base with a negative integer exponent
3489 // "produces a real result, so cast to real." The cast of a
3490 // to float MUST happen before the zero check below: notzero
3491 // never faults on a float zero, so the all-integer
3492 // `0 ** -n` becomes pow(0.0,-n)=Inf rather than an error.
3493 if !cf && b.l < 0 {
3494 a = mnumber { l: 0, d: a.l as f64, type_: MN_FLOAT }; // c:1340
3495 b = mnumber { l: 0, d: b.l as f64, type_: MN_FLOAT }; // c:1341
3496 cf = true; // c:1339 (a.type = b.type = c.type = MN_FLOAT)
3497 }
3498 if !cf {
3499 // c:1344 — for (c.u.l = 1; b.u.l--; c.u.l *= a.u.l).
3500 // zsh's naive O(e) loop times out on a pathological
3501 // exponent (`0 ** 4.6e9` loops billions of times).
3502 // zshrs computes the IDENTICAL value via
3503 // exponentiation-by-squaring in O(log e): multiplication
3504 // mod 2^64 is associative, so the wrapped product is
3505 // bit-identical to the repeated-multiply result for every
3506 // base/exponent (verified against zsh across overflow
3507 // cases). b.l is >= 0 here (negative exponents were cast
3508 // to float above).
3509 let base = a.l;
3510 let mut e = b.l;
3511 let mut result = 1i64;
3512 let mut acc = base;
3513 while e > 0 {
3514 if e & 1 == 1 {
3515 result = result.wrapping_mul(acc);
3516 }
3517 e >>= 1;
3518 if e > 0 {
3519 acc = acc.wrapping_mul(acc);
3520 }
3521 }
3522 mnumber {
3523 l: result,
3524 d: 0.0,
3525 type_: MN_INTEGER,
3526 }
3527 } else {
3528 let af = (if a.type_ == MN_FLOAT { a.d } else { a.l as f64 });
3529 let bf = (if b.type_ == MN_FLOAT { b.d } else { b.l as f64 });
3530 // c:1346 — `if (b.u.d <= 0 && !notzero(a)) return;`
3531 // notzero faults (division by zero) only on an INTEGER
3532 // zero, so a base that was cast to float above slips
3533 // through and yields Inf; a genuine integer-zero base
3534 // (e.g. `0 ** -4.0`, no cast) still errors.
3535 if bf <= 0.0 && !notzero(a) {
3536 m_error_set("division by zero".to_string());
3537 return;
3538 }
3539 // c:1348 — (-num ** b) with non-integer b is imaginary
3540 if af < 0.0 && bf != bf.trunc() {
3541 m_error_set("bad math expression: imaginary power".to_string()); // c:1350
3542 return;
3543 }
3544 mnumber {
3545 l: 0,
3546 d: af.powf(bf), // c:1356
3547 type_: MN_FLOAT,
3548 }
3549 }
3550 }
3551
3552 COMMA => b,
3553 EQ => b,
3554
3555 _ => mnumber {
3556 l: 0,
3557 d: 0.0,
3558 type_: MN_INTEGER,
3559 },
3560 }
3561 };
3562
3563 // Handle assignment
3564 if (tp & (OP_E2 | OP_E2IO)) != 0 {
3565 if let Some(ref name) = mv_a.lval {
3566 let final_val = setmathvar(name, result);
3567 push(final_val, Some(name.clone()));
3568 } else {
3569 m_error_set("lvalue required".to_string());
3570 push(
3571 mnumber {
3572 l: 0,
3573 d: 0.0,
3574 type_: MN_INTEGER,
3575 },
3576 None,
3577 );
3578 }
3579 } else {
3580 push(result, None);
3581 }
3582 return;
3583 }
3584
3585 // Unary operators
3586 if m_stack_is_empty() {
3587 // zsh: unary op with empty stack -> `bad math
3588 // expression: operand expected at end of string`.
3589 // zshrs's bare `stack empty` had no match for scripts
3590 // grepping zsh's canonical wording.
3591 m_error_set("bad math expression: operand expected at end of string".to_string());
3592 return;
3593 }
3594
3595 let mv = pop_with_lval();
3596 let val = if (mv.val.type_ == MN_UNSET) {
3597 if let Some(ref name) = mv.lval {
3598 getmathparam(name)
3599 } else {
3600 mnumber {
3601 l: 0,
3602 d: 0.0,
3603 type_: MN_INTEGER,
3604 }
3605 }
3606 } else {
3607 mv.val
3608 };
3609
3610 match what {
3611 NOT => {
3612 let result = mnumber {
3613 l: if ((val.type_ == MN_INTEGER && val.l == 0)
3614 || (val.type_ == MN_FLOAT && val.d == 0.0)
3615 || val.type_ == MN_UNSET)
3616 {
3617 1
3618 } else {
3619 0
3620 },
3621 d: 0.0,
3622 type_: MN_INTEGER,
3623 };
3624 push(result, None);
3625 }
3626 COMP => {
3627 let result = mnumber {
3628 l: !(if val.type_ == MN_FLOAT {
3629 val.d as i64
3630 } else {
3631 val.l
3632 }),
3633 d: 0.0,
3634 type_: MN_INTEGER,
3635 };
3636 push(result, None);
3637 }
3638 UPLUS => {
3639 push(val, None);
3640 }
3641 UMINUS => {
3642 let result = if (val.type_ == MN_FLOAT) {
3643 mnumber {
3644 l: 0,
3645 d: -(if val.type_ == MN_FLOAT {
3646 val.d
3647 } else {
3648 val.l as f64
3649 }),
3650 type_: MN_FLOAT,
3651 }
3652 } else {
3653 // c:Src/math.c UMINUS — negating INT_MIN is UB in
3654 // C but in two's complement wraps to INT_MIN. zsh
3655 // prints \`-9223372036854775808\` for \`\$((-(2**63)))\`.
3656 // Rust's plain unary `-` panics in debug builds on
3657 // i64::MIN, so use wrapping_neg.
3658 let v = if val.type_ == MN_FLOAT {
3659 val.d as i64
3660 } else {
3661 val.l
3662 };
3663 mnumber {
3664 l: v.wrapping_neg(),
3665 d: 0.0,
3666 type_: MN_INTEGER,
3667 }
3668 };
3669 push(result, None);
3670 }
3671 POSTPLUS => {
3672 // ++/-- on a literal (`5++`, `--5`) is a zsh error:
3673 // "bad math expression: lvalue required". Without the
3674 // mv.lval guard, zshrs silently incremented the
3675 // literal value and returned it, masking the bug.
3676 if mv.lval.is_none() {
3677 m_error_set("bad math expression: lvalue required".to_string());
3678 return;
3679 }
3680 let name = mv.lval.as_ref().unwrap();
3681 let new_val = if (val.type_ == MN_FLOAT) {
3682 mnumber {
3683 l: 0,
3684 d: (if val.type_ == MN_FLOAT {
3685 val.d
3686 } else {
3687 val.l as f64
3688 }) + 1.0,
3689 type_: MN_FLOAT,
3690 }
3691 } else {
3692 mnumber {
3693 l: (if val.type_ == MN_FLOAT {
3694 val.d as i64
3695 } else {
3696 val.l
3697 }) + 1,
3698 d: 0.0,
3699 type_: MN_INTEGER,
3700 }
3701 };
3702 setmathvar(name, new_val);
3703 push(val, None); // Return original value
3704 }
3705 POSTMINUS => {
3706 if mv.lval.is_none() {
3707 m_error_set("bad math expression: lvalue required".to_string());
3708 return;
3709 }
3710 let name = mv.lval.as_ref().unwrap();
3711 let new_val = if (val.type_ == MN_FLOAT) {
3712 mnumber {
3713 l: 0,
3714 d: (if val.type_ == MN_FLOAT {
3715 val.d
3716 } else {
3717 val.l as f64
3718 }) - 1.0,
3719 type_: MN_FLOAT,
3720 }
3721 } else {
3722 mnumber {
3723 l: (if val.type_ == MN_FLOAT {
3724 val.d as i64
3725 } else {
3726 val.l
3727 }) - 1,
3728 d: 0.0,
3729 type_: MN_INTEGER,
3730 }
3731 };
3732 setmathvar(name, new_val);
3733 push(val, None);
3734 }
3735 PREPLUS => {
3736 if mv.lval.is_none() {
3737 m_error_set("bad math expression: lvalue required".to_string());
3738 return;
3739 }
3740 let name = mv.lval.as_ref().unwrap();
3741 let new_val = if (val.type_ == MN_FLOAT) {
3742 mnumber {
3743 l: 0,
3744 d: (if val.type_ == MN_FLOAT {
3745 val.d
3746 } else {
3747 val.l as f64
3748 }) + 1.0,
3749 type_: MN_FLOAT,
3750 }
3751 } else {
3752 mnumber {
3753 l: (if val.type_ == MN_FLOAT {
3754 val.d as i64
3755 } else {
3756 val.l
3757 }) + 1,
3758 d: 0.0,
3759 type_: MN_INTEGER,
3760 }
3761 };
3762 setmathvar(name, new_val);
3763 push(new_val, mv.lval);
3764 }
3765 PREMINUS => {
3766 if mv.lval.is_none() {
3767 m_error_set("bad math expression: lvalue required".to_string());
3768 return;
3769 }
3770 let name = mv.lval.as_ref().unwrap();
3771 let new_val = if (val.type_ == MN_FLOAT) {
3772 mnumber {
3773 l: 0,
3774 d: (if val.type_ == MN_FLOAT {
3775 val.d
3776 } else {
3777 val.l as f64
3778 }) - 1.0,
3779 type_: MN_FLOAT,
3780 }
3781 } else {
3782 mnumber {
3783 l: (if val.type_ == MN_FLOAT {
3784 val.d as i64
3785 } else {
3786 val.l
3787 }) - 1,
3788 d: 0.0,
3789 type_: MN_INTEGER,
3790 }
3791 };
3792 setmathvar(name, new_val);
3793 push(new_val, mv.lval);
3794 }
3795 QUEST => {
3796 // Ternary: stack has [cond, true_val, false_val]
3797 // val already popped = false_val
3798 // Need to pop true_val and cond
3799 if m_stack_len() < 2 {
3800 m_error_set("?: needs 3 operands".to_string());
3801 return;
3802 }
3803 let false_val = val;
3804 let true_val = pop();
3805 let cond = pop();
3806 let result = if !((cond.type_ == MN_INTEGER && cond.l == 0)
3807 || (cond.type_ == MN_FLOAT && cond.d == 0.0)
3808 || cond.type_ == MN_UNSET)
3809 {
3810 true_val
3811 } else {
3812 false_val
3813 };
3814 push(result, None);
3815 }
3816 COLON => {
3817 m_error_set("bad math expression: ':' without '?'".to_string()); // c:1427
3818 }
3819 _ => {
3820 m_error_set("unknown operator".to_string());
3821 }
3822 }
3823}
3824
3825/// Port of `bop(int tk)` from `Src/math.c:1454`.
3826///
3827/// Short-circuit boolean prologue. Inspects (without popping) the
3828/// top of stack and bumps `m_noeval()` for the parse-only side of
3829/// `&&` / `||` / their assignment forms. The matching decrement
3830/// happens after `mathparse` recurses for the RHS.
3831/// WARNING: param names don't match C — Rust=() vs C=(tk)
3832pub(crate) fn bop(tk: i32) {
3833 if m_stack_is_empty() {
3834 return;
3835 }
3836 let mv = m_stack_top_clone().unwrap();
3837 let val = if (mv.val.type_ == MN_UNSET) {
3838 if let Some(ref name) = mv.lval {
3839 getmathparam(name)
3840 } else {
3841 mnumber {
3842 l: 0,
3843 d: 0.0,
3844 type_: MN_INTEGER,
3845 }
3846 }
3847 } else {
3848 mv.val
3849 };
3850
3851 // c:Src/math.c:1461 — `tst = (spval->type & MN_FLOAT) ? (zlong)spval->u.d
3852 // : spval->u.l;`. A FLOAT operand is TRUNCATED to integer for the
3853 // short-circuit truth test (`(zlong)0.5` == 0 → falsy), NOT compared
3854 // against 0.0. zsh's `&&`/`||` therefore treat a fractional float like
3855 // 0.5 as false. The prior `val.d == 0.0` test made 0.5 spuriously TRUE,
3856 // so `0.5 || (2+3)` short-circuited on the truthy 0.5 and set noeval for
3857 // the RHS; a COMPOUND RHS under noeval collapses to a dummy 0, and the
3858 // `||` operator then combined truncated-0 with that 0 → wrong result 0
3859 // (zsh evaluates the RHS and yields 1). A bare-literal RHS masked the
3860 // bug because its value survives noeval.
3861 let tst = if val.type_ & MN_FLOAT != 0 {
3862 (val.d as i64) != 0
3863 } else {
3864 val.l != 0
3865 };
3866 match tk {
3867 DAND | DANDEQ if !tst => {
3868 m_noeval_inc();
3869 }
3870 DOR | DOREQ if tst => {
3871 m_noeval_inc();
3872 }
3873 _ => {}
3874 }
3875}
3876
3877/// Port of `mnumber matheval(char *s)` from `Src/math.c:1480`.
3878///
3879/// C body (c:1481-1500):
3880/// ```c
3881/// char *junk;
3882/// mnumber x;
3883/// int xmtok = mtok;
3884/// /* maintain outputradix and outputunderscore across levels of evaluation */
3885/// if (!mlevel)
3886/// outputradix = outputunderscore = 0;
3887///
3888/// if (*s == Nularg)
3889/// s++;
3890/// if (!*s) {
3891/// x.type = MN_INTEGER;
3892/// x.u.l = 0;
3893/// return x;
3894/// }
3895/// x = mathevall(s, MPREC_TOP, &junk);
3896/// mtok = xmtok;
3897/// if (*junk)
3898/// zerr("bad math expression: illegal character: %c", *junk);
3899/// return x;
3900/// ```
3901///
3902/// Three divergences in the previous Rust port:
3903/// 1. Missing Nularg-byte skip at c:1489-1490 — `$(())` lexes
3904/// with a leading Nularg sentinel; without the skip, the math
3905/// evaluator chokes on the 0xa1 byte instead of evaluating
3906/// the empty expression as 0.
3907/// 2. Missing empty-input fast path at c:1491-1495 — empty
3908/// string returned MN_INTEGER 0 in C; Rust port tried to
3909/// evaluate via mathevall and produced a parse error.
3910/// 3. Missing mtok save/restore around mathevall (c:1483, c:1496) —
3911/// recursive math calls (e.g. `$((f($((g)))))`) overwrote the
3912/// outer call's mtok mid-parse.
3913pub fn matheval(s: &str) -> Result<mnumber, String> {
3914 // c:1480
3915 // c:1483 — `int xmtok = mtok;` save.
3916 let xmtok = M_MTOK.with(|c| c.get()); // c:1483
3917
3918 // c:1489-1490 — `if (*s == Nularg) s++;`. The 0xa1 sentinel byte
3919 // can prefix expressions emerging from the parser; skip it.
3920 let s = if let Some(rest) = s.strip_prefix(Nularg) {
3921 // c:1489
3922 rest
3923 } else {
3924 s
3925 };
3926 // c:1486-1487 — `if (!mlevel) outputradix = outputunderscore = 0;`
3927 //
3928 // Only a TOP-LEVEL evaluation starts from a clean radix. When `mlevel` is
3929 // already nonzero this `matheval` is running underneath an in-flight one
3930 // (a `functions -M` math function whose body runs `(( … ))`), and C leaves
3931 // the radix alone so the outer `[#16]` still governs the printed result.
3932 if M_LEVEL.with(|c| c.get()) == 0 {
3933 // c:1486
3934 reset_output_format(); // c:1487
3935 }
3936
3937 // c:1491-1495 — empty expression returns MN_INTEGER 0.
3938 if s.is_empty() {
3939 // c:1491
3940 return Ok(mnumber {
3941 l: 0,
3942 d: 0.0,
3943 type_: MN_INTEGER,
3944 }); // c:1493-1494
3945 }
3946 new(s);
3947 let result = mathevall();
3948 // c:1496 — `mtok = xmtok;` restore. Done even on error path.
3949 M_MTOK.with(|c| c.set(xmtok)); // c:1496
3950 // c:Src/math.c:1500 — `lastmathval = z;` records the result of this
3951 // top-level eval so callmathfunc's MFF_USERFUNC branch can return it.
3952 if let Ok(ref n) = result {
3953 M_LASTMATHVAL.with(|c| c.set(*n));
3954 }
3955 result
3956}
3957
3958/// Port of `mnumber matheval(char *s)` integer-coerce front-end
3959/// `mod_export zlong mathevali(char *s)` from Src/math.c:1505.
3960///
3961/// C body (c:1505-1509):
3962/// ```c
3963/// mnumber x = matheval(s);
3964/// return (x.type & MN_FLOAT) ? (zlong)x.u.d : x.u.l;
3965/// ```
3966///
3967/// Uses bitwise AND against MN_FLOAT — `x.type` is a bitfield holding
3968/// MN_INTEGER (1), MN_FLOAT (2), MN_UNSET (4). The previous Rust port
3969/// did `n.type_ == MN_FLOAT` (strict equality) — which misclassifies
3970/// any result where MN_FLOAT is set alongside another flag (e.g. an
3971/// uninitialized-then-set result might carry MN_FLOAT|MN_UNSET = 6).
3972pub fn mathevali(s: &str) -> Result<i64, String> {
3973 // c:1505
3974 matheval(s).map(|n| // c:1506
3975 if (n.type_ & MN_FLOAT) != 0 { n.d as i64 } else { n.l }) // c:1508
3976}
3977
3978/// Variant of `mathevali` that runs in NOEVAL mode — parses and
3979/// type-checks but does NOT execute side effects (assignments to
3980/// paramtab via setmathvar's c:1002-1003 noeval gate). Used by the
3981/// compile-time pre-check at compile_zsh.rs to validate `(( expr ))`
3982/// without polluting the param table. Bug #617.
3983pub fn mathevali_noeval(s: &str) -> Result<i64, String> {
3984 // new() inside matheval resets noeval to 0; we work around that
3985 // by intercepting at matheval's entry. Run matheval, but bump
3986 // noeval AFTER new() has reset it — by hooking via the mathevall
3987 // path with noeval pre-set wouldn't work because new() also resets.
3988 //
3989 // Solution: replicate matheval's setup but set noeval manually
3990 // before mathevall. mathevall itself respects noeval inside the
3991 // op() dispatch — setmathvar checks at c:1002.
3992 let xmtok = M_MTOK.with(|c| c.get());
3993 let s_skip = if let Some(rest) = s.strip_prefix(Nularg) {
3994 rest
3995 } else {
3996 s
3997 };
3998 if s_skip.is_empty() {
3999 return Ok(0);
4000 }
4001 new(s_skip);
4002 m_noeval_set(1); // bump AFTER new() reset
4003 let result = mathevall();
4004 m_noeval_set(0);
4005 M_MTOK.with(|c| c.set(xmtok));
4006 result.map(|n| {
4007 if (n.type_ & MN_FLOAT) != 0 {
4008 n.d as i64
4009 } else {
4010 n.l
4011 }
4012 })
4013}
4014
4015/// Port of `zlong mathevalarg(char *s, char **ss)` from `Src/math.c:1514-1539`.
4016///
4017/// C body (c:1517-1538):
4018/// ```c
4019/// mnumber x;
4020/// int xmtok = mtok;
4021/// /* At this entry point we don't allow an empty expression,
4022/// * whereas we do with matheval(). */
4023/// if (*s == Nularg)
4024/// s++;
4025/// if (!*s) {
4026/// zerr("bad math expression: empty string");
4027/// return (zlong)0;
4028/// }
4029/// x = mathevall(s, MPREC_ARG, ss);
4030/// if (mtok == COMMA)
4031/// (*ss)--;
4032/// mtok = xmtok;
4033/// return (x.type & MN_FLOAT) ? (zlong)x.u.d : x.u.l;
4034/// ```
4035///
4036/// Two key differences from `matheval`:
4037/// 1. Empty input is an ERROR (zerr + return 0), NOT silent 0.
4038/// C's comment: `$array[$ind]` where `$ind` is unset should
4039/// produce an error, not silently index 0.
4040/// 2. Uses `MPREC_ARG` precedence so the parser stops at the
4041/// end-of-arg boundary (comma, close-paren) rather than
4042/// consuming everything as one top-level expression.
4043/// (Rust mathevall doesn't yet thread the prec_tp arg;
4044/// flagged for follow-up.)
4045pub(crate) fn mathevalarg(expr: &str) -> i64 {
4046 // c:1514
4047 // c:1517 — `int xmtok = mtok;` save.
4048 let xmtok = M_MTOK.with(|c| c.get()); // c:1517
4049 // c:1528-1529 — `if (*s == Nularg) s++;`. Skip the parser sentinel.
4050 let s = if let Some(rest) = expr.strip_prefix(Nularg) {
4051 // c:1528
4052 rest
4053 } else {
4054 expr
4055 };
4056 // c:1530-1532 — empty after Nularg-skip is a HARD error here.
4057 if s.is_empty() {
4058 // c:1530
4059 zerr("bad math expression: empty string"); // c:1531
4060 return 0; // c:1532
4061 }
4062 // c:1534 — `mathevall(s, MPREC_ARG, ss)`. The Rust port doesn't yet
4063 // thread the prec_tp arg through mathevall (uses C_PREC/Z_PREC toggle
4064 // only); structural follow-up.
4065 // c:1538 — `(x.type & MN_FLOAT) ? (zlong)x.u.d : x.u.l`. Bitwise
4066 // check against MN_FLOAT; strict equality `== MN_FLOAT` misclassifies
4067 // composite type bitfields (e.g. MN_FLOAT|MN_UNSET).
4068 let result = matheval(s).map(|n| // c:1538
4069 if (n.type_ & MN_FLOAT) != 0 { n.d as i64 } else { n.l }
4070 ).unwrap_or(0);
4071 // c:1537 — `mtok = xmtok;` restore.
4072 M_MTOK.with(|c| c.set(xmtok)); // c:1537
4073 result
4074}
4075
4076/// Port of `checkunary(int mtokc, char *mptr)` from `Src/math.c:1548`.
4077///
4078/// Two roles. (1) Validate that the just-lexed token (`m_mtok()`)
4079/// matches the parser's expectation: an operand was wanted but an
4080/// operator (`OP_*` flags) showed up, or vice versa. Mismatch
4081/// emits zsh's `bad math expression: <kind> expected at <ctx>`
4082/// with `<kind>` being `operator` or `operand` and `<ctx>` taken
4083/// from the input pointer at the start of the bad token. (2)
4084/// Update `m_unary()` for the next iteration based on `OP_OPF`.
4085/// WARNING: param names don't match C — Rust=() vs C=(mtokc, mptr)
4086pub(crate) fn checkunary() {
4087 // Direct port of zsh math.c checkunary() (line 1548).
4088 // Two roles:
4089 // 1. Validate that the just-lexed token (`m_mtok()`)
4090 // matches the parser's expectation (operator vs
4091 // operand). Mismatch emits zsh's
4092 // "bad math expression: <kind> expected at <ctx>"
4093 // with `<kind>` = `operator` (errmsg=2) or `operand`
4094 // (errmsg=1). zshrs previously only did step 2,
4095 // which left e.g. `let "5 5"` and `$((2#1011x))`
4096 // silently accepting bogus input.
4097 // 2. Update `m_unary()` for the next iteration.
4098 let tp = OP_TYPE[m_mtok() as usize];
4099 let is_op_token = (tp & (OP_A2 | OP_A2IR | OP_A2IO | OP_E2 | OP_E2IO | OP_OP)) != 0;
4100 let errmsg = if is_op_token {
4101 if m_unary() {
4102 1
4103 } else {
4104 0
4105 }
4106 } else if !m_unary() {
4107 2
4108 } else {
4109 0
4110 };
4111 if errmsg != 0 && !m_error_some() {
4112 let errtype = if errmsg == 2 { "operator" } else { "operand" };
4113 // zsh's `mptr` is the input position BEFORE zzlex
4114 // consumed the bad token. We track the same via
4115 // `tok_start` which zzlex updates after whitespace
4116 // skip. Walk forward past whitespace (mirrors zsh's
4117 // `inblank` skip) so the error context starts at
4118 // the first visible char.
4119 let input_owned = m_input_clone();
4120 let bytes = input_owned.as_bytes();
4121 let mut start = m_tok_start();
4122 while start < bytes.len() && matches!(bytes[start], b' ' | b'\t' | b'\n') {
4123 start += 1;
4124 }
4125 // zsh truncates after 10 chars and appends `...` if
4126 // there's more remaining (the over flag in the C
4127 // source). Mirror that to keep error messages
4128 // bounded for long bogus expressions.
4129 let remaining = m_input_slice_from(start);
4130 let (ctx, over) = if remaining.chars().count() > 10 {
4131 let truncated: String = remaining.chars().take(10).collect();
4132 (truncated, true)
4133 } else {
4134 (remaining.to_string(), false)
4135 };
4136 if ctx.is_empty() {
4137 m_error_set(format!(
4138 "bad math expression: {} expected at end of string",
4139 errtype
4140 ));
4141 } else {
4142 m_error_set(format!(
4143 "bad math expression: {} expected at `{}{}'",
4144 errtype,
4145 ctx,
4146 if over { "..." } else { "" }
4147 ));
4148 }
4149 }
4150 m_unary_set((tp & OP_OPF) == 0);
4151}
4152
4153/// Operator-precedence parser - closely follows zsh math.c mathparse()
4154/// Port of `mathparse(int pc)` from `Src/math.c:1594`.
4155/// WARNING: param names don't match C — Rust=() vs C=(pc)
4156pub(crate) fn mathparse(pc: u8) {
4157 if m_error_some() {
4158 return;
4159 }
4160
4161 m_mtok_set(zzlex());
4162
4163 // Handle empty input
4164 if pc == top_prec() && m_mtok() == EOI {
4165 return;
4166 }
4167
4168 checkunary();
4169
4170 while m_prec()[m_mtok() as usize] <= pc {
4171 if m_error_some() {
4172 return;
4173 }
4174
4175 match m_mtok() {
4176 NUM => {
4177 push(m_yyval(), None);
4178 }
4179 ID => {
4180 let lval = m_yylval_clone();
4181 if m_noeval() > 0 {
4182 push(
4183 mnumber {
4184 l: 0,
4185 d: 0.0,
4186 type_: MN_INTEGER,
4187 },
4188 Some(lval),
4189 );
4190 } else {
4191 push(
4192 mnumber {
4193 l: 0,
4194 d: 0.0,
4195 type_: MN_UNSET,
4196 },
4197 Some(lval),
4198 );
4199 }
4200 }
4201 CID => {
4202 let lval = m_yylval_clone();
4203 let val = if m_noeval() > 0 {
4204 mnumber {
4205 l: 0,
4206 d: 0.0,
4207 type_: MN_INTEGER,
4208 }
4209 } else {
4210 getcvar(&lval)
4211 };
4212 push(val, Some(lval));
4213 }
4214 FUNC => {
4215 let func_call = m_yylval_clone();
4216 let val = if m_noeval() > 0 {
4217 mnumber {
4218 l: 0,
4219 d: 0.0,
4220 type_: MN_INTEGER,
4221 }
4222 } else {
4223 callmathfunc(&func_call)
4224 };
4225 push(val, None);
4226 }
4227 M_INPAR => {
4228 mathparse(top_prec());
4229 if m_mtok() != M_OUTPAR {
4230 if !m_error_some() {
4231 // Match zsh's `bad math expression: ')'
4232 // expected` so error diagnostics align.
4233 m_error_set("bad math expression: ')' expected".to_string());
4234 }
4235 return;
4236 }
4237 }
4238 QUEST => {
4239 // Ternary operator
4240 if m_stack_is_empty() {
4241 m_error_set("bad math expression".to_string());
4242 return;
4243 }
4244 let mv = m_stack_top_clone().unwrap();
4245 let cond = get_value(&mv);
4246
4247 let q = !((cond.type_ == MN_INTEGER && cond.l == 0)
4248 || (cond.type_ == MN_FLOAT && cond.d == 0.0)
4249 || cond.type_ == MN_UNSET);
4250 if !q {
4251 m_noeval_inc();
4252 }
4253 let colon_prec = m_prec()[COLON as usize];
4254 let stack_before = m_stack_len();
4255 mathparse(colon_prec - 1);
4256 if !q {
4257 m_noeval_dec();
4258 }
4259
4260 if m_mtok() != COLON {
4261 if !m_error_some() {
4262 // Distinguish whether the inner parse
4263 // produced an operand: stack grew →
4264 // colon expected; stack same → operand
4265 // missing (input ran out at end of
4266 // string after `?`).
4267 if m_stack_len() > stack_before {
4268 m_error_set("bad math expression: ':' expected".to_string());
4269 } else {
4270 m_error_set(
4271 "bad math expression: operand expected at end of string"
4272 .to_string(),
4273 );
4274 }
4275 }
4276 return;
4277 }
4278
4279 if q {
4280 m_noeval_inc();
4281 }
4282 let quest_prec = m_prec()[QUEST as usize];
4283 mathparse(quest_prec);
4284 if q {
4285 m_noeval_dec();
4286 }
4287
4288 op(QUEST);
4289 continue;
4290 }
4291 _ => {
4292 // Binary/unary operator
4293 let otok = m_mtok();
4294 let onoeval = m_noeval();
4295 let tp = OP_TYPE[otok as usize];
4296 // Orphan binary at start: `let "*"`, `let "*5"`,
4297 // `let "/"`. zsh keeps its input pointer at the
4298 // start of the bad operator and emits `operand
4299 // expected at \`<remaining>'`. zshrs previously
4300 // collapsed every operand-missing case into "at
4301 // end of string" which lost the operator
4302 // location for orphan-at-start expressions.
4303 let is_binary = (tp & (OP_A2 | OP_A2IR | OP_A2IO | OP_E2 | OP_E2IO)) != 0;
4304 if m_stack_is_empty() && is_binary {
4305 let remaining = m_input_slice_from(m_tok_start());
4306 m_error_set(format!(
4307 "bad math expression: operand expected at `{}'",
4308 remaining
4309 ));
4310 return;
4311 }
4312 if (tp & 0x03) == BOOL {
4313 bop(otok);
4314 }
4315 let otok_prec = m_prec()[otok as usize];
4316 // Right-to-left gets same prec, left-to-right gets prec-1
4317 let adjust = if (tp & 0x01) != RL { 1 } else { 0 };
4318 mathparse(otok_prec - adjust);
4319 m_noeval_set(onoeval);
4320 op(otok);
4321 continue;
4322 }
4323 }
4324
4325 // After operand (Num, Id, Func, InPar), get next token
4326 m_mtok_set(zzlex());
4327 checkunary();
4328 }
4329}
4330/// Zsh precedence table (default)
4331static Z_PREC: [u8; TOKCOUNT] = [
4332 1, 137, 2, 2, 2, // InPar OutPar Not Comp PostPlus
4333 2, 2, 2, 4, 5, // PostMinus UPlus UMinus And Xor
4334 6, 8, 8, 8, 9, // Or Mul Div Mod Plus
4335 9, 3, 3, 10, 10, // Minus ShLeft ShRight Les Leq
4336 10, 10, 11, 11, 12, // Gre Geq Deq Neq DAnd
4337 13, 13, 14, 15, 16, // DOr DXor Quest Colon Eq
4338 16, 16, 16, 16, 16, // PlusEq MinusEq MulEq DivEq ModEq
4339 16, 16, 16, 16, 16, // AndEq XorEq OrEq ShLeftEq ShRightEq
4340 16, 16, 16, 17, 200, // DAndEq DOrEq DXorEq Comma Eoi
4341 2, 2, 0, 0, 7, // PrePlus PreMinus Num Id Power
4342 0, 16, 0, // CId PowerEq Func
4343];
4344
4345/// C precedence table (used with C_PRECEDENCES option)
4346static C_PREC: [u8; TOKCOUNT] = [
4347 1, 137, 2, 2, 2, 2, 2, 2, 9, 10, 11, 4, 4, 4, 5, 5, 6, 6, 7, 7, 7, 7, 8, 8, 12, 14, 13, 15, 16,
4348 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 200, 2, 2, 0, 0, 3, 0, 17, 0,
4349];
4350
4351/// Operator type table (matches C math.c type[] array)
4352static OP_TYPE: [u16; TOKCOUNT] = [
4353 // InPar, OutPar, Not, Comp, PostPlus
4354 LR,
4355 LR | OP_OP | OP_OPF,
4356 RL,
4357 RL,
4358 RL | OP_OP | OP_OPF,
4359 // PostMinus, UPlus, UMinus, And, Xor
4360 RL | OP_OP | OP_OPF,
4361 RL,
4362 RL,
4363 LR | OP_A2IO,
4364 LR | OP_A2IO,
4365 // Or, Mul, Div, Mod, Plus
4366 LR | OP_A2IO,
4367 LR | OP_A2,
4368 LR | OP_A2,
4369 LR | OP_A2,
4370 LR | OP_A2,
4371 // Minus, ShLeft, ShRight, Les, Leq
4372 LR | OP_A2,
4373 LR | OP_A2IO,
4374 LR | OP_A2IO,
4375 LR | OP_A2IR,
4376 LR | OP_A2IR,
4377 // Gre, Geq, Deq, Neq, DAnd
4378 LR | OP_A2IR,
4379 LR | OP_A2IR,
4380 LR | OP_A2IR,
4381 LR | OP_A2IR,
4382 BOOL | OP_A2IO,
4383 // DOr, DXor, Quest, Colon, Eq
4384 BOOL | OP_A2IO,
4385 LR | OP_A2IO,
4386 RL | OP_OP,
4387 RL | OP_OP,
4388 RL | OP_E2,
4389 // PlusEq, MinusEq, MulEq, DivEq, ModEq
4390 RL | OP_E2,
4391 RL | OP_E2,
4392 RL | OP_E2,
4393 RL | OP_E2,
4394 RL | OP_E2,
4395 // AndEq, XorEq, OrEq, ShLeftEq, ShRightEq
4396 RL | OP_E2IO,
4397 RL | OP_E2IO,
4398 RL | OP_E2IO,
4399 RL | OP_E2IO,
4400 RL | OP_E2IO,
4401 // DAndEq, DOrEq, DXorEq, Comma, Eoi
4402 BOOL | OP_E2IO,
4403 BOOL | OP_E2IO,
4404 RL | OP_A2IO,
4405 RL | OP_A2,
4406 RL | OP_OP,
4407 // PrePlus, PreMinus, Num, Id, Power
4408 RL,
4409 RL,
4410 LR | OP_OPF,
4411 LR | OP_OPF,
4412 RL | OP_A2,
4413 // CId, PowerEq, Func
4414 LR | OP_OPF,
4415 RL | OP_E2,
4416 LR | OP_OPF,
4417];
4418
4419// WARNING: NOT IN MATH.C — Rust-only helper. See save_state above.
4420fn restore_state(saved: xyy_locals) {
4421 m_input_set(saved.input);
4422 m_pos_set(saved.pos);
4423 m_tok_start_set(saved.tok_start);
4424 m_yyval_set(saved.yyval);
4425 m_yylval_set(saved.yylval);
4426 M_STACK.with(|c| *c.borrow_mut() = saved.stack);
4427 m_mtok_set(saved.mtok);
4428 m_unary_set(saved.unary);
4429 m_noeval_set(saved.noeval);
4430 M_ERROR.with(|c| *c.borrow_mut() = saved.error);
4431 m_variables_set(saved.variables);
4432 m_string_variables_set(saved.string_variables);
4433 m_prec_set(saved.prec);
4434 m_c_precedences_set(saved.c_precedences);
4435 m_force_float_set(saved.force_float);
4436 m_octal_zeroes_set(saved.octal_zeroes);
4437 M_LASTBASE.with(|c| c.set(saved.lastbase));
4438}
4439
4440// MathState struct DELETED — state now lives in M_* thread_locals
4441// (matching C math.c's module statics + mathevall's xyy* save/restore).
4442
4443// WARNING: NOT IN MATH.C — Rust-only initializer. C `mathevall()`
4444// (math.c:367) takes the input as a parameter and seeds the module
4445// statics inline at function entry; Rust port factors that seeding
4446// out so call sites can chain `with_*` setters before invoking
4447// `mathevall()`.
4448/// Initialize thread_local math state from a fresh input string.
4449/// Mirrors the entry-side state setup in C `mathevall()` (math.c:367).
4450pub(crate) fn new(input: &str) {
4451 m_input_set(input.to_string());
4452 m_pos_set(0);
4453 m_tok_start_set(0);
4454 m_yyval_set(mnumber {
4455 l: 0,
4456 d: 0.0,
4457 type_: MN_INTEGER,
4458 });
4459 m_yylval_set(String::new());
4460 M_STACK.with(|c| {
4461 c.borrow_mut().clear();
4462 });
4463 m_mtok_set(EOI);
4464 m_unary_set(true);
4465 m_noeval_set(0);
4466 m_lastbase_set(-1);
4467 m_prec_set(&Z_PREC);
4468 m_c_precedences_set(false);
4469 m_force_float_set(false);
4470 m_octal_zeroes_set(false);
4471 m_variables_set(HashMap::new());
4472 m_string_variables_set(HashMap::new());
4473 m_lastval_set(0);
4474 m_pid_set(std::process::id() as i64);
4475 m_error_clear();
4476}
4477
4478// WARNING: NOT IN MATH.C — Rust-only setter. zsh C reads parameters
4479// directly from the global param table on demand; the Rust port
4480// caller seeds an in-memory map up front via this fn.
4481pub(crate) fn with_variables(vars: HashMap<String, mnumber>) {
4482 m_variables_set(vars);
4483}
4484
4485// WARNING: NOT IN MATH.C — Rust-only setter. Parses each value as
4486// numeric → `mnumber` if possible, otherwise stores the raw string
4487// for `getmathparam`'s recursive-eval path (e.g. `a="3+2"; $((a))`).
4488/// Inject variables from string->string mapping (for shell integration)
4489pub(crate) fn with_string_variables(vars: &HashMap<String, String>) {
4490 for (k, v) in vars {
4491 if let Ok(i) = v.parse::<i64>() {
4492 m_variables_insert(
4493 k.clone(),
4494 mnumber {
4495 l: i,
4496 d: 0.0,
4497 type_: MN_INTEGER,
4498 },
4499 );
4500 } else if let Ok(f) = v.parse::<f64>() {
4501 m_variables_insert(
4502 k.clone(),
4503 mnumber {
4504 l: 0,
4505 d: f,
4506 type_: MN_FLOAT,
4507 },
4508 );
4509 } else if !v.is_empty() {
4510 // Non-numeric string — keep raw so getmathparam can
4511 // recursively evaluate it as an arith expression.
4512 // zsh: `a="3+2"; $((a))` returns 5.
4513 m_string_variables_insert(k.clone(), v.clone());
4514 }
4515 }
4516}
4517
4518// WARNING: NOT IN MATH.C — Rust-only accessor. zsh C writes back
4519// to the global param table during evaluation; ShellExecutor
4520// integration uses this to harvest the post-eval variables map and
4521// merge it into its own `variables` table.
4522/// Extract modified variables as string->string mapping (for shell integration)
4523pub(crate) fn extract_string_variables() -> HashMap<String, String> {
4524 M_VARIABLES.with(|c| {
4525 c.borrow()
4526 .iter()
4527 .map(|(k, v)| {
4528 (
4529 k.clone(),
4530 match v.type_ {
4531 MN_INTEGER => v.l.to_string(),
4532 MN_FLOAT => {
4533 let f = v.d;
4534 if isnan(f) {
4535 "NaN".to_string()
4536 } else if isinf(f) {
4537 if f > 0.0 {
4538 "Inf".to_string()
4539 } else {
4540 "-Inf".to_string()
4541 }
4542 } else {
4543 format!("{:.10}", f)
4544 }
4545 }
4546 _ => "0".to_string(),
4547 },
4548 )
4549 })
4550 .collect()
4551 })
4552}
4553
4554// WARNING: NOT IN MATH.C — Rust-only setopt mirror. zsh C reads
4555// the option flag directly from `isset(CPRECEDENCES)` inside
4556// `mathevall()`; this setter caches the bit so the evaluator
4557// avoids re-reading the option tree on every token.
4558pub(crate) fn with_c_precedences(enable: bool) {
4559 m_c_precedences_set(enable);
4560 m_prec_set(if enable { &C_PREC } else { &Z_PREC });
4561}
4562
4563// WARNING: NOT IN MATH.C — Rust-only setopt mirror for FORCE_FLOAT.
4564pub(crate) fn with_force_float(enable: bool) {
4565 m_force_float_set(enable);
4566}
4567
4568// WARNING: NOT IN MATH.C — Rust-only setopt mirror for OCTAL_ZEROES.
4569pub(crate) fn with_octal_zeroes(enable: bool) {
4570 m_octal_zeroes_set(enable);
4571}
4572
4573// WARNING: NOT IN MATH.C — Rust-only setter for `$?` (last command
4574// status) so the `?`-token in unary position can read it. zsh C
4575// reads `lastval` directly as a global.
4576pub(crate) fn with_lastval(val: i32) {
4577 m_lastval_set(val);
4578}
4579
4580// WARNING: NOT IN MATH.C — Rust-only cursor read. C uses `*ptr`
4581// directly without an fn-shaped wrapper.
4582pub(crate) fn peek() -> Option<char> {
4583 m_input_clone()[m_pos()..].chars().next()
4584}
4585
4586// WARNING: NOT IN MATH.C — Rust-only cursor advance. C uses
4587// `*ptr++` directly.
4588pub(crate) fn advance() -> Option<char> {
4589 let c = peek()?;
4590 m_pos_add(c.len_utf8());
4591 Some(c)
4592}
4593
4594// WARNING: NOT IN MATH.C — Rust-only char classifier. C uses
4595// ctype.h `idigit()` macro directly.
4596fn is_digit(c: char) -> bool {
4597 c.is_ascii_digit()
4598}
4599
4600// WARNING: NOT IN MATH.C — Rust-only char classifier. C uses
4601// `iident()` / `isalpha()` macros directly.
4602fn is_ident_start(c: char) -> bool {
4603 c.is_ascii_alphabetic() || c == '_'
4604}
4605
4606// WARNING: NOT IN MATH.C — Rust-only char classifier. C uses
4607// `iident()` macro directly.
4608fn is_ident(c: char) -> bool {
4609 c.is_ascii_alphanumeric() || c == '_'
4610}
4611
4612// WARNING: NOT IN MATH.C — Rust-only stack helper. C inlines
4613// this inside `pop()` (math.c:931) — its `noget` flag controls
4614// whether to resolve the deferred Unset+lval read; zshrs splits
4615// the two paths into separate ported so the resolved-vs-raw choice
4616// is at the call site.
4617pub(crate) fn pop_with_lval() -> mathvalue {
4618 m_stack_pop().unwrap_or_default()
4619}
4620
4621// WARNING: NOT IN MATH.C — Rust-only value-resolver. C inlines
4622// the deferred-variable-read pattern inside `pop()` and `op()`
4623// (math.c:931, 1154); the Rust port factors it out for `bop`
4624// and `mathparse` to inspect-without-consuming.
4625pub(crate) fn get_value(mv: &mathvalue) -> mnumber {
4626 if (mv.val.type_ == MN_UNSET) {
4627 if let Some(ref name) = mv.lval {
4628 return getmathparam(name);
4629 }
4630 }
4631 mv.val
4632}
4633
4634// WARNING: NOT IN MATH.C — Rust-only helper. C inlines the
4635// expression `prec[COMMA] + 1` directly in mathparse() and
4636// mathevall() everywhere it's needed (math.c:1594, 367).
4637pub(crate) fn top_prec() -> u8 {
4638 m_prec()[COMMA as usize] + 1
4639}
4640
4641// WARNING: NOT IN MATH.C — Rust-only accessor (note plural — singular
4642// `getmathparam` IS in math.c:337). zsh C's caller reads the param
4643// table directly post-eval; this returns a snapshot of the in-memory
4644// variables map for ShellExecutor integration.
4645/// Get updated variables after evaluation
4646pub(crate) fn getmathparams() -> HashMap<String, mnumber> {
4647 m_variables_clone()
4648}
4649
4650// ===========================================================
4651// Methods moved verbatim from src/ported/vm_helper because their
4652// C counterpart's source file maps 1:1 to this Rust module.
4653// Phase: math
4654// ===========================================================
4655
4656// BEGIN moved-from-exec-rs
4657// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
4658
4659// END moved-from-exec-rs
4660
4661// ===========================================================
4662// Free ported moved verbatim from src/ported/vm_helper.
4663// ===========================================================
4664// BEGIN moved-from-exec-rs (free ported)
4665/// Pop argc arguments from the VM stack into a Vec<String>.
4666///
4667/// `Value::Array` entries (produced by `${arr[@]}`, glob expansion, brace
4668/// expansion, etc.) splice into multiple argv-style args — same flattening
4669/// rule as fusevm's `Op::Exec`. Without this, a builtin like `echo
4670/// ${arr[@]}` with `arr=(x y z)` would receive a single space-joined arg
4671/// `"x y z"` instead of three separate args.
4672/// Subscript-arith parser namespace. Holds the three pre-resolve parsers
4673/// `eval_arith_expr` runs against an expression before substituting array
4674/// references — the C source's `mathexpr()` (Src/math.c) inlines this work
4675/// inside the lexer, but Rust splits it out so the assignment-target arms
4676/// don't get confused with read sites.
4677// WARNING: NOT IN MATH.C — Rust-only string parser. C `setmathvar`
4678// (math.c:972) walks the lvalue pointer left in place by zzlex,
4679// so subscripted compound assigns fall out of the lexer for free.
4680// zshrs sees `((a[i]+=v))` as raw text and must split it before
4681// pre_resolve_array_subscripts substitutes the read value in place.
4682#[inline]
4683/// Detect `name[idx]=rhs` (or `name[idx]+=rhs`, etc.) at the start of
4684/// an arith expression. Returns (name, idx_expr, rhs). Used by
4685/// `eval_arith_expr` to handle `((a[i]=expr))` — the regular pre-
4686/// resolve pass would substitute a[i] with its current value first,
4687/// turning the expression into `0=42` which is invalid.
4688/// Parse `name[idx]OP rhs?` where OP is `++`, `--`, `+=`, `-=`, etc.
4689/// Returns (name, idx_expr, op, rhs). For `++`/`--`, rhs is empty.
4690pub(crate) fn parse_compound(expr: &str) -> Option<(String, String, String, String)> {
4691 let trimmed = expr.trim();
4692 let bytes = trimmed.as_bytes();
4693 if bytes.is_empty() || !(bytes[0] == b'_' || bytes[0].is_ascii_alphabetic()) {
4694 return None;
4695 }
4696 let mut i = 1;
4697 while i < bytes.len() && (bytes[i] == b'_' || bytes[i].is_ascii_alphanumeric()) {
4698 i += 1;
4699 }
4700 let name = trimmed[..i].to_string();
4701 if i >= bytes.len() || bytes[i] != b'[' {
4702 return None;
4703 }
4704 let idx_start = i + 1;
4705 let mut depth = 1;
4706 let mut j = idx_start;
4707 while j < bytes.len() && depth > 0 {
4708 match bytes[j] {
4709 b'[' => depth += 1,
4710 b']' => {
4711 depth -= 1;
4712 if depth == 0 {
4713 break;
4714 }
4715 }
4716 _ => {}
4717 }
4718 j += 1;
4719 }
4720 if j >= bytes.len() {
4721 return None;
4722 }
4723 let idx_expr = trimmed[idx_start..j].to_string();
4724 let mut k = j + 1;
4725 while k < bytes.len() && bytes[k].is_ascii_whitespace() {
4726 k += 1;
4727 }
4728 if k >= bytes.len() {
4729 return None;
4730 }
4731 let rest = &bytes[k..];
4732 // Try 3-char operators first (`<<=`, `>>=`, `**=`), then 2-char
4733 // (`++`, `--`, `+=`, `-=`, `*=`, `/=`, `%=`, `&=`, `|=`, `^=`).
4734 let (op, op_len) = match rest {
4735 [b'<', b'<', b'=', ..] => ("<<=", 3),
4736 [b'>', b'>', b'=', ..] => (">>=", 3),
4737 [b'*', b'*', b'=', ..] => ("**=", 3),
4738 [b'+', b'+', ..] => ("++", 2),
4739 [b'-', b'-', ..] => ("--", 2),
4740 [b'+', b'=', ..] => ("+=", 2),
4741 [b'-', b'=', ..] => ("-=", 2),
4742 [b'*', b'=', ..] => ("*=", 2),
4743 [b'/', b'=', ..] => ("/=", 2),
4744 [b'%', b'=', ..] => ("%=", 2),
4745 [b'&', b'=', ..] => ("&=", 2),
4746 [b'|', b'=', ..] => ("|=", 2),
4747 [b'^', b'=', ..] => ("^=", 2),
4748 _ => return None,
4749 };
4750 let rhs = trimmed[k + op_len..].trim().to_string();
4751 // For `++` / `--`, the rhs MUST be empty (anything else would be
4752 // a parse error). For `+=` etc., rhs is the value expression.
4753 if (op == "++" || op == "--") && !rhs.is_empty() {
4754 return None;
4755 }
4756 Some((name, idx_expr, op.to_string(), rhs))
4757}
4758// WARNING: NOT IN MATH.C — Rust-only string parser. C handles
4759// `++NAME[IDX]` via the lexer leaving the lvalue pointer set; the
4760// Rust port pre-parses the text. See parse_compound above.
4761/// Pre-increment/decrement on subscript: `++NAME[IDX]` / `--NAME[IDX]`.
4762/// Returns (name, idx_expr, op) where op is "++" or "--".
4763pub(crate) fn parse_pre_inc(expr: &str) -> Option<(String, String, String)> {
4764 let trimmed = expr.trim();
4765 let (after_op, pre_op) = if let Some(s) = trimmed.strip_prefix("++") {
4766 (s, "++")
4767 } else if let Some(s) = trimmed.strip_prefix("--") {
4768 (s, "--")
4769 } else {
4770 return None;
4771 };
4772 let after_op = after_op.trim_start();
4773 let bytes = after_op.as_bytes();
4774 if bytes.is_empty() || !(bytes[0] == b'_' || bytes[0].is_ascii_alphabetic()) {
4775 return None;
4776 }
4777 let mut i = 1;
4778 while i < bytes.len() && (bytes[i] == b'_' || bytes[i].is_ascii_alphanumeric()) {
4779 i += 1;
4780 }
4781 let name = after_op[..i].to_string();
4782 if i >= bytes.len() || bytes[i] != b'[' {
4783 return None;
4784 }
4785 let idx_start = i + 1;
4786 let mut depth = 1;
4787 let mut j = idx_start;
4788 while j < bytes.len() && depth > 0 {
4789 match bytes[j] {
4790 b'[' => depth += 1,
4791 b']' => {
4792 depth -= 1;
4793 if depth == 0 {
4794 break;
4795 }
4796 }
4797 _ => {}
4798 }
4799 j += 1;
4800 }
4801 if j >= bytes.len() {
4802 return None;
4803 }
4804 let idx_expr = after_op[idx_start..j].to_string();
4805 // After ], must be end of input (or whitespace).
4806 let mut k = j + 1;
4807 while k < bytes.len() && bytes[k].is_ascii_whitespace() {
4808 k += 1;
4809 }
4810 if k != bytes.len() {
4811 return None;
4812 }
4813 Some((name, idx_expr, pre_op.to_string()))
4814}
4815// WARNING: NOT IN MATH.C — Rust-only string parser for `NAME[IDX]=v`.
4816// See parse_compound above for the rationale.
4817pub(crate) fn parse_assign(expr: &str) -> Option<(String, String, String)> {
4818 let trimmed = expr.trim();
4819 let bytes = trimmed.as_bytes();
4820 if bytes.is_empty() || !(bytes[0] == b'_' || bytes[0].is_ascii_alphabetic()) {
4821 return None;
4822 }
4823 let mut i = 1;
4824 while i < bytes.len() && (bytes[i] == b'_' || bytes[i].is_ascii_alphanumeric()) {
4825 i += 1;
4826 }
4827 let name = trimmed[..i].to_string();
4828 if i >= bytes.len() || bytes[i] != b'[' {
4829 return None;
4830 }
4831 let idx_start = i + 1;
4832 let mut depth = 1;
4833 let mut j = idx_start;
4834 while j < bytes.len() && depth > 0 {
4835 match bytes[j] {
4836 b'[' => depth += 1,
4837 b']' => {
4838 depth -= 1;
4839 if depth == 0 {
4840 break;
4841 }
4842 }
4843 _ => {}
4844 }
4845 j += 1;
4846 }
4847 if j >= bytes.len() {
4848 return None;
4849 }
4850 let idx_expr = trimmed[idx_start..j].to_string();
4851 // Skip ]
4852 let mut k = j + 1;
4853 while k < bytes.len() && bytes[k].is_ascii_whitespace() {
4854 k += 1;
4855 }
4856 if k >= bytes.len() || bytes[k] != b'=' {
4857 return None;
4858 }
4859 // Reject `==` and `=~` (comparison/regex, not assignment).
4860 if k + 1 < bytes.len() && (bytes[k + 1] == b'=' || bytes[k + 1] == b'~') {
4861 return None;
4862 }
4863 let rhs = trimmed[k + 1..].trim().to_string();
4864 Some((name, idx_expr, rhs))
4865}
4866
4867// END moved-from-exec-rs (free ported)
4868
4869// ===========================================================
4870// Numeric formatting helpers moved from src/ported/vm_helper.
4871// Mirror Src/math.c / Src/utils.c base+digit-grouping logic.
4872// ===========================================================
4873
4874// WARNING: NOT IN MATH.C — `convbase` lives in `Src/params.c:5632`
4875// (called from math.c:1089). This file holds a duplicate that
4876// predates the params.rs port; canonical home is
4877// `convbase`. This entry is drift pending
4878// cleanup; do not add new callers — use `convbase`.
4879/// Format an integer in the given base (2-36) using zsh's
4880// `convbase` lives in params.rs (matching C: defined in params.c:5632
4881// as a 1-line delegation to `convbase_ptr` at params.c:5586). The
4882// math.rs entry that used to duplicate it is removed; callers should
4883// import `convbase` directly.
4884
4885#[cfg(test)]
4886mod tests {
4887 use super::*;
4888
4889 /// `setmathvar` writes through to paramtab via `setnparam`.
4890 /// After the call, `getsparam(name)` should return the value.
4891 #[test]
4892 fn setmathvar_writes_to_paramtab() {
4893 let _g = crate::test_util::global_state_lock();
4894 // setnparam early-returns under `unset(EXECOPT)`. Enable it
4895 // (default in interactive shells; tests run with all opts
4896 // unset by default).
4897 opt_state_set("exec", true);
4898 // Sanity: a direct setiparam call should also work.
4899 unsetparam("mvar1_baseline");
4900 crate::ported::params::setiparam("mvar1_baseline", 42);
4901 let baseline = getsparam("mvar1_baseline");
4902 assert_eq!(
4903 baseline.as_deref(),
4904 Some("42"),
4905 "baseline setiparam path; got {:?}",
4906 baseline
4907 );
4908 unsetparam("mvar1_baseline");
4909
4910 unsetparam("mvar1");
4911 let v = mnumber {
4912 l: 42,
4913 d: 0.0,
4914 type_: MN_INTEGER,
4915 };
4916 let returned = setmathvar("mvar1", v);
4917 assert_eq!(returned.l, 42);
4918 let stored = getsparam("mvar1");
4919 assert_eq!(
4920 stored.as_deref(),
4921 Some("42"),
4922 "setmathvar should write through; got {:?}",
4923 stored
4924 );
4925 unsetparam("mvar1");
4926 opt_state_set("exec", false);
4927 }
4928
4929 /// `setmathvar` with empty name emits zerr and returns 0.
4930 #[test]
4931 fn setmathvar_empty_name_returns_zero() {
4932 let _g = crate::test_util::global_state_lock();
4933 let v = mnumber {
4934 l: 99,
4935 d: 0.0,
4936 type_: MN_INTEGER,
4937 };
4938 let returned = setmathvar("", v);
4939 assert_eq!(returned.l, 0);
4940 assert_eq!(returned.type_, MN_INTEGER);
4941 }
4942
4943 /// End-to-end round trip: `setmathvar` writes through paramtab;
4944 /// `getmathparam` reads back the same value via `getsparam`. Pins
4945 /// the full math ↔ paramtab integration that the recent setmathvar
4946 /// and getsparam-PM_INTEGER fixes enable together.
4947 #[test]
4948 fn setmathvar_getmathparam_roundtrip() {
4949 let _g = crate::test_util::global_state_lock();
4950 opt_state_set("exec", true);
4951 unsetparam("rt_int");
4952 unsetparam("rt_float");
4953
4954 // Integer roundtrip
4955 let n_in = mnumber {
4956 l: 123,
4957 d: 0.0,
4958 type_: MN_INTEGER,
4959 };
4960 setmathvar("rt_int", n_in);
4961 let n_out = getmathparam("rt_int");
4962 assert_eq!(n_out.type_, MN_INTEGER);
4963 assert_eq!(n_out.l, 123);
4964
4965 // Float roundtrip
4966 let f_in = mnumber {
4967 l: 0,
4968 d: 3.14,
4969 type_: MN_FLOAT,
4970 };
4971 setmathvar("rt_float", f_in);
4972 let f_out = getmathparam("rt_float");
4973 // Stored as paramtab PM_FFLOAT (per setnparam c:3687); read
4974 // back as MN_FLOAT.
4975 assert_eq!(f_out.type_, MN_FLOAT);
4976 assert!(
4977 (f_out.d - 3.14).abs() < 1e-9,
4978 "expected ~3.14, got {}",
4979 f_out.d
4980 );
4981
4982 unsetparam("rt_int");
4983 unsetparam("rt_float");
4984 opt_state_set("exec", false);
4985 }
4986
4987 /// `setmathvar` with subscript splits at `[` and writes to the
4988 /// base name (subscript handling is upstream).
4989 #[test]
4990 fn setmathvar_subscript_writes_to_base_name() {
4991 let _g = crate::test_util::global_state_lock();
4992 opt_state_set("exec", true);
4993 unsetparam("mvar2");
4994 let v = mnumber {
4995 l: 7,
4996 d: 0.0,
4997 type_: MN_INTEGER,
4998 };
4999 setmathvar("mvar2[5]", v);
5000 let stored = getsparam("mvar2");
5001 // The base "mvar2" got the value; subscript element handling
5002 // is upstream so we just confirm the param was created.
5003 assert!(stored.is_some());
5004 unsetparam("mvar2");
5005 opt_state_set("exec", false);
5006 }
5007
5008 /// `setmathvar` MUST short-circuit when `noeval` is set (c:1002-1003).
5009 /// Used by the unused branch of a math ternary: `(( cond ? a=1 : b=2 ))`
5010 /// evaluates only ONE side; the other side runs with noeval=1 to
5011 /// type-check without side effects. A regression that ignores
5012 /// noeval would assign BOTH sides, corrupting the unselected
5013 /// variable on every conditional expression.
5014 ///
5015 /// Pin: with noeval set, assigning to "ne_var" must NOT create
5016 /// the param. The return value still equals the input (so the
5017 /// arithmetic stack sees a sane value); paramtab stays unchanged.
5018 #[test]
5019 fn setmathvar_noeval_skips_paramtab_write() {
5020 let _g = crate::test_util::global_state_lock();
5021 opt_state_set("exec", true);
5022 unsetparam("ne_var");
5023
5024 // Set the math-local noeval counter.
5025 M_NOEVAL.with(|n| n.set(1));
5026
5027 let v = mnumber {
5028 l: 42,
5029 d: 0.0,
5030 type_: MN_INTEGER,
5031 };
5032 let ret = setmathvar("ne_var", v);
5033 assert_eq!(
5034 ret.l, 42,
5035 "c:1003 — `return v` so the stack still sees the value"
5036 );
5037 // The paramtab MUST NOT have a new entry.
5038 assert!(
5039 getsparam("ne_var").is_none(),
5040 "c:1002-1003 — noeval suppresses the paramtab write"
5041 );
5042
5043 // Restore noeval so subsequent tests aren't affected.
5044 M_NOEVAL.with(|n| n.set(0));
5045 opt_state_set("exec", false);
5046 }
5047
5048 /// `setmathvar` returns a value RE-TYPED to match the destination
5049 /// param's type (C c:1014-1027). Assigning a FLOAT to a PM_INTEGER
5050 /// param must return an integer-typed mnumber with the float
5051 /// truncated. This matches C's "assignment returns the typed
5052 /// value" contract — used by chained assignment expressions like
5053 /// `(( a = b = 3.7 ))` where `a`'s type drives the cascade.
5054 #[test]
5055 fn setmathvar_float_into_integer_coerces_return_type() {
5056 let _g = crate::test_util::global_state_lock();
5057 opt_state_set("exec", true);
5058 unsetparam("intvar");
5059 // Pre-create as PM_INTEGER so the type is fixed.
5060 crate::ported::params::setiparam("intvar", 0);
5061
5062 // Assign a float; expect integer return with truncated value.
5063 let v = mnumber {
5064 l: 0,
5065 d: 3.7,
5066 type_: MN_FLOAT,
5067 };
5068 let ret = setmathvar("intvar", v);
5069 assert_eq!(
5070 ret.type_, MN_INTEGER,
5071 "c:1016-1020 — PM_INTEGER target must return MN_INTEGER"
5072 );
5073 assert_eq!(
5074 ret.l, 3,
5075 "c:1018 — float→int truncates (3.7 → 3, not rounded)"
5076 );
5077
5078 unsetparam("intvar");
5079 opt_state_set("exec", false);
5080 }
5081
5082 /// Pin `mathevalarg` empty-string error path per c:1530-1532.
5083 /// Unlike `matheval` which returns MN_INTEGER 0 on empty, this
5084 /// entry point treats empty as a HARD error (emits zerr and
5085 /// returns 0). Used by callers like `$array[$ind]` where unset
5086 /// `$ind` should produce a diagnostic rather than silently index 0.
5087 #[test]
5088 fn mathevalarg_empty_emits_error_and_returns_zero() {
5089 let _g = crate::test_util::global_state_lock();
5090 // Empty input → returns 0 with error message emitted.
5091 let r = mathevalarg("");
5092 assert_eq!(r, 0, "c:1532 — empty input returns 0");
5093
5094 // Nularg-only → also empty after skip, returns 0.
5095 let nularg_only: String = "\u{a1}".to_string();
5096 let r = mathevalarg(&nularg_only);
5097 assert_eq!(
5098 r, 0,
5099 "c:1528-1532 — Nularg-only is empty after skip, returns 0"
5100 );
5101
5102 // Valid expression → real evaluation.
5103 let r = mathevalarg("1 + 2");
5104 assert_eq!(r, 3, "c:1534 — non-empty expression evaluates normally");
5105
5106 // Nularg-prefixed expression → skipped, then evaluated.
5107 let nularg_plus: String = "\u{a1}5 * 5".to_string();
5108 let r = mathevalarg(&nularg_plus);
5109 assert_eq!(
5110 r, 25,
5111 "c:1529 — Nularg skipped, then `5 * 5` evaluates to 25"
5112 );
5113 }
5114
5115 /// Pin `matheval` empty + Nularg fast paths per c:1489-1495.
5116 /// Empty input MUST return MN_INTEGER 0 without invoking the
5117 /// parser; the Nularg sentinel (0xa1) byte at the start of the
5118 /// input MUST be skipped before the empty check.
5119 #[test]
5120 fn matheval_empty_input_returns_zero_int() {
5121 let _g = crate::test_util::global_state_lock();
5122 // Empty string → MN_INTEGER 0 (c:1491-1494).
5123 let r = matheval("").expect("empty string must return 0, not error");
5124 assert_eq!(
5125 r.type_, MN_INTEGER,
5126 "c:1493 — empty input returns MN_INTEGER"
5127 );
5128 assert_eq!(r.l, 0, "c:1494 — empty input value is 0");
5129
5130 // Nularg-only string → also returns 0 (c:1489-1494).
5131 let nularg_only: String = "\u{a1}".to_string();
5132 let r = matheval(&nularg_only)
5133 .expect("Nularg-only must return 0 (treated as empty after skip)");
5134 assert_eq!(
5135 r.type_, MN_INTEGER,
5136 "c:1489-1493 — Nularg-only input treated as empty → MN_INTEGER"
5137 );
5138 assert_eq!(r.l, 0, "c:1494 — Nularg-only input value is 0");
5139
5140 // Nularg + expression → evaluates the expression (c:1490 skip).
5141 let nularg_plus: String = "\u{a1}1 + 2".to_string();
5142 let r =
5143 matheval(&nularg_plus).expect("Nularg prefix must be skipped and expression evaluated");
5144 let v = if r.type_ == MN_FLOAT { r.d as i64 } else { r.l };
5145 assert_eq!(v, 3, "c:1490 — Nularg skipped, then `1 + 2` evaluates to 3");
5146 }
5147
5148 #[test]
5149 fn test_basic_arithmetic() {
5150 let _g = crate::test_util::global_state_lock();
5151 assert_eq!(mathevali("1 + 2").unwrap(), 3);
5152 assert_eq!(mathevali("10 - 3").unwrap(), 7);
5153 assert_eq!(mathevali("4 * 5").unwrap(), 20);
5154 assert_eq!(mathevali("20 / 4").unwrap(), 5);
5155 assert_eq!(mathevali("17 % 5").unwrap(), 2);
5156 }
5157
5158 #[test]
5159 fn test_precedence() {
5160 let _g = crate::test_util::global_state_lock();
5161 assert_eq!(mathevali("2 + 3 * 4").unwrap(), 14);
5162 assert_eq!(mathevali("(2 + 3) * 4").unwrap(), 20);
5163 assert_eq!(mathevali("2 ** 3 ** 2").unwrap(), 512); // Right associative
5164 }
5165
5166 #[test]
5167 fn test_comparison() {
5168 let _g = crate::test_util::global_state_lock();
5169 assert_eq!(mathevali("5 > 3").unwrap(), 1);
5170 assert_eq!(mathevali("5 < 3").unwrap(), 0);
5171 assert_eq!(mathevali("5 == 5").unwrap(), 1);
5172 assert_eq!(mathevali("5 != 3").unwrap(), 1);
5173 assert_eq!(mathevali("5 >= 5").unwrap(), 1);
5174 assert_eq!(mathevali("5 <= 5").unwrap(), 1);
5175 }
5176
5177 #[test]
5178 fn test_logical() {
5179 let _g = crate::test_util::global_state_lock();
5180 assert_eq!(mathevali("1 && 1").unwrap(), 1);
5181 assert_eq!(mathevali("1 && 0").unwrap(), 0);
5182 assert_eq!(mathevali("1 || 0").unwrap(), 1);
5183 assert_eq!(mathevali("0 || 0").unwrap(), 0);
5184 assert_eq!(mathevali("!0").unwrap(), 1);
5185 assert_eq!(mathevali("!1").unwrap(), 0);
5186 }
5187
5188 #[test]
5189 fn test_bitwise() {
5190 let _g = crate::test_util::global_state_lock();
5191 assert_eq!(mathevali("5 & 3").unwrap(), 1);
5192 assert_eq!(mathevali("5 | 3").unwrap(), 7);
5193 assert_eq!(mathevali("5 ^ 3").unwrap(), 6);
5194 assert_eq!(mathevali("~0").unwrap(), -1);
5195 assert_eq!(mathevali("1 << 4").unwrap(), 16);
5196 assert_eq!(mathevali("16 >> 2").unwrap(), 4);
5197 }
5198
5199 #[test]
5200 fn test_ternary() {
5201 let _g = crate::test_util::global_state_lock();
5202 assert_eq!(mathevali("1 ? 10 : 20").unwrap(), 10);
5203 assert_eq!(mathevali("0 ? 10 : 20").unwrap(), 20);
5204 assert_eq!(mathevali("(5 > 3) ? 100 : 200").unwrap(), 100);
5205 }
5206
5207 #[test]
5208 fn test_power() {
5209 let _g = crate::test_util::global_state_lock();
5210 assert_eq!(mathevali("2 ** 10").unwrap(), 1024);
5211 assert_eq!(mathevali("3 ** 3").unwrap(), 27);
5212 assert!(
5213 (matheval("2.0 ** 0.5")
5214 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
5215 .unwrap()
5216 - std::f64::consts::SQRT_2)
5217 .abs()
5218 < 0.0001
5219 );
5220 }
5221
5222 #[test]
5223 fn test_float() {
5224 let _g = crate::test_util::global_state_lock();
5225 assert!(
5226 (matheval("3.14 + 0.01")
5227 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
5228 .unwrap()
5229 - 3.15)
5230 .abs()
5231 < 0.0001
5232 );
5233 assert!(
5234 (matheval("1.5 * 2.0")
5235 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
5236 .unwrap()
5237 - 3.0)
5238 .abs()
5239 < 0.0001
5240 );
5241 }
5242
5243 #[test]
5244 fn test_unary() {
5245 let _g = crate::test_util::global_state_lock();
5246 assert_eq!(mathevali("-5").unwrap(), -5);
5247 assert_eq!(mathevali("- -5").unwrap(), 5); // space needed to avoid --
5248 assert_eq!(mathevali("+5").unwrap(), 5);
5249 assert_eq!(mathevali("-(-5)").unwrap(), 5);
5250 }
5251
5252 #[test]
5253 fn test_base() {
5254 let _g = crate::test_util::global_state_lock();
5255 assert_eq!(mathevali("0xFF").unwrap(), 255);
5256 assert_eq!(mathevali("0b1010").unwrap(), 10);
5257 assert_eq!(mathevali("16#FF").unwrap(), 255);
5258 assert_eq!(mathevali("2#1010").unwrap(), 10);
5259 assert_eq!(mathevali("[16]FF").unwrap(), 255);
5260 }
5261
5262 #[test]
5263 fn test_variables() {
5264 let _g = crate::test_util::global_state_lock();
5265 let mut vars = HashMap::new();
5266 vars.insert(
5267 "x".to_string(),
5268 mnumber {
5269 l: 10,
5270 d: 0.0,
5271 type_: MN_INTEGER,
5272 },
5273 );
5274 vars.insert(
5275 "y".to_string(),
5276 mnumber {
5277 l: 20,
5278 d: 0.0,
5279 type_: MN_INTEGER,
5280 },
5281 );
5282
5283 new("x + y");
5284 with_variables(vars);
5285 assert_eq!(
5286 ({
5287 let __m = mathevall().unwrap();
5288 if __m.type_ == MN_FLOAT {
5289 __m.d as i64
5290 } else {
5291 __m.l
5292 }
5293 }),
5294 30
5295 );
5296 }
5297
5298 #[test]
5299 fn test_assignment() {
5300 let _g = crate::test_util::global_state_lock();
5301 new("x = 5");
5302 mathevall().unwrap();
5303 assert_eq!(
5304 ({
5305 let __m = m_variables_get("x").unwrap();
5306 if __m.type_ == MN_FLOAT {
5307 __m.d as i64
5308 } else {
5309 __m.l
5310 }
5311 }),
5312 5
5313 );
5314
5315 new("x = 5, x += 3");
5316 let result = mathevall().unwrap();
5317 assert_eq!(
5318 (if result.type_ == MN_FLOAT {
5319 result.d as i64
5320 } else {
5321 result.l
5322 }),
5323 8
5324 );
5325 }
5326
5327 #[test]
5328 fn test_increment() {
5329 let _g = crate::test_util::global_state_lock();
5330 let mut vars = HashMap::new();
5331 vars.insert(
5332 "x".to_string(),
5333 mnumber {
5334 l: 5,
5335 d: 0.0,
5336 type_: MN_INTEGER,
5337 },
5338 );
5339
5340 new("++x");
5341 with_variables(vars.clone());
5342 assert_eq!(
5343 ({
5344 let __m = mathevall().unwrap();
5345 if __m.type_ == MN_FLOAT {
5346 __m.d as i64
5347 } else {
5348 __m.l
5349 }
5350 }),
5351 6
5352 );
5353 assert_eq!(
5354 ({
5355 let __m = m_variables_get("x").unwrap();
5356 if __m.type_ == MN_FLOAT {
5357 __m.d as i64
5358 } else {
5359 __m.l
5360 }
5361 }),
5362 6
5363 );
5364
5365 new("x++");
5366 with_variables(vars.clone());
5367 assert_eq!(
5368 ({
5369 let __m = mathevall().unwrap();
5370 if __m.type_ == MN_FLOAT {
5371 __m.d as i64
5372 } else {
5373 __m.l
5374 }
5375 }),
5376 5
5377 );
5378 assert_eq!(
5379 ({
5380 let __m = m_variables_get("x").unwrap();
5381 if __m.type_ == MN_FLOAT {
5382 __m.d as i64
5383 } else {
5384 __m.l
5385 }
5386 }),
5387 6
5388 );
5389 }
5390
5391 #[test]
5392 fn test_functions() {
5393 let _g = crate::test_util::global_state_lock();
5394 // c:Src/math.c:1037 — `callmathfunc` requires `zsh/mathfunc`
5395 // to be in the loaded-modules table. zsh -fc returns
5396 // "unknown function: sqrt" without `zmodload zsh/mathfunc`
5397 // — the same gating now applies in zshrs. Boot the module
5398 // here so the unit test exercises the math-function bodies
5399 // not the missing-module guard.
5400 crate::ported::module::MODULESTAB
5401 .lock()
5402 .unwrap()
5403 .load_module("zsh/mathfunc");
5404 assert!(
5405 (matheval("sqrt(4)")
5406 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
5407 .unwrap()
5408 - 2.0)
5409 .abs()
5410 < 0.0001
5411 );
5412 assert!(
5413 (matheval("sin(0)")
5414 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
5415 .unwrap())
5416 .abs()
5417 < 0.0001
5418 );
5419 assert!(
5420 (matheval("cos(0)")
5421 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
5422 .unwrap()
5423 - 1.0)
5424 .abs()
5425 < 0.0001
5426 );
5427 assert!(
5428 (matheval("abs(-5)")
5429 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
5430 .unwrap()
5431 - 5.0)
5432 .abs()
5433 < 0.0001
5434 );
5435 assert!(
5436 (matheval("floor(3.7)")
5437 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
5438 .unwrap()
5439 - 3.0)
5440 .abs()
5441 < 0.0001
5442 );
5443 assert!(
5444 (matheval("ceil(3.2)")
5445 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
5446 .unwrap()
5447 - 4.0)
5448 .abs()
5449 < 0.0001
5450 );
5451 }
5452
5453 #[test]
5454 fn test_special_values() {
5455 let _g = crate::test_util::global_state_lock();
5456 assert!(matheval("Inf")
5457 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
5458 .unwrap()
5459 .is_infinite());
5460 assert!(matheval("NaN")
5461 .map(|n| (if n.type_ == MN_FLOAT { n.d } else { n.l as f64 }))
5462 .unwrap()
5463 .is_nan());
5464 }
5465
5466 #[test]
5467 fn test_errors() {
5468 let _g = crate::test_util::global_state_lock();
5469 assert!(matheval("1 / 0").is_err());
5470 assert!(matheval("1 +").is_err());
5471 // Empty arith expression is a parse error in zsh:
5472 // $ zsh -c '(( ))'; echo $? → 1
5473 // zsh aborts with `bad math expression: empty parentheses`.
5474 assert!(matheval("()").is_err());
5475 }
5476
5477 #[test]
5478 fn test_underscore_in_numbers() {
5479 let _g = crate::test_util::global_state_lock();
5480 assert_eq!(mathevali("1_000_000").unwrap(), 1000000);
5481 assert_eq!(mathevali("0xFF_FF").unwrap(), 65535);
5482 }
5483
5484 #[test]
5485 fn test_comma_operator() {
5486 let _g = crate::test_util::global_state_lock();
5487 assert_eq!(mathevali("1, 2, 3").unwrap(), 3);
5488 assert_eq!(mathevali("(x = 1, y = 2, x + y)").unwrap(), 3);
5489 }
5490
5491 /// c:1505 — integer divide-by-zero is a runtime error in `$(( ))`.
5492 /// A regression returning 0 silently masks programmer errors.
5493 #[test]
5494 fn mathevali_divide_by_zero_errors() {
5495 let _g = crate::test_util::global_state_lock();
5496 assert!(mathevali("1/0").is_err());
5497 assert!(mathevali("5/(2-2)").is_err());
5498 }
5499
5500 /// c:1505-1508 — `mathevali` returns `(x.type & MN_FLOAT) ?
5501 /// (zlong)x.u.d : x.u.l`. A float result rounded toward zero;
5502 /// MN_INTEGER returns x.u.l unchanged. Regression target: the
5503 /// previous Rust port used strict equality `== MN_FLOAT` which
5504 /// misclassifies any composite MN_FLOAT|MN_X bitfield.
5505 #[test]
5506 fn mathevali_truncates_float_via_bitmask_not_strict_eq() {
5507 let _g = crate::test_util::global_state_lock();
5508 // Float expression → truncated toward zero (3.7 → 3, -3.7 → -3).
5509 assert_eq!(mathevali("3.7").unwrap(), 3);
5510 assert_eq!(mathevali("-3.7").unwrap(), -3);
5511 // Pure integer expression → MN_INTEGER path, no truncation.
5512 assert_eq!(mathevali("42").unwrap(), 42);
5513 }
5514
5515 /// c:1480 — mod-by-zero is also an error (matches POSIX).
5516 #[test]
5517 fn mathevali_mod_by_zero_errors() {
5518 let _g = crate::test_util::global_state_lock();
5519 assert!(mathevali("5 % 0").is_err());
5520 }
5521
5522 /// c:1505 — operator precedence: `*` binds tighter than `+`.
5523 /// Regression flipping this would silently break every
5524 /// `$(( a + b * c ))` users compute.
5525 #[test]
5526 fn mathevali_respects_multiplicative_over_additive_precedence() {
5527 let _g = crate::test_util::global_state_lock();
5528 assert_eq!(mathevali("1 + 2 * 3").unwrap(), 7);
5529 assert_eq!(mathevali("(1 + 2) * 3").unwrap(), 9);
5530 assert_eq!(mathevali("10 - 2 * 3").unwrap(), 4);
5531 }
5532
5533 /// c:1505 — bitshift `<<` `>>` from `$(( ))` grammar. Regression
5534 /// dropping them breaks any hex-mask / bit-pack computation.
5535 #[test]
5536 fn mathevali_bitshift_operators() {
5537 let _g = crate::test_util::global_state_lock();
5538 assert_eq!(mathevali("1 << 4").unwrap(), 16);
5539 assert_eq!(mathevali("256 >> 3").unwrap(), 32);
5540 }
5541
5542 /// c:1505 — `&&` short-circuits on zero LHS. Regression that
5543 /// evaluates the RHS would surface side-effects (or divide-
5544 /// by-zero) the user expected NOT to fire.
5545 #[test]
5546 fn mathevali_logical_and_short_circuits_on_zero_lhs() {
5547 let _g = crate::test_util::global_state_lock();
5548 // If RHS evaluated, `1/0` would error. Short-circuit must skip.
5549 assert_eq!(mathevali("0 && 1/0").unwrap(), 0);
5550 }
5551
5552 /// c:1505 — `||` short-circuits on non-zero LHS. Same rationale.
5553 #[test]
5554 fn mathevali_logical_or_short_circuits_on_nonzero_lhs() {
5555 let _g = crate::test_util::global_state_lock();
5556 assert_eq!(mathevali("1 || 1/0").unwrap(), 1);
5557 }
5558
5559 /// c:1505 — ternary `cond ? a : b` evaluates ONLY the selected
5560 /// branch. Regression that evaluates both surfaces side-effects
5561 /// in the unused branch.
5562 #[test]
5563 fn mathevali_ternary_evaluates_only_selected_branch() {
5564 let _g = crate::test_util::global_state_lock();
5565 assert_eq!(mathevali("1 ? 42 : 1/0").unwrap(), 42);
5566 assert_eq!(mathevali("0 ? 1/0 : 42").unwrap(), 42);
5567 }
5568
5569 /// `Src/math.c:467-490` — `$(( ))` integer arithmetic supports
5570 /// hex (`0x`/`0X`), binary (`0b`/`0B`). Octal-via-leading-zero
5571 /// (`0777`) is OPT-IN behind the OCTALZEROES option — by default
5572 /// `0777` parses as decimal 777 (matches C's c:489 conditional).
5573 /// Pin both the hex/binary path AND the default-decimal behavior
5574 /// for leading-zero.
5575 #[test]
5576 fn mathevali_parses_hex_and_binary_literals() {
5577 let _g = crate::test_util::global_state_lock();
5578 // octalzeroes is reset to OFF by global_state_lock (test_util.rs)
5579 // so the `0777`-as-decimal pin works regardless of which test
5580 // ran first.
5581 // Hex literals at c:471 (lowchar 'x').
5582 assert_eq!(mathevali("0xff").unwrap(), 255);
5583 assert_eq!(mathevali("0x10").unwrap(), 16);
5584 assert_eq!(mathevali("0xff + 1").unwrap(), 256);
5585 // Binary literals at c:471 (lowchar 'b').
5586 assert_eq!(mathevali("0b1010").unwrap(), 10);
5587 assert_eq!(mathevali("0b11111111").unwrap(), 255);
5588 // Default-OCTALZEROES-off: `0777` is decimal 777, NOT octal 511.
5589 assert_eq!(
5590 mathevali("0777").unwrap(),
5591 777,
5592 "c:489 — leading-zero parses as decimal when OCTALZEROES off"
5593 );
5594 }
5595
5596 /// c:1505 — bitwise AND/OR/XOR. Each operator has its own
5597 /// precedence tier between shifts and logical ops. Regression
5598 /// flipping precedence between `&` and `|` would break `$(( a &
5599 /// b | c ))` (must be `(a&b) | c`, not `a & (b|c)`).
5600 #[test]
5601 fn mathevali_bitwise_operators_and_or_xor() {
5602 let _g = crate::test_util::global_state_lock();
5603 // Boolean truth-table cases.
5604 assert_eq!(mathevali("12 & 10").unwrap(), 8, "1100 & 1010 = 1000");
5605 assert_eq!(mathevali("12 | 10").unwrap(), 14, "1100 | 1010 = 1110");
5606 assert_eq!(mathevali("12 ^ 10").unwrap(), 6, "1100 ^ 1010 = 0110");
5607 // Precedence: `&` > `^` > `|`, so `a & b | c` == `(a&b) | c`.
5608 assert_eq!(
5609 mathevali("12 & 10 | 1").unwrap(),
5610 9,
5611 "c:1505 — & binds tighter than | : (12 & 10) | 1 = 8 | 1 = 9"
5612 );
5613 }
5614
5615 /// c:1505 — comparison ops produce 0 or 1 (Boolean semantics).
5616 /// Pin all six relational ops.
5617 #[test]
5618 fn mathevali_comparison_operators_return_zero_or_one() {
5619 let _g = crate::test_util::global_state_lock();
5620 assert_eq!(mathevali("1 < 2").unwrap(), 1);
5621 assert_eq!(mathevali("2 < 1").unwrap(), 0);
5622 assert_eq!(mathevali("2 > 1").unwrap(), 1);
5623 assert_eq!(mathevali("1 > 2").unwrap(), 0);
5624 assert_eq!(mathevali("2 <= 2").unwrap(), 1);
5625 assert_eq!(mathevali("2 >= 2").unwrap(), 1);
5626 assert_eq!(mathevali("2 == 2").unwrap(), 1);
5627 assert_eq!(mathevali("2 != 2").unwrap(), 0);
5628 }
5629
5630 /// c:1505 — unary minus and bitwise NOT. The C parser must
5631 /// distinguish `1 - 2` (binary) from `1 + -2` (unary).
5632 #[test]
5633 fn mathevali_unary_minus_and_bitwise_not() {
5634 let _g = crate::test_util::global_state_lock();
5635 assert_eq!(mathevali("-5").unwrap(), -5);
5636 assert_eq!(mathevali("-(2 + 3)").unwrap(), -5);
5637 assert_eq!(mathevali("1 + -2").unwrap(), -1);
5638 // Bitwise NOT (~).
5639 assert_eq!(mathevali("~0").unwrap(), -1, "two's-complement: ~0 = -1");
5640 assert_eq!(mathevali("~5").unwrap(), -6);
5641 }
5642
5643 /// c:1505 — logical NOT operator `!`. Maps 0 → 1, anything-else → 0.
5644 #[test]
5645 fn mathevali_logical_not() {
5646 let _g = crate::test_util::global_state_lock();
5647 assert_eq!(mathevali("!0").unwrap(), 1);
5648 assert_eq!(mathevali("!1").unwrap(), 0);
5649 assert_eq!(mathevali("!42").unwrap(), 0);
5650 // Double-NOT normalises to 0/1.
5651 assert_eq!(mathevali("!!42").unwrap(), 1);
5652 assert_eq!(mathevali("!!0").unwrap(), 0);
5653 }
5654
5655 /// `Src/math.c:109-161` — math token IDs are `#define`d as a
5656 /// densely-packed integer ladder used as indices into the precedence
5657 /// (`Z_PREC` / `C_PREC`) and type (`OP_TYPE`) tables. Position is
5658 /// load-bearing: shifting any value silently mis-routes every math
5659 /// expression at runtime. Pin every value individually so a reorder
5660 /// or off-by-one fails this test. QUEST=27 and COMMA=43 specifically
5661 /// were previously typed in Title-case (`Quest`/`Comma`) violating
5662 /// the C-source casing rule.
5663 #[test]
5664 fn math_token_ids_match_c_source_position_for_position() {
5665 let _g = crate::test_util::global_state_lock();
5666 let table = [
5667 ("M_INPAR", M_INPAR, 0),
5668 ("M_OUTPAR", M_OUTPAR, 1),
5669 ("NOT", NOT, 2),
5670 ("COMP", COMP, 3),
5671 ("POSTPLUS", POSTPLUS, 4),
5672 ("POSTMINUS", POSTMINUS, 5),
5673 ("UPLUS", UPLUS, 6),
5674 ("UMINUS", UMINUS, 7),
5675 ("AND", AND, 8),
5676 ("XOR", XOR, 9),
5677 ("OR", OR, 10),
5678 ("MUL", MUL, 11),
5679 ("DIV", DIV, 12),
5680 ("MOD", MOD, 13),
5681 ("PLUS", PLUS, 14),
5682 ("MINUS", MINUS, 15),
5683 ("SHLEFT", SHLEFT, 16),
5684 ("SHRIGHT", SHRIGHT, 17),
5685 ("LES", LES, 18),
5686 ("LEQ", LEQ, 19),
5687 ("GRE", GRE, 20),
5688 ("GEQ", GEQ, 21),
5689 ("DEQ", DEQ, 22),
5690 ("NEQ", NEQ, 23),
5691 ("DAND", DAND, 24),
5692 ("DOR", DOR, 25),
5693 ("DXOR", DXOR, 26),
5694 ("QUEST", QUEST, 27), // c:136 — was Title-case Quest, divergent
5695 ("COLON", COLON, 28),
5696 ("EQ", EQ, 29),
5697 ("PLUSEQ", PLUSEQ, 30),
5698 ("MINUSEQ", MINUSEQ, 31),
5699 ("MULEQ", MULEQ, 32),
5700 ("DIVEQ", DIVEQ, 33),
5701 ("MODEQ", MODEQ, 34),
5702 ("ANDEQ", ANDEQ, 35),
5703 ("XOREQ", XOREQ, 36),
5704 ("OREQ", OREQ, 37),
5705 ("SHLEFTEQ", SHLEFTEQ, 38),
5706 ("SHRIGHTEQ", SHRIGHTEQ, 39),
5707 ("DANDEQ", DANDEQ, 40),
5708 ("DOREQ", DOREQ, 41),
5709 ("DXOREQ", DXOREQ, 42),
5710 ("COMMA", COMMA, 43), // c:152 — was Title-case Comma, divergent
5711 ("EOI", EOI, 44),
5712 ("PREPLUS", PREPLUS, 45),
5713 ("PREMINUS", PREMINUS, 46),
5714 ("NUM", NUM, 47),
5715 ("ID", ID, 48),
5716 ("POWER", POWER, 49),
5717 ("CID", CID, 50),
5718 ("POWEREQ", POWEREQ, 51),
5719 ("FUNC", FUNC, 52),
5720 ];
5721 for (name, got, want) in table {
5722 assert_eq!(
5723 got, want,
5724 "c:109-161 — {} must be {} (C source value)",
5725 name, want
5726 );
5727 }
5728 // TOKCOUNT = 53 must equal the table length (no holes).
5729 assert_eq!(
5730 TOKCOUNT,
5731 table.len() + 0,
5732 "c:162 — TOKCOUNT must match the number of tokens"
5733 );
5734 // QUEST sits between DXOR and COLON; gap was 26→28 BEFORE the
5735 // QUEST=27 fix, exposing a missing index. Pin the ordering.
5736 assert_eq!(QUEST, DXOR + 1, "c:136 — QUEST immediately follows DXOR");
5737 assert_eq!(COLON, QUEST + 1, "c:137 — COLON immediately follows QUEST");
5738 assert_eq!(
5739 COMMA,
5740 DXOREQ + 1,
5741 "c:152 — COMMA immediately follows DXOREQ"
5742 );
5743 assert_eq!(EOI, COMMA + 1, "c:153 — EOI immediately follows COMMA");
5744 }
5745
5746 /// `Src/math.c:109-162` — the precedence and type tables MUST have
5747 /// length `TOKCOUNT`. Pin both lengths so a future token addition
5748 /// without table updates fails immediately.
5749 #[test]
5750 fn math_dispatch_tables_match_tokcount() {
5751 let _g = crate::test_util::global_state_lock();
5752 assert_eq!(
5753 Z_PREC.len(),
5754 TOKCOUNT,
5755 "Z_PREC must have one slot per math token"
5756 );
5757 assert_eq!(
5758 C_PREC.len(),
5759 TOKCOUNT,
5760 "C_PREC must have one slot per math token"
5761 );
5762 assert_eq!(
5763 OP_TYPE.len(),
5764 TOKCOUNT,
5765 "OP_TYPE must have one slot per math token"
5766 );
5767 }
5768
5769 // ═══════════════════════════════════════════════════════════════════
5770 // matheval / mathevali — anchored to `zsh -c 'echo $(( ... ))'`.
5771 // Each expected value verified against zsh 5.9. Where zshrs diverges
5772 // the test FAILS, exposing the bug. matheval returns mnumber; we
5773 // mostly use mathevali for integer comparisons.
5774 // ═══════════════════════════════════════════════════════════════════
5775
5776 fn mi(expr: &str) -> i64 {
5777 let _g = crate::test_util::global_state_lock();
5778 mathevali(expr).unwrap_or_else(|e| panic!("mathevali({expr:?}) → Err({e})"))
5779 }
5780
5781 // ── Literals ────────────────────────────────────────────────────
5782 /// `echo $(( 42 ))` → 42
5783 #[test]
5784 fn matheval_decimal_literal() {
5785 assert_eq!(mi("42"), 42);
5786 }
5787
5788 /// `echo $(( -7 ))` → -7
5789 #[test]
5790 fn matheval_unary_minus_literal() {
5791 assert_eq!(mi("-7"), -7);
5792 }
5793
5794 /// `echo $(( 0xff ))` → 255
5795 #[test]
5796 fn matheval_hex_literal_lowercase() {
5797 assert_eq!(mi("0xff"), 255);
5798 }
5799
5800 /// `echo $(( 0XDEAD ))` → 57005
5801 #[test]
5802 fn matheval_hex_literal_uppercase() {
5803 assert_eq!(mi("0XDEAD"), 0xDEAD);
5804 }
5805
5806 /// `echo $(( 16#FF ))` → 255 (zsh base# literal)
5807 #[test]
5808 fn matheval_base_hash_hex() {
5809 assert_eq!(mi("16#FF"), 255);
5810 }
5811
5812 /// `echo $(( 2#1010 ))` → 10
5813 #[test]
5814 fn matheval_base_hash_binary() {
5815 assert_eq!(mi("2#1010"), 10);
5816 }
5817
5818 /// `echo $(( 8#17 ))` → 15
5819 #[test]
5820 fn matheval_base_hash_octal() {
5821 assert_eq!(mi("8#17"), 15);
5822 }
5823
5824 /// `echo $(( 010 ))` → 10 (zsh default: NOT octal unless OCTAL_ZEROES set)
5825 /// Test relies on `test_util::global_state_lock()` (acquired inside `mi`)
5826 /// to reset `octalzeroes` to OFF on entry — see test_util.rs:53.
5827 #[test]
5828 fn matheval_leading_zero_is_decimal_not_octal() {
5829 assert_eq!(mi("010"), 10);
5830 }
5831
5832 // ── Binary arithmetic ──────────────────────────────────────────
5833 /// `echo $(( 1 + 2 + 3 ))` → 6
5834 #[test]
5835 fn matheval_addition_chain() {
5836 assert_eq!(mi("1 + 2 + 3"), 6);
5837 }
5838
5839 /// `echo $(( 2 * 3 + 4 ))` → 10 (precedence: * over +)
5840 #[test]
5841 fn matheval_precedence_mul_over_add() {
5842 assert_eq!(mi("2 * 3 + 4"), 10);
5843 }
5844
5845 /// `echo $(( 2 * (3 + 4) ))` → 14 (parens override)
5846 #[test]
5847 fn matheval_parens_override_precedence() {
5848 assert_eq!(mi("2 * (3 + 4)"), 14);
5849 }
5850
5851 /// `echo $(( 17 / 5 ))` → 3 (integer division, truncates toward 0)
5852 #[test]
5853 fn matheval_integer_division_truncates() {
5854 assert_eq!(mi("17 / 5"), 3);
5855 }
5856
5857 /// `echo $(( 1 / 4 ))` → 0 (integer division of small numerator)
5858 #[test]
5859 fn matheval_integer_division_below_one() {
5860 assert_eq!(mi("1 / 4"), 0);
5861 }
5862
5863 /// `echo $(( 17 % 5 ))` → 2
5864 #[test]
5865 fn matheval_modulo() {
5866 assert_eq!(mi("17 % 5"), 2);
5867 }
5868
5869 /// `echo $(( 2 ** 10 ))` → 1024
5870 #[test]
5871 fn matheval_power() {
5872 assert_eq!(mi("2 ** 10"), 1024);
5873 }
5874
5875 /// `echo $(( 3 ** 3 ))` → 27
5876 #[test]
5877 fn matheval_power_small_cubed() {
5878 assert_eq!(mi("3 ** 3"), 27);
5879 }
5880
5881 /// `echo $(( -2 ** 2 ))` → 4 (zsh: unary binds tighter than **)
5882 #[test]
5883 fn matheval_unary_binds_tighter_than_power() {
5884 assert_eq!(mi("-2 ** 2"), 4);
5885 }
5886
5887 // ── Bitwise ─────────────────────────────────────────────────────
5888 /// `echo $(( 0xff & 0x0f ))` → 15
5889 #[test]
5890 fn matheval_bitand() {
5891 assert_eq!(mi("0xff & 0x0f"), 15);
5892 }
5893
5894 /// `echo $(( 0xff | 0x100 ))` → 511
5895 #[test]
5896 fn matheval_bitor() {
5897 assert_eq!(mi("0xff | 0x100"), 511);
5898 }
5899
5900 /// `echo $(( 0xff ^ 0x0f ))` → 240
5901 #[test]
5902 fn matheval_bitxor() {
5903 assert_eq!(mi("0xff ^ 0x0f"), 240);
5904 }
5905
5906 /// `echo $(( ~0 ))` → -1 (two's-complement bitwise NOT)
5907 #[test]
5908 fn matheval_bitnot_zero_is_minus_one() {
5909 assert_eq!(mi("~0"), -1);
5910 }
5911
5912 /// `echo $(( 1 << 8 ))` → 256
5913 #[test]
5914 fn matheval_left_shift() {
5915 assert_eq!(mi("1 << 8"), 256);
5916 }
5917
5918 /// `echo $(( 256 >> 4 ))` → 16
5919 #[test]
5920 fn matheval_right_shift() {
5921 assert_eq!(mi("256 >> 4"), 16);
5922 }
5923
5924 /// `echo $(( -1 >> 1 ))` → -1 (arithmetic shift, sign-preserving)
5925 #[test]
5926 fn matheval_arithmetic_right_shift_preserves_sign() {
5927 assert_eq!(mi("-1 >> 1"), -1);
5928 }
5929
5930 // ── Comparison & logical ───────────────────────────────────────
5931 /// `echo $(( 5 == 5 ))` → 1
5932 #[test]
5933 fn matheval_eq_true() {
5934 assert_eq!(mi("5 == 5"), 1);
5935 }
5936
5937 /// `echo $(( 5 != 6 ))` → 1
5938 #[test]
5939 fn matheval_ne_true() {
5940 assert_eq!(mi("5 != 6"), 1);
5941 }
5942
5943 /// `echo $(( 3 < 5 ))` → 1
5944 #[test]
5945 fn matheval_lt_true() {
5946 assert_eq!(mi("3 < 5"), 1);
5947 }
5948
5949 /// `echo $(( 5 <= 5 ))` → 1
5950 #[test]
5951 fn matheval_le_true_on_equal() {
5952 assert_eq!(mi("5 <= 5"), 1);
5953 }
5954
5955 /// `echo $(( 1 && 1 ))` → 1
5956 #[test]
5957 fn matheval_logand_both_true() {
5958 assert_eq!(mi("1 && 1"), 1);
5959 }
5960
5961 /// `echo $(( 0 || 1 ))` → 1
5962 #[test]
5963 fn matheval_logor_one_true() {
5964 assert_eq!(mi("0 || 1"), 1);
5965 }
5966
5967 /// `echo $(( !0 ))` → 1
5968 #[test]
5969 fn matheval_lognot_false() {
5970 assert_eq!(mi("!0"), 1);
5971 }
5972
5973 /// `echo $(( !5 ))` → 0
5974 #[test]
5975 fn matheval_lognot_truthy() {
5976 assert_eq!(mi("!5"), 0);
5977 }
5978
5979 // ── Ternary ─────────────────────────────────────────────────────
5980 /// `echo $(( 1 ? 10 : 20 ))` → 10
5981 #[test]
5982 fn matheval_ternary_true_branch() {
5983 assert_eq!(mi("1 ? 10 : 20"), 10);
5984 }
5985
5986 /// `echo $(( 0 ? 10 : 20 ))` → 20
5987 #[test]
5988 fn matheval_ternary_false_branch() {
5989 assert_eq!(mi("0 ? 10 : 20"), 20);
5990 }
5991
5992 // ── Comma operator ─────────────────────────────────────────────
5993 /// `echo $(( (1,2,3) ))` → 3 (comma returns last)
5994 #[test]
5995 fn matheval_comma_returns_last() {
5996 assert_eq!(mi("(1,2,3)"), 3);
5997 }
5998
5999 // ── Floats via matheval (mnumber tag) ──────────────────────────
6000 /// `1.0 / 4` returns a float (MN_FLOAT) — pin the type discriminator.
6001 #[test]
6002 fn matheval_float_div_returns_mn_float_type() {
6003 let _g = crate::test_util::global_state_lock();
6004 let n = matheval("1.0 / 4").expect("matheval");
6005 // MN_FLOAT flag must be set on the result type.
6006 assert_ne!(
6007 n.type_ & MN_FLOAT,
6008 0,
6009 "1.0 / 4 must carry MN_FLOAT in type; got type_=0x{:x}",
6010 n.type_
6011 );
6012 // d field holds the float value; should be ~0.25.
6013 assert!(
6014 (n.d - 0.25).abs() < 1e-9,
6015 "1.0 / 4 d-field should be 0.25; got {}",
6016 n.d
6017 );
6018 }
6019
6020 /// `42` returns an integer (MN_INTEGER, not MN_FLOAT).
6021 #[test]
6022 fn matheval_integer_literal_returns_mn_integer_type() {
6023 let _g = crate::test_util::global_state_lock();
6024 let n = matheval("42").expect("matheval");
6025 assert_eq!(
6026 n.type_ & MN_FLOAT,
6027 0,
6028 "42 must NOT carry MN_FLOAT; got type_=0x{:x}",
6029 n.type_
6030 );
6031 assert_eq!(n.l, 42);
6032 }
6033
6034 /// matheval on empty string → MN_INTEGER 0 (C c:1491-1495 fast path).
6035 #[test]
6036 fn matheval_empty_input_returns_zero() {
6037 let _g = crate::test_util::global_state_lock();
6038 let n = matheval("").expect("matheval(\"\") must succeed");
6039 assert_eq!(n.l, 0, "empty input → 0");
6040 assert_eq!(
6041 n.type_, MN_INTEGER,
6042 "empty input → MN_INTEGER (c:1491-1495)"
6043 );
6044 }
6045
6046 // ── mathevali (integer-coerce front-end) ───────────────────────
6047 /// `mathevali` integer-coerces float results via `(zlong)x.u.d` — pin
6048 /// the truncation semantics (away from zero is wrong; C truncates).
6049 #[test]
6050 fn mathevali_truncates_float_toward_zero() {
6051 let _g = crate::test_util::global_state_lock();
6052 // 7.9 → truncates to 7 (NOT rounds to 8)
6053 assert_eq!(mathevali("7.9").unwrap(), 7);
6054 // -7.9 → truncates to -7 (NOT rounds to -8)
6055 assert_eq!(mathevali("-7.9").unwrap(), -7);
6056 }
6057
6058 // ═══════════════════════════════════════════════════════════════════
6059 // zsh test-corpus pins — Test/C01arith.ztst arithmetic regression
6060 // suite. Each test cites the ztst line range; pass = lock current
6061 // correct behavior, #[ignore = "ZSHRS BUG: ..."] = tracked gap.
6062 // ═══════════════════════════════════════════════════════════════════
6063
6064 /// `Test/C01arith.ztst:6-10` — basic integer literal.
6065 #[test]
6066 fn zsh_corpus_basic_integer_literal() {
6067 let _g = crate::test_util::global_state_lock();
6068 assert_eq!(matheval("42").unwrap().l, 42, "ztst:9 — int literal");
6069 }
6070
6071 /// `Test/C01arith.ztst:22-25` — `((29.1 % 13.0 * 10) + 0.5)` = 31.6
6072 /// → integer-coerced to 31.
6073 #[test]
6074 fn zsh_corpus_float_modulo_then_int_truncation() {
6075 let _g = crate::test_util::global_state_lock();
6076 let r = mathevali("(29.1 % 13.0 * 10) + 0.5");
6077 assert_eq!(r.ok(), Some(31), "ztst:25 — float % then int truncation");
6078 }
6079
6080 /// `Test/C01arith.ztst:27-29` — multi-base input:
6081 /// `0x10 + 0X01 + 2#1010` = 16 + 1 + 10 = 27.
6082 #[test]
6083 fn zsh_corpus_multi_base_input() {
6084 let _g = crate::test_util::global_state_lock();
6085 assert_eq!(
6086 mathevali("0x10 + 0X01 + 2#1010").unwrap(),
6087 27,
6088 "ztst:29 — hex + 2#binary sum",
6089 );
6090 }
6091
6092 /// `Test/C01arith.ztst:41-44` — float→int truncation:
6093 /// `(( i = 32.5 ))` then int → 32.
6094 #[test]
6095 fn zsh_corpus_float_truncates_in_integer_context() {
6096 let _g = crate::test_util::global_state_lock();
6097 assert_eq!(
6098 mathevali("32.5").unwrap(),
6099 32,
6100 "ztst:44 — truncate, not round"
6101 );
6102 }
6103
6104 /// `Test/C01arith.ztst:46-50` — operator precedence chain:
6105 /// `4 - - 3 * 7 << 1 & 7 ^ 1 | 16 ** 2` = 1591 (zsh-default
6106 /// MATH_OPS precedence, NOT C precedence).
6107 #[test]
6108 fn zsh_corpus_zsh_precedence_chain() {
6109 let _g = crate::test_util::global_state_lock();
6110 let r = mathevali("4 - - 3 * 7 << 1 & 7 ^ 1 | 16 ** 2");
6111 assert_eq!(r.ok(), Some(1591), "ztst:50 — zsh-default precedence");
6112 }
6113
6114 /// `Test/C01arith.ztst:96-97` — mixed int+float:
6115 /// `3 + 5 * 1.75` = 11.75 (float promotion).
6116 #[test]
6117 fn zsh_corpus_mixed_int_float_promotes_to_float() {
6118 let _g = crate::test_util::global_state_lock();
6119 let n = matheval("3 + 5 * 1.75").unwrap();
6120 assert!(
6121 (n.d - 11.75).abs() < 1e-9,
6122 "ztst:96 — 3+5*1.75 = 11.75 (float promotion)"
6123 );
6124 }
6125
6126 /// `Test/C01arith.ztst:62-64` — logical precedence:
6127 /// `1 < 2 || 2 < 2 && 3 > 4` = 1 (|| lower than &&).
6128 #[test]
6129 fn zsh_corpus_logical_precedence_or_low_and_high() {
6130 let _g = crate::test_util::global_state_lock();
6131 let n = mathevali("1 < 2 || 2 < 2 && 3 > 4").unwrap();
6132 assert_eq!(n, 1, "ztst:64 — || lower than &&");
6133 }
6134
6135 /// `Test/C01arith.ztst:66-68` — nested ternary right-associative:
6136 /// `1+4 ? 3+2 ? 4+3 ? 5+6 ? 4*8 : 0 : 0 : 0 : 0` = 32.
6137 #[test]
6138 fn zsh_corpus_ternary_right_associative_nested() {
6139 let _g = crate::test_util::global_state_lock();
6140 let n = mathevali("1+4 ? 3+2 ? 4+3 ? 5+6 ? 4*8 : 0 : 0 : 0 : 0").unwrap();
6141 assert_eq!(n, 32, "ztst:68 — nested ternary right-associative");
6142 }
6143
6144 /// `Test/C01arith.ztst:78-80` — comma returns last:
6145 /// `0, 4 ? 3 : 1, 5` = 5.
6146 #[test]
6147 fn zsh_corpus_comma_returns_last_value() {
6148 let _g = crate::test_util::global_state_lock();
6149 let n = mathevali("0, 4 ? 3 : 1, 5").unwrap();
6150 assert_eq!(n, 5, "ztst:80 — comma operator returns last");
6151 }
6152
6153 /// `Test/C01arith.ztst:9` — `1 + 2 * 3` = 7 (precedence).
6154 #[test]
6155 fn zsh_corpus_integer_precedence_mul_before_add() {
6156 let _g = crate::test_util::global_state_lock();
6157 let n = mathevali("1 + 2 * 3").unwrap();
6158 assert_eq!(n, 7, "ztst:9 — *,/ before +,-");
6159 }
6160
6161 /// `Test/C01arith.ztst:18` — `1.5 + 2.5` = 4.0.
6162 #[test]
6163 fn zsh_corpus_basic_float_add() {
6164 let _g = crate::test_util::global_state_lock();
6165 let n = matheval("1.5 + 2.5").unwrap();
6166 assert!((n.d - 4.0).abs() < 1e-9, "ztst:18 — 1.5+2.5=4.0");
6167 }
6168
6169 /// `Test/C01arith.ztst:24-26` — `7.5 % 2.5` = 0.0 (float modulo).
6170 #[test]
6171 fn zsh_corpus_float_modulo_exact_division() {
6172 let _g = crate::test_util::global_state_lock();
6173 let n = matheval("7.5 % 2.5").unwrap();
6174 assert!(n.d.abs() < 1e-9, "ztst:24 — 7.5%2.5=0.0");
6175 }
6176
6177 /// `Test/C01arith.ztst:46-50` — full zsh precedence chain.
6178 /// `4 - - 3 * 7 << 1 & 7 ^ 1 | 16 ** 2` = 1591 under default
6179 /// (non-C-precedence) zsh.
6180 #[test]
6181 fn zsh_corpus_full_zsh_precedence_chain() {
6182 let _g = crate::test_util::global_state_lock();
6183 let n = mathevali("4 - - 3 * 7 << 1 & 7 ^ 1 | 16 ** 2").unwrap();
6184 assert_eq!(n, 1591, "ztst:50 — default zsh precedence = 1591");
6185 }
6186
6187 // ═══════════════════════════════════════════════════════════════════
6188 // C-parity tests pinning Src/math.c output-format + lastbase
6189 // accessor helpers.
6190 // ═══════════════════════════════════════════════════════════════════
6191
6192 /// `outputradix()` returns the current `$OUTPUT_RADIX` value.
6193 /// C: reads the global `outputradix` int.
6194 #[test]
6195 fn outputradix_returns_int_no_panic() {
6196 let _g = crate::test_util::global_state_lock();
6197 let _r = outputradix();
6198 }
6199
6200 /// `outputunderscore()` returns the current digit-group setting.
6201 #[test]
6202 fn outputunderscore_returns_int_no_panic() {
6203 let _g = crate::test_util::global_state_lock();
6204 let _r = outputunderscore();
6205 }
6206
6207 /// `reset_output_format()` is a no-panic clearing call.
6208 #[test]
6209 fn reset_output_format_no_panic() {
6210 let _g = crate::test_util::global_state_lock();
6211 reset_output_format();
6212 reset_output_format();
6213 }
6214
6215 /// `lastbase()` returns the integer base of the last math
6216 /// literal parsed (C's `lastbase` global).
6217 #[test]
6218 fn lastbase_returns_int_no_panic() {
6219 let _g = crate::test_util::global_state_lock();
6220 let _r = lastbase();
6221 }
6222
6223 /// `set_lastbase(N)` then `lastbase()` returns N.
6224 #[test]
6225 fn set_lastbase_round_trips() {
6226 let _g = crate::test_util::global_state_lock();
6227 let saved = lastbase();
6228 set_lastbase(16);
6229 assert_eq!(lastbase(), 16);
6230 set_lastbase(saved);
6231 }
6232
6233 /// `m_noeval_set(0)` then `m_noeval()` returns 0.
6234 #[test]
6235 fn m_noeval_default_is_zero() {
6236 let _g = crate::test_util::global_state_lock();
6237 m_noeval_set(0);
6238 assert_eq!(m_noeval(), 0);
6239 }
6240
6241 /// `m_noeval_set(N)` round-trips through getter.
6242 #[test]
6243 fn m_noeval_set_round_trips() {
6244 let _g = crate::test_util::global_state_lock();
6245 let saved = m_noeval();
6246 m_noeval_set(3);
6247 assert_eq!(m_noeval(), 3);
6248 m_noeval_set(saved);
6249 }
6250
6251 // ═══════════════════════════════════════════════════════════════════
6252 // Additional C-parity tests for Src/math.c matheval + mathevali +
6253 // outputradix / outputunderscore / reset_output_format.
6254 // ═══════════════════════════════════════════════════════════════════
6255
6256 /// c:1491 — `matheval("")` returns MN_INTEGER 0.
6257 #[test]
6258 fn matheval_empty_returns_integer_zero() {
6259 let _g = crate::test_util::global_state_lock();
6260 let r = matheval("").expect("empty must succeed");
6261 assert_eq!(r.l, 0);
6262 assert_eq!(r.type_, MN_INTEGER);
6263 }
6264
6265 /// c:1489 — Nularg-prefixed expression is treated as the suffix.
6266 /// `Nularg + "42"` → 42.
6267 #[test]
6268 fn matheval_nularg_prefix_skipped() {
6269 let _g = crate::test_util::global_state_lock();
6270 let s = format!("{}42", Nularg);
6271 let r = matheval(&s).expect("must succeed");
6272 assert_eq!(r.l, 42, "Nularg prefix stripped, '42' evaluated");
6273 }
6274
6275 /// c:1480 — `matheval("1+2")` returns 3.
6276 #[test]
6277 fn matheval_basic_addition() {
6278 let _g = crate::test_util::global_state_lock();
6279 let r = matheval("1+2").expect("must succeed");
6280 assert_eq!(r.l, 3);
6281 }
6282
6283 /// c:1480 — `matheval("10*5")` returns 50.
6284 #[test]
6285 fn matheval_basic_multiplication() {
6286 let _g = crate::test_util::global_state_lock();
6287 let r = matheval("10*5").expect("must succeed");
6288 assert_eq!(r.l, 50);
6289 }
6290
6291 /// c:1505 — `mathevali("3+4")` returns 7 (integer-coerce).
6292 #[test]
6293 fn mathevali_integer_result() {
6294 let _g = crate::test_util::global_state_lock();
6295 let r = mathevali("3+4").expect("must succeed");
6296 assert_eq!(r, 7);
6297 }
6298
6299 /// c:1505 — `mathevali("3.7")` truncates to 3 (MN_FLOAT → i64).
6300 #[test]
6301 fn mathevali_float_truncates() {
6302 let _g = crate::test_util::global_state_lock();
6303 let r = mathevali("3.7").expect("must succeed");
6304 assert_eq!(r, 3, "float must truncate (cast to i64)");
6305 }
6306
6307 /// c:1505 — `mathevali("-2.7")` truncates toward zero → -2.
6308 #[test]
6309 fn mathevali_negative_float_truncates_toward_zero() {
6310 let _g = crate::test_util::global_state_lock();
6311 let r = mathevali("-2.7").expect("must succeed");
6312 assert_eq!(r, -2, "-2.7 truncates to -2 (toward zero, not -3)");
6313 }
6314
6315 /// c:898 — `reset_output_format()` clears both radix + underscore.
6316 #[test]
6317 fn reset_output_format_clears_both_state() {
6318 let _g = crate::test_util::global_state_lock();
6319 reset_output_format();
6320 assert_eq!(outputradix(), 0);
6321 assert_eq!(outputunderscore(), 0);
6322 }
6323
6324 /// c:889 — `outputradix` is deterministic right after reset.
6325 #[test]
6326 fn outputradix_zero_after_reset() {
6327 let _g = crate::test_util::global_state_lock();
6328 reset_output_format();
6329 for _ in 0..5 {
6330 assert_eq!(outputradix(), 0);
6331 }
6332 }
6333
6334 /// c:1049 — `lastbase` accessor returns valid base value.
6335 #[test]
6336 fn lastbase_accessor_returns_value() {
6337 let _g = crate::test_util::global_state_lock();
6338 let saved = lastbase();
6339 set_lastbase(16);
6340 assert_eq!(lastbase(), 16);
6341 set_lastbase(saved);
6342 }
6343
6344 /// c:1061 — `set_lastbase(8)` then `lastbase()` returns 8.
6345 #[test]
6346 fn set_lastbase_round_trips_pin() {
6347 let _g = crate::test_util::global_state_lock();
6348 let saved = lastbase();
6349 set_lastbase(8);
6350 assert_eq!(lastbase(), 8);
6351 set_lastbase(saved);
6352 }
6353
6354 // ═══════════════════════════════════════════════════════════════════
6355 // Additional C-parity tests for Src/math.c
6356 // c:889 outputradix / c:894 outputunderscore / c:901 reset_output_format
6357 // c:1022 m_noeval / c:1049 lastbase / c:2952 matheval / c:2995 mathevali
6358 // ═══════════════════════════════════════════════════════════════════
6359
6360 /// c:889 — `outputradix` returns i32 (compile-time type pin).
6361 #[test]
6362 fn outputradix_returns_i32_type() {
6363 let _g = crate::test_util::global_state_lock();
6364 let _: i32 = outputradix();
6365 }
6366
6367 /// c:894 — `outputunderscore` returns i32 (compile-time type pin).
6368 #[test]
6369 fn outputunderscore_returns_i32_type() {
6370 let _g = crate::test_util::global_state_lock();
6371 let _: i32 = outputunderscore();
6372 }
6373
6374 /// c:1022 — `m_noeval` returns i32 (compile-time type pin).
6375 #[test]
6376 fn m_noeval_returns_i32_type() {
6377 let _g = crate::test_util::global_state_lock();
6378 let _: i32 = m_noeval();
6379 }
6380
6381 /// c:1022 + c:1029 — `m_noeval` set/get round-trip preserves value.
6382 #[test]
6383 fn m_noeval_set_get_round_trip() {
6384 let _g = crate::test_util::global_state_lock();
6385 let saved = m_noeval();
6386 m_noeval_set(42);
6387 assert_eq!(m_noeval(), 42, "m_noeval round-trips");
6388 m_noeval_set(saved);
6389 }
6390
6391 /// c:1049 — `lastbase` returns i32 (compile-time type pin).
6392 #[test]
6393 fn lastbase_returns_i32_type() {
6394 let _g = crate::test_util::global_state_lock();
6395 let _: i32 = lastbase();
6396 }
6397
6398 /// c:2952 — `matheval("")` empty returns Result<mnumber, String> type.
6399 #[test]
6400 fn matheval_returns_result_type() {
6401 let _g = crate::test_util::global_state_lock();
6402 let _: Result<mnumber, String> = matheval("");
6403 }
6404
6405 /// c:2995 — `mathevali("")` empty returns Result<i64, String>.
6406 #[test]
6407 fn mathevali_returns_result_i64_type() {
6408 let _g = crate::test_util::global_state_lock();
6409 let _: Result<i64, String> = mathevali("");
6410 }
6411
6412 /// c:2952 — `matheval("0")` returns Ok with l=0.
6413 #[test]
6414 fn matheval_zero_returns_ok_zero() {
6415 let _g = crate::test_util::global_state_lock();
6416 let r = matheval("0").expect("0 must parse");
6417 assert_eq!(r.l, 0, "matheval('0').l = 0");
6418 }
6419
6420 /// c:2995 — `mathevali("1+1")` returns Ok(2).
6421 #[test]
6422 fn mathevali_simple_addition_returns_two() {
6423 let _g = crate::test_util::global_state_lock();
6424 let r = mathevali("1+1").expect("1+1 must parse");
6425 assert_eq!(r, 2, "1+1 must equal 2");
6426 }
6427
6428 /// c:2952 — `matheval` is pure for the same input (no side effects).
6429 #[test]
6430 fn matheval_is_pure_for_constants() {
6431 let _g = crate::test_util::global_state_lock();
6432 for s in ["0", "1", "42", "100"] {
6433 let first = matheval(s).map(|r| r.l).unwrap_or(0);
6434 for _ in 0..3 {
6435 let r = matheval(s).map(|r| r.l).unwrap_or(0);
6436 assert_eq!(r, first, "matheval({:?}) must be pure", s);
6437 }
6438 }
6439 }
6440
6441 /// c:2995 — `mathevali` accepts an undefined identifier as 0/parameter
6442 /// lookup per zsh math semantics (unset → 0); pin the deterministic
6443 /// fallback behavior rather than expecting Err. C body c:3030+
6444 /// passes unknown idents through `getmathparam` which returns 0 for
6445 /// unset names by zsh-default contract.
6446 #[test]
6447 fn mathevali_undefined_ident_deterministic() {
6448 let _g = crate::test_util::global_state_lock();
6449 let first = mathevali("__never_real_var_xyz__");
6450 for _ in 0..3 {
6451 assert_eq!(
6452 mathevali("__never_real_var_xyz__"),
6453 first,
6454 "mathevali on undefined ident must be deterministic"
6455 );
6456 }
6457 }
6458
6459 // ═══════════════════════════════════════════════════════════════════
6460 // Regression pins for setmathvar subscripted-lvalue writes.
6461 //
6462 // Prior to commit 2026-05-29, setmathvar stripped the subscript
6463 // before calling setnparam, so `(( h[k]++ ))` wrote to the bare
6464 // base name and wiped the assoc/array. C's setmathvar at
6465 // Src/math.c:1004 passes the FULL `mvp->lval` (subscript and
6466 // all) to setnparam — fixed by routing subscripted writes
6467 // through assignsparam after pre-evaluating numeric subscripts
6468 // via matheval.
6469 // ═══════════════════════════════════════════════════════════════════
6470
6471 /// Helper — read assoc element directly out of the hashed-storage
6472 /// backing map. `getsparam("h[k]")` doesn't resolve the subscript
6473 /// form in our port; the canonical hash-element read goes through
6474 /// expansion (subst), so these unit tests poke the storage map
6475 /// the same way `assignsparam`'s subscript-write path does.
6476 fn assoc_read(base: &str, key: &str) -> Option<String> {
6477 let m = crate::ported::params::paramtab_hashed_storage()
6478 .lock()
6479 .ok()?;
6480 m.get(base).and_then(|map| map.get(key).cloned())
6481 }
6482
6483 /// c:Src/math.c:1004 — `(( assoc[key]++ ))` must mutate the
6484 /// hash element, not the bare assoc param. Bug: silent no-op
6485 /// (counts[apple] stayed at 10).
6486 #[test]
6487 fn setmathvar_assoc_post_increment_mutates_hash_element() {
6488 let _g = crate::test_util::global_state_lock();
6489 crate::ported::params::unsetparam("counts");
6490 // Create assoc with apple=10.
6491 let _ = crate::ported::params::assignsparam("counts[apple]", "10", 0);
6492 // (( counts[apple]++ )) → read 10, write 11.
6493 let _ = setmathvar(
6494 "counts[apple]",
6495 mnumber {
6496 l: 11,
6497 d: 0.0,
6498 type_: MN_INTEGER,
6499 },
6500 );
6501 assert_eq!(
6502 assoc_read("counts", "apple"),
6503 Some("11".to_string()),
6504 "(( counts[apple]++ )) must mutate the hash element",
6505 );
6506 crate::ported::params::unsetparam("counts");
6507 }
6508
6509 /// c:Src/math.c:1004 — `(( assoc[key] = N ))` on an existing
6510 /// assoc creates/updates the slot without wiping siblings.
6511 #[test]
6512 fn setmathvar_assoc_assign_creates_slot_preserving_siblings() {
6513 let _g = crate::test_util::global_state_lock();
6514 crate::ported::params::unsetparam("h");
6515 let _ = crate::ported::params::assignsparam("h[a]", "1", 0);
6516 let _ = crate::ported::params::assignsparam("h[b]", "2", 0);
6517 let _ = setmathvar(
6518 "h[c]",
6519 mnumber {
6520 l: 99,
6521 d: 0.0,
6522 type_: MN_INTEGER,
6523 },
6524 );
6525 assert_eq!(
6526 assoc_read("h", "a"),
6527 Some("1".to_string()),
6528 "sibling h[a] preserved",
6529 );
6530 assert_eq!(
6531 assoc_read("h", "b"),
6532 Some("2".to_string()),
6533 "sibling h[b] preserved",
6534 );
6535 assert_eq!(
6536 assoc_read("h", "c"),
6537 Some("99".to_string()),
6538 "h[c] created with assigned value",
6539 );
6540 crate::ported::params::unsetparam("h");
6541 }
6542
6543 /// c:Src/math.c:1004 — `(( arr[i] = N ))` on indexed array
6544 /// must write element i, not wipe the array and replace with
6545 /// a scalar.
6546 #[test]
6547 fn setmathvar_indexed_array_assign_writes_element() {
6548 let _g = crate::test_util::global_state_lock();
6549 crate::ported::params::unsetparam("arr");
6550 let _ = crate::ported::params::assignaparam(
6551 "arr",
6552 vec!["10".to_string(), "20".to_string(), "30".to_string()],
6553 0,
6554 );
6555 let _ = setmathvar(
6556 "arr[2]",
6557 mnumber {
6558 l: 99,
6559 d: 0.0,
6560 type_: MN_INTEGER,
6561 },
6562 );
6563 assert_eq!(
6564 crate::ported::params::getaparam("arr"),
6565 Some(vec!["10".to_string(), "99".to_string(), "30".to_string()]),
6566 "(( arr[2]=99 )) must write slot 2 only",
6567 );
6568 crate::ported::params::unsetparam("arr");
6569 }
6570
6571 /// c:Src/math.c:1004 + Src/params.c:1367 — `(( arr[i + 1] = N ))`
6572 /// inside math context: subscript body is a math expression and
6573 /// must be evaluated. Pinned because the previous Rust port
6574 /// passed "arr[i + 1]" verbatim and assignsparam's i64-parse on
6575 /// the body failed, auto-vivifying as PM_HASHED.
6576 #[test]
6577 fn setmathvar_array_with_math_subscript_pre_evaluates_index() {
6578 let _g = crate::test_util::global_state_lock();
6579 crate::ported::params::unsetparam("arr");
6580 crate::ported::params::unsetparam("i");
6581 let _ = crate::ported::params::setiparam("i", 2);
6582 let _ = crate::ported::params::assignaparam(
6583 "arr",
6584 vec![
6585 "a".to_string(),
6586 "b".to_string(),
6587 "c".to_string(),
6588 "d".to_string(),
6589 ],
6590 0,
6591 );
6592 // arr[i + 1] → arr[3] after matheval.
6593 let _ = setmathvar(
6594 "arr[i + 1]",
6595 mnumber {
6596 l: 77,
6597 d: 0.0,
6598 type_: MN_INTEGER,
6599 },
6600 );
6601 let got = crate::ported::params::getaparam("arr");
6602 assert_eq!(
6603 got.as_ref().and_then(|v| v.get(2)).map(|s| s.as_str()),
6604 Some("77"),
6605 "arr[i+1] with i=2 must write slot 3",
6606 );
6607 crate::ported::params::unsetparam("arr");
6608 crate::ported::params::unsetparam("i");
6609 }
6610
6611 /// c:Src/math.c:1004 — chained `(( h[k]++ ))` calls compound:
6612 /// three increments on a fresh slot yield 3, not 1.
6613 #[test]
6614 fn setmathvar_assoc_three_increments_compound_to_three() {
6615 let _g = crate::test_util::global_state_lock();
6616 crate::ported::params::unsetparam("hc");
6617 let _ = crate::ported::params::assignsparam("hc[x]", "0", 0);
6618 for _ in 0..3 {
6619 // Read current then write read+1 — like (( hc[x]++ )).
6620 let cur = assoc_read("hc", "x")
6621 .and_then(|s| s.parse::<i64>().ok())
6622 .unwrap_or(0);
6623 let _ = setmathvar(
6624 "hc[x]",
6625 mnumber {
6626 l: cur + 1,
6627 d: 0.0,
6628 type_: MN_INTEGER,
6629 },
6630 );
6631 }
6632 assert_eq!(
6633 assoc_read("hc", "x"),
6634 Some("3".to_string()),
6635 "three (( hc[x]++ )) must compound to 3",
6636 );
6637 crate::ported::params::unsetparam("hc");
6638 }
6639
6640 /// c:Src/math.c:1004 — `(( h[fresh]++ ))` auto-vivifies the
6641 /// slot to value 1 (read NULL → 0, write 0+1).
6642 #[test]
6643 fn setmathvar_assoc_post_increment_on_unset_slot_creates_with_one() {
6644 let _g = crate::test_util::global_state_lock();
6645 crate::ported::params::unsetparam("hv");
6646 // Create the assoc first (typeset -A hv).
6647 let _ = crate::ported::params::assignsparam("hv[seed]", "0", 0);
6648 // (( hv[fresh]++ )) — fresh slot should become 1.
6649 let _ = setmathvar(
6650 "hv[fresh]",
6651 mnumber {
6652 l: 1,
6653 d: 0.0,
6654 type_: MN_INTEGER,
6655 },
6656 );
6657 assert_eq!(
6658 assoc_read("hv", "fresh"),
6659 Some("1".to_string()),
6660 "(( hv[fresh]++ )) on unset slot must create with 1",
6661 );
6662 crate::ported::params::unsetparam("hv");
6663 }
6664
6665 /// c:Src/math.c:1002 — NO_EXEC mode: setmathvar returns val
6666 /// without any param-table mutation, even for subscripted
6667 /// names. Pinned because the new subscript branch must respect
6668 /// the noeval guard added by the previous code path.
6669 #[test]
6670 fn setmathvar_subscript_respects_noeval_guard() {
6671 let _g = crate::test_util::global_state_lock();
6672 crate::ported::params::unsetparam("nev");
6673 let _ = crate::ported::params::assignsparam("nev[k]", "1", 0);
6674 M_NOEVAL.with(|n| n.set(1));
6675 let v = mnumber {
6676 l: 999,
6677 d: 0.0,
6678 type_: MN_INTEGER,
6679 };
6680 let ret = setmathvar("nev[k]", v);
6681 M_NOEVAL.with(|n| n.set(0));
6682 assert_eq!(ret.l, 999, "noeval returns val unchanged");
6683 assert_eq!(
6684 assoc_read("nev", "k"),
6685 Some("1".to_string()),
6686 "noeval must suppress the paramtab write",
6687 );
6688 crate::ported::params::unsetparam("nev");
6689 }
6690}