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