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