Skip to main content

zsh/extensions/
arith_compiler.rs

1//! ArithCompiler — lowers zsh arithmetic expressions
2//! (`$((...))`) into fusevm bytecodes. Used by `ZshCompiler` (in
3//! `compile_zsh.rs`).
4//!
5//! **zshrs-original infrastructure with C-zsh-derived semantics.**
6//! C zsh has no arithmetic compiler — `Src/math.c::matheval()`
7//! tokenizes and evaluates in one pass via `getmathparam()` /
8//! `mathevall()`. zshrs splits compilation from evaluation: the
9//! tokenizer here matches `zzlex()` / `mathlex()` from
10//! Src/math.c, but instead of pushing onto the math eval stack
11//! we emit fusevm Ops which the JIT can specialize.
12
13use fusevm::{ChunkBuilder, Op, Value};
14use std::collections::HashMap;
15
16// ═══════════════════════════════════════════════════════════════════════════
17// ArithCompiler — lowers arithmetic expressions → fusevm bytecodes
18// ═══════════════════════════════════════════════════════════════════════════
19
20/// Arithmetic expression compiler.
21///
22/// Takes a zsh arithmetic expression (the content inside
23/// `$((...))`) and emits fusevm bytecodes that compute the result.
24///
25/// **Tokenizer port**: same lexer shape as `zzlex()` /
26/// `mathlex()` from Src/math.c. **Emit step**: zshrs-original —
27/// C zsh evaluates inline via `mathevall()` (Src/math.c) and has
28/// no compile-then-run path.
29pub struct ArithCompiler<'a> {
30    /// `input` field.
31    pub input: &'a str,
32    /// `pos` field.
33    pub pos: usize,
34    /// `builder` field.
35    pub builder: ChunkBuilder,
36    /// Variable name → slot index
37    pub slots: HashMap<String, u16>,
38    /// `next_slot` field.
39    pub next_slot: u16,
40    /// Names this expression ASSIGNS to (`x = …`, `x += …`, `x++`, `--x`).
41    ///
42    /// c:Src/math.c:1364-1372 — only an `OP_E2`/`OP_E2IO` operator reaches
43    /// `setmathvar`; a name that is merely READ never gets written back.
44    /// `compile_arith_str` uses this to write back exactly the names C
45    /// would, instead of every identifier the expression mentions.
46    pub assigned: std::collections::HashSet<String>,
47}
48
49// Token types matching the `MTYPE_*` enum from Src/math.c.
50// Each variant corresponds to one of the operators / operand
51// kinds the C source's `zzlex()` produces.
52#[derive(Debug, Clone, Copy, PartialEq)]
53enum Tok {
54    Num(i64),
55    Float(f64),
56    Ident,
57    Plus,
58    Minus,
59    Mul,
60    Div,
61    Mod,
62    Pow,
63    BitAnd,
64    BitOr,
65    BitXor,
66    BitNot,
67    Shl,
68    Shr,
69    LogAnd,
70    LogOr,
71    LogNot,
72    Eq,
73    Neq,
74    Lt,
75    Gt,
76    Leq,
77    Geq,
78    Assign,
79    PlusAssign,
80    MinusAssign,
81    MulAssign,
82    DivAssign,
83    ModAssign,
84    // c:Src/math.c:135-142 ANDEQ/XOREQ/OREQ/SHLEFTEQ/SHRIGHTEQ and
85    // c:Src/math.c:51/40-42 POWEREQ/DANDEQ/DOREQ/DXOREQ — the compound
86    // assignments the old tokenizer had no variants for, so
87    // `compile_arith` had to route every expression containing them
88    // to the runtime evaluator.
89    AndAssign,
90    XorAssign,
91    OrAssign,
92    ShlAssign,
93    ShrAssign,
94    PowAssign,
95    LogAndAssign,
96    LogOrAssign,
97    LogXorAssign,
98    /// c:Src/math.c:126 `DXOR` — `^^`, logical xor. Not a C operator.
99    LogXor,
100    PreInc,
101    PreDec,
102    PostInc,
103    PostDec,
104    LParen,
105    RParen,
106    Comma,
107    Quest,
108    Colon,
109    Eoi,
110}
111
112impl<'a> ArithCompiler<'a> {
113    /// `new` — see implementation.
114    pub fn new(input: &'a str) -> Self {
115        Self {
116            input,
117            pos: 0,
118            builder: ChunkBuilder::new(),
119            slots: HashMap::new(),
120            next_slot: 0,
121            assigned: std::collections::HashSet::new(),
122        }
123    }
124
125    /// Compile the arithmetic expression to fusevm bytecodes.
126    /// Returns the compiled chunk.
127    pub fn compile(mut self) -> fusevm::Chunk {
128        self.builder.set_source("$((...))");
129        self.builder.emit(Op::PushFrame, 0);
130        self.expr();
131        self.builder.emit(Op::ReturnValue, 0);
132        self.builder.build()
133    }
134
135    /// Get or allocate a slot for a variable name.
136    /// c:Src/math.c:1364-1372 — an assignment operator stores through
137    /// `setmathvar` at its own position in the expression, so a store
138    /// that ran before an error survives it (`(( y = 3, x = 5/0 ))` keeps
139    /// y) and one reached after it never happens (c:1161 `if (errflag)
140    /// return;`, enforced by BUILTIN_SET_MATH_VAR).
141    ///
142    /// Stack: `[value]` → `[stored value]`. c:Src/math.c:1370-1371 —
143    /// `c = setmathvar(mvp, c); push(c, …)`: the value of an assignment
144    /// is what `setmathvar` returns, which c:1014-1030 converts to the
145    /// TYPE of the parameter assigned (`typeset -i x; (( y = (x = 1.7) ))`
146    /// gives y 1, not 1.7). `slot` is scratch only.
147    ///
148    /// `pval` is true when the operator READ the lvalue first (`op=`,
149    /// `++`, `--`): c:Src/math.c:975-993 then stores through the cached
150    /// value handle instead of setnparam (no WARN_* report, no retyping).
151    fn emit_setmathvar(&mut self, name: &str, slot: u16, pval: bool) {
152        self.builder.emit(Op::SetSlot(slot), 0);
153        let name_const = self.builder.add_constant(Value::str(name));
154        self.builder.emit(Op::LoadConst(name_const), 0);
155        self.builder.emit(Op::GetSlot(slot), 0);
156        self.builder.emit(Op::LoadInt(pval as i64), 0);
157        self.builder.emit(
158            Op::CallBuiltin(crate::vm_helper::BUILTIN_SET_MATH_VAR, 3),
159            0,
160        );
161    }
162
163    /// Read a parameter's numeric value where an operator consumes it.
164    /// c:Src/math.c:1631 pushes an `ID` as `MN_UNSET` with no value; the
165    /// value is fetched by `getmathparam` only when an operator pops it
166    /// (c:936 `pop`, c:1375 `op`, c:1462 `bop`, c:1647 `QUEST`). So a
167    /// name that is only ASSIGNED (`x=/bar; (( x = 32 ))`) is never read,
168    /// and neither is one in a branch not taken (`(( 0 && y ))`,
169    /// `(( 1 ? 2 : y ))`) — a non-numeric value there is no error.
170    /// Reading in operand position is equivalent to C's deferred read as
171    /// long as the operand's own name is not modified before its operator
172    /// runs; `arith_uncompilable_reason` sends those expressions to the
173    /// runtime evaluator. Stack: `[]` → `[value]`.
174    fn emit_getmathparam(&mut self, name: &str) {
175        let name_const = self.builder.add_constant(Value::str(name));
176        self.builder.emit(Op::LoadConst(name_const), 0);
177        self.builder.emit(
178            Op::CallBuiltin(crate::vm_helper::BUILTIN_GET_MATH_VAR, 1),
179            0,
180        );
181    }
182
183    pub fn slot_for(&mut self, name: &str) -> u16 {
184        if let Some(&slot) = self.slots.get(name) {
185            return slot;
186        }
187        let slot = self.next_slot;
188        self.next_slot += 1;
189        self.slots.insert(name.to_string(), slot);
190        slot
191    }
192
193    /// Walk the input string and collect all identifier names that appear.
194    /// Used by `compile_arith_inline` to pre-load values from
195    /// `executor.variables` and to know which slots to write back after.
196    /// Excludes language keywords and numeric literals.
197    pub fn collect_identifiers(&self, expr: &str) -> Vec<String> {
198        let bytes = expr.as_bytes();
199        let mut names: Vec<String> = Vec::new();
200        let mut i = 0;
201        while i < bytes.len() {
202            let b = bytes[i];
203            // Strip an optional leading `$` (and `${...}` braces) — `(( $1 ))`,
204            // `(( $x ))`, `(( ${count} ))` should all pre-load just like
205            // `(( x ))`.
206            let with_dollar = b == b'$';
207            if with_dollar {
208                if i + 1 >= bytes.len() {
209                    i += 1;
210                    continue;
211                }
212                if bytes[i + 1] == b'{' {
213                    i += 2;
214                    let start = i;
215                    while i < bytes.len() && bytes[i] != b'}' {
216                        i += 1;
217                    }
218                    let name = expr[start..i].to_string();
219                    if !name.is_empty() && !names.contains(&name) {
220                        names.push(name);
221                    }
222                    if i < bytes.len() {
223                        i += 1; // skip `}`
224                    }
225                    continue;
226                }
227                i += 1;
228                let start = i;
229                if bytes
230                    .get(i)
231                    .copied()
232                    .map(|c| c.is_ascii_digit())
233                    .unwrap_or(false)
234                {
235                    while i < bytes.len() && bytes[i].is_ascii_digit() {
236                        i += 1;
237                    }
238                    let name = expr[start..i].to_string();
239                    if !names.contains(&name) {
240                        names.push(name);
241                    }
242                    continue;
243                }
244            }
245            if b.is_ascii_alphabetic() || b == b'_' || (with_dollar && i < bytes.len()) {
246                let start = i;
247                while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
248                    i += 1;
249                }
250                let name = expr[start..i].to_string();
251                if !name.is_empty() && !names.contains(&name) {
252                    names.push(name);
253                }
254            } else {
255                i += 1;
256            }
257        }
258        names
259    }
260
261    // ── Tokenizer ──
262
263    fn skip_whitespace(&mut self) {
264        while self.pos < self.input.len() {
265            let b = self.input.as_bytes()[self.pos];
266            if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' {
267                self.pos += 1;
268            } else {
269                break;
270            }
271        }
272    }
273
274    fn peek_char(&self) -> Option<u8> {
275        self.input.as_bytes().get(self.pos).copied()
276    }
277
278    fn next_char(&mut self) -> Option<u8> {
279        let c = self.input.as_bytes().get(self.pos).copied();
280        if c.is_some() {
281            self.pos += 1;
282        }
283        c
284    }
285
286    fn read_ident(&mut self) -> String {
287        let start = self.pos;
288        while self.pos < self.input.len() {
289            let b = self.input.as_bytes()[self.pos];
290            if b.is_ascii_alphanumeric() || b == b'_' {
291                self.pos += 1;
292            } else {
293                break;
294            }
295        }
296        self.input[start..self.pos].to_string()
297    }
298
299    fn read_number(&mut self) -> Tok {
300        let start = self.pos;
301
302        // Handle hex: 0x... (c:Src/math.c lexconstant — 0x/0X prefix
303        // always parses as base 16 regardless of OCTALZEROES.)
304        if self.pos + 1 < self.input.len()
305            && self.input.as_bytes()[self.pos] == b'0'
306            && (self.input.as_bytes()[self.pos + 1] == b'x'
307                || self.input.as_bytes()[self.pos + 1] == b'X')
308        {
309            self.pos += 2;
310            while self.pos < self.input.len() && self.input.as_bytes()[self.pos].is_ascii_hexdigit()
311            {
312                self.pos += 1;
313            }
314            let val = i64::from_str_radix(&self.input[start + 2..self.pos], 16).unwrap_or(0);
315            // c:Src/math.c lexconstant — set `lastbase = 16` for the
316            // PM_INTEGER pm.base inheritance path in assignsparam.
317            // Without this `(( X = 0xff )); echo \$X` printed 255
318            // instead of zsh's `16#FF`.
319            crate::ported::math::set_lastbase(16);
320            return Tok::Num(val);
321        }
322
323        // Decimal integer (and possibly base-N or octal). Greedy: walk
324        // all digits, then check for `#` (base-N) or `.` (float).
325        while self.pos < self.input.len() && self.input.as_bytes()[self.pos].is_ascii_digit() {
326            self.pos += 1;
327        }
328
329        // c:Src/math.c — `N#digits` base-N literal. The leading
330        // number is the base (2..=36); the digits after `#` use that
331        // base. zsh accepts `#` (signed result) and `##` (unsigned),
332        // but for the arith_compiler scope just `#` is sufficient.
333        //
334        // !!! DASH-STRICT GATE (no C counterpart) !!! real dash/ash have POSIX
335        // arithmetic ONLY (decimal, `0` octal, `0x` hex) — `base#num` is a
336        // zsh/bash/ksh extension they REJECT ("expecting EOF: 16#ff"). Under
337        // `zshrs --dash`/`--ash`, do NOT consume the `#` as a base separator;
338        // leave the decimal parsed and let the stray `#` surface as an
339        // unexpected token, matching the real shell's error. bash-family --sh
340        // and --ksh keep accepting base#num (their real shells do too).
341        if self.pos < self.input.len()
342            && self.input.as_bytes()[self.pos] == b'#'
343            && !crate::dash_mode::dash_strict()
344        {
345            let base_str = &self.input[start..self.pos];
346            if let Ok(base) = base_str.parse::<u32>() {
347                if (2..=36).contains(&base) {
348                    self.pos += 1; // skip `#`
349                    let digit_start = self.pos;
350                    while self.pos < self.input.len() {
351                        let b = self.input.as_bytes()[self.pos];
352                        let in_base = if base <= 10 {
353                            b.is_ascii_digit() && (b - b'0') < base as u8
354                        } else {
355                            b.is_ascii_digit()
356                                || (b.is_ascii_alphabetic() && {
357                                    let v = if b.is_ascii_lowercase() {
358                                        b - b'a' + 10
359                                    } else {
360                                        b - b'A' + 10
361                                    };
362                                    (v as u32) < base
363                                })
364                        };
365                        if in_base {
366                            self.pos += 1;
367                        } else {
368                            break;
369                        }
370                    }
371                    let digits = &self.input[digit_start..self.pos];
372                    let val = i64::from_str_radix(digits, base).unwrap_or(0);
373                    // c:Src/math.c lexconstant — record the source
374                    // base so PM_INTEGER assignment can inherit it
375                    // for display formatting (`(( X = 2#1010 ));
376                    // echo \$X` → `2#1010`).
377                    crate::ported::math::set_lastbase(base as i32);
378                    return Tok::Num(val);
379                }
380            }
381        }
382
383        // c:Src/math.c — `010` (leading zero) is octal ONLY when
384        // OCTALZEROES is set; default off, so a leading-zero literal
385        // is decimal. Honour the option here.
386        let lex_octal = self.pos > start + 1
387            && self.input.as_bytes()[start] == b'0'
388            && self.input.as_bytes()[start + 1].is_ascii_digit()
389            && crate::ported::zsh_h::isset(crate::ported::zsh_h::OCTALZEROES);
390        if lex_octal {
391            let val = i64::from_str_radix(&self.input[start + 1..self.pos], 8).unwrap_or(0);
392            return Tok::Num(val);
393        }
394
395        // Float (after decimal-integer scan).
396        if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b'.' {
397            self.pos += 1;
398            while self.pos < self.input.len() && self.input.as_bytes()[self.pos].is_ascii_digit() {
399                self.pos += 1;
400            }
401            let val: f64 = self.input[start..self.pos].parse().unwrap_or(0.0);
402            return Tok::Float(val);
403        }
404
405        let val: i64 = self.input[start..self.pos].parse().unwrap_or(0);
406        Tok::Num(val)
407    }
408
409    fn next_tok(&mut self) -> (Tok, String) {
410        self.skip_whitespace();
411
412        let Some(c) = self.peek_char() else {
413            return (Tok::Eoi, String::new());
414        };
415
416        match c {
417            b'0'..=b'9' => {
418                let tok = self.read_number();
419                (tok, String::new())
420            }
421            b'a'..=b'z' | b'A'..=b'Z' | b'_' => {
422                let name = self.read_ident();
423                (Tok::Ident, name)
424            }
425            b'$' => {
426                // `$NAME` / `${NAME}` / `$N` in arithmetic: consume the `$`
427                // and read the var name as a normal identifier. zsh
428                // accepts both `$x` and `x` in `(( ))`; the value is loaded
429                // from the variable table either way (positional `$1`,
430                // `$2` are stored under those names too).
431                self.pos += 1;
432                if self.peek_char() == Some(b'{') {
433                    self.pos += 1;
434                    let mut name = String::new();
435                    while let Some(b) = self.peek_char() {
436                        if b == b'}' {
437                            self.pos += 1;
438                            break;
439                        }
440                        name.push(b as char);
441                        self.pos += 1;
442                    }
443                    (Tok::Ident, name)
444                } else if let Some(b) = self.peek_char() {
445                    if b.is_ascii_digit() {
446                        // Positional `$N` — read all digits as the name.
447                        let mut name = String::new();
448                        while let Some(d) = self.peek_char() {
449                            if !d.is_ascii_digit() {
450                                break;
451                            }
452                            name.push(d as char);
453                            self.pos += 1;
454                        }
455                        (Tok::Ident, name)
456                    } else if b.is_ascii_alphabetic() || b == b'_' {
457                        let name = self.read_ident();
458                        (Tok::Ident, name)
459                    } else {
460                        // `$` followed by special char — not a meaningful
461                        // arith form; emit zero.
462                        (Tok::Num(0), String::new())
463                    }
464                } else {
465                    (Tok::Num(0), String::new())
466                }
467            }
468            b'+' => {
469                self.pos += 1;
470                match self.peek_char() {
471                    // !!! DASH-STRICT GATE !!! `++`/`--` are C inc/dec operators
472                    // that POSIX arithmetic (dash/ash) lacks — they reject
473                    // `$((a++))`. Don't consume the second `+`: it becomes a
474                    // separate unary `+` with no operand → parse error, like
475                    // dash. `+=` is POSIX-valid and stays.
476                    Some(b'+') if !crate::dash_mode::dash_strict() => {
477                        self.pos += 1;
478                        (Tok::PreInc, String::new())
479                    }
480                    Some(b'=') => {
481                        self.pos += 1;
482                        (Tok::PlusAssign, String::new())
483                    }
484                    _ => (Tok::Plus, String::new()),
485                }
486            }
487            b'-' => {
488                self.pos += 1;
489                match self.peek_char() {
490                    Some(b'-') if !crate::dash_mode::dash_strict() => {
491                        self.pos += 1;
492                        (Tok::PreDec, String::new())
493                    }
494                    Some(b'=') => {
495                        self.pos += 1;
496                        (Tok::MinusAssign, String::new())
497                    }
498                    _ => (Tok::Minus, String::new()),
499                }
500            }
501            b'*' => {
502                self.pos += 1;
503                match self.peek_char() {
504                    Some(b'*') => {
505                        self.pos += 1;
506                        if self.peek_char() == Some(b'=') {
507                            self.pos += 1;
508                            // c:Src/math.c:51 POWEREQ — `a **= b` is
509                            // `a = a ** b`. Lexing it as MulAssign made
510                            // `a=2; (( a **= 3 ))` yield 6 instead of 8.
511                            (Tok::PowAssign, String::new())
512                        } else {
513                            (Tok::Pow, String::new())
514                        }
515                    }
516                    Some(b'=') => {
517                        self.pos += 1;
518                        (Tok::MulAssign, String::new())
519                    }
520                    _ => (Tok::Mul, String::new()),
521                }
522            }
523            b'/' => {
524                self.pos += 1;
525                if self.peek_char() == Some(b'=') {
526                    self.pos += 1;
527                    (Tok::DivAssign, String::new())
528                } else {
529                    (Tok::Div, String::new())
530                }
531            }
532            b'%' => {
533                self.pos += 1;
534                if self.peek_char() == Some(b'=') {
535                    self.pos += 1;
536                    (Tok::ModAssign, String::new())
537                } else {
538                    (Tok::Mod, String::new())
539                }
540            }
541            b'&' => {
542                self.pos += 1;
543                match self.peek_char() {
544                    Some(b'&') => {
545                        self.pos += 1;
546                        // c:Src/math.c:40 DANDEQ — `&&=`.
547                        if self.peek_char() == Some(b'=') {
548                            self.pos += 1;
549                            (Tok::LogAndAssign, String::new())
550                        } else {
551                            (Tok::LogAnd, String::new())
552                        }
553                    }
554                    Some(b'=') => {
555                        self.pos += 1;
556                        (Tok::AndAssign, String::new()) // c:35 ANDEQ
557                    }
558                    _ => (Tok::BitAnd, String::new()),
559                }
560            }
561            b'|' => {
562                self.pos += 1;
563                match self.peek_char() {
564                    Some(b'|') => {
565                        self.pos += 1;
566                        // c:Src/math.c:41 DOREQ — `||=`.
567                        if self.peek_char() == Some(b'=') {
568                            self.pos += 1;
569                            (Tok::LogOrAssign, String::new())
570                        } else {
571                            (Tok::LogOr, String::new())
572                        }
573                    }
574                    Some(b'=') => {
575                        self.pos += 1;
576                        (Tok::OrAssign, String::new()) // c:37 OREQ
577                    }
578                    _ => (Tok::BitOr, String::new()),
579                }
580            }
581            b'^' => {
582                self.pos += 1;
583                match self.peek_char() {
584                    Some(b'^') => {
585                        self.pos += 1;
586                        // c:Src/math.c:42 DXOREQ — `^^=`.
587                        if self.peek_char() == Some(b'=') {
588                            self.pos += 1;
589                            (Tok::LogXorAssign, String::new())
590                        } else {
591                            (Tok::LogXor, String::new()) // c:26 DXOR
592                        }
593                    }
594                    Some(b'=') => {
595                        self.pos += 1;
596                        (Tok::XorAssign, String::new()) // c:36 XOREQ
597                    }
598                    _ => (Tok::BitXor, String::new()),
599                }
600            }
601            b'~' => {
602                self.pos += 1;
603                (Tok::BitNot, String::new())
604            }
605            b'!' => {
606                self.pos += 1;
607                if self.peek_char() == Some(b'=') {
608                    self.pos += 1;
609                    (Tok::Neq, String::new())
610                } else {
611                    (Tok::LogNot, String::new())
612                }
613            }
614            b'<' => {
615                self.pos += 1;
616                match self.peek_char() {
617                    Some(b'<') => {
618                        self.pos += 1;
619                        // c:Src/math.c:38 SHLEFTEQ — `<<=`.
620                        if self.peek_char() == Some(b'=') {
621                            self.pos += 1;
622                            (Tok::ShlAssign, String::new())
623                        } else {
624                            (Tok::Shl, String::new())
625                        }
626                    }
627                    Some(b'=') => {
628                        self.pos += 1;
629                        (Tok::Leq, String::new())
630                    }
631                    _ => (Tok::Lt, String::new()),
632                }
633            }
634            b'>' => {
635                self.pos += 1;
636                match self.peek_char() {
637                    Some(b'>') => {
638                        self.pos += 1;
639                        // c:Src/math.c:39 SHRIGHTEQ — `>>=`.
640                        if self.peek_char() == Some(b'=') {
641                            self.pos += 1;
642                            (Tok::ShrAssign, String::new())
643                        } else {
644                            (Tok::Shr, String::new())
645                        }
646                    }
647                    Some(b'=') => {
648                        self.pos += 1;
649                        (Tok::Geq, String::new())
650                    }
651                    _ => (Tok::Gt, String::new()),
652                }
653            }
654            b'=' => {
655                self.pos += 1;
656                if self.peek_char() == Some(b'=') {
657                    self.pos += 1;
658                    (Tok::Eq, String::new())
659                } else {
660                    (Tok::Assign, String::new())
661                }
662            }
663            b'(' => {
664                self.pos += 1;
665                (Tok::LParen, String::new())
666            }
667            b')' => {
668                self.pos += 1;
669                (Tok::RParen, String::new())
670            }
671            b',' => {
672                self.pos += 1;
673                (Tok::Comma, String::new())
674            }
675            b'?' => {
676                self.pos += 1;
677                (Tok::Quest, String::new())
678            }
679            b':' => {
680                self.pos += 1;
681                (Tok::Colon, String::new())
682            }
683            _ => {
684                self.pos += 1;
685                (Tok::Eoi, String::new())
686            }
687        }
688    }
689
690    // ── Recursive descent → emit ops ──
691    //
692    // c:Src/math.c:220-273 `z_prec` — zsh's DEFAULT operator precedence,
693    // which is NOT C's. Reading the table tightest-binding first:
694    //
695    //   2  unary  ++ -- ! ~ +x -x
696    //   3  << >>          <- shifts bind TIGHTER than everything below
697    //   4  &
698    //   5  ^
699    //   6  |
700    //   7  **
701    //   8  * / %
702    //   9  + -
703    //  10  < > <= >=
704    //  11  == !=
705    //  12  &&
706    //  13  || ^^
707    //  14  ?     15  :
708    //  16  = += -= *= /= %= &= ^= |= <<= >>= &&= ||= ^^= **=
709    //  17  ,
710    //
711    // The cascade below is one function per level in that order. It used
712    // to be C's order (bitwise loosest, shifts between `+` and the
713    // comparisons), which is why `compile_arith` had to route every
714    // expression containing `& | ^ < > **` to the runtime evaluator: the
715    // compiled answer was wrong. c:Src/math.c:190 `c_prec` is the table
716    // zsh switches to under `setopt c_precedences`; `prec_tables_agree`
717    // below is what keeps that option honest.
718    /// `expr` — see implementation.
719    pub fn expr(&mut self) {
720        self.comma_expr();
721    }
722
723    /// c:Src/math.c z_prec[COMMA] = 17 — loosest binding. Each operand is
724    /// evaluated in order and all but the last discarded (c:1373-1377
725    /// `case COMMA: c = b`).
726    fn comma_expr(&mut self) {
727        self.assign_expr();
728        loop {
729            let (tok, _) = self.peek_tok();
730            if tok == Tok::Comma {
731                let _ = self.next_tok();
732                self.builder.emit(Op::Pop, 0);
733                self.assign_expr();
734            } else {
735                break;
736            }
737        }
738    }
739
740    /// c:Src/math.c z_prec[EQ..POWEREQ] = 16, `type[] = RL` — right-
741    /// associative, so the RHS recurses back into this level.
742    fn assign_expr(&mut self) {
743        let save_pos = self.pos;
744
745        self.skip_whitespace();
746        if let Some(c) = self.peek_char() {
747            if c.is_ascii_alphabetic() || c == b'_' {
748                let name = self.read_ident();
749                self.skip_whitespace();
750                let (tok, _) = self.peek_tok();
751                if tok == Tok::Assign {
752                    let _ = self.next_tok(); // consume =
753                    let slot = self.slot_for(&name);
754                    self.assigned.insert(name.clone());
755                    self.assign_expr();
756                    self.emit_setmathvar(&name, slot, false);
757                    return;
758                }
759                if let Some(binop) = compound_assign_op(tok) {
760                    let _ = self.next_tok(); // consume op=
761                    let slot = self.slot_for(&name);
762                    self.assigned.insert(name.clone());
763                    // c:Src/math.c:1373-1375 — `op()` pops the right
764                    // operand, THEN fetches the left one through
765                    // `getmathparam`, so the lvalue is read after the RHS
766                    // ran: `x=1; (( x += (x = 5) ))` is 10. The RHS waits
767                    // in `slot` while the lvalue loads.
768                    self.assign_expr();
769                    self.builder.emit(Op::SetSlot(slot), 0);
770                    self.emit_getmathparam(&name);
771                    self.builder.emit(Op::GetSlot(slot), 0);
772                    self.emit_binop(binop);
773                    self.emit_setmathvar(&name, slot, true);
774                    return;
775                }
776                // Not an assignment — rewind and re-parse as a value.
777                self.pos = save_pos;
778            }
779        }
780
781        self.ternary_expr();
782    }
783
784    fn peek_tok(&mut self) -> (Tok, String) {
785        let save = self.pos;
786        let tok = self.next_tok();
787        self.pos = save;
788        tok
789    }
790
791    /// c:Src/math.c z_prec[QUEST] = 14, z_prec[COLON] = 15.
792    fn ternary_expr(&mut self) {
793        self.logor_expr();
794        let (tok, _) = self.peek_tok();
795        if tok == Tok::Quest {
796            let _ = self.next_tok(); // consume ?
797            let else_jump = self.builder.emit(Op::JumpIfFalse(0), 0);
798            self.assign_expr(); // true branch
799            let (colon, _) = self.peek_tok();
800            let end_jump = self.builder.emit(Op::Jump(0), 0);
801            let else_target = self.builder.current_pos();
802            self.builder.patch_jump(else_jump, else_target);
803            if colon == Tok::Colon {
804                let _ = self.next_tok(); // consume :
805            }
806            self.assign_expr(); // false branch
807            let end_target = self.builder.current_pos();
808            self.builder.patch_jump(end_jump, end_target);
809        }
810    }
811
812    /// c:Src/math.c z_prec[DOR] = z_prec[DXOR] = 13 — `||` and `^^` share
813    /// a level. `||` short-circuits (c:1200 `BOOL`); `^^` cannot, since
814    /// both sides decide the answer.
815    fn logor_expr(&mut self) {
816        self.logand_expr();
817        loop {
818            let (tok, _) = self.peek_tok();
819            match tok {
820                Tok::LogOr => {
821                    let _ = self.next_tok();
822                    let skip = self.builder.emit(Op::JumpIfTrueKeep(0), 0);
823                    self.builder.emit(Op::Pop, 0);
824                    self.logand_expr();
825                    self.builder.patch_jump(skip, self.builder.current_pos());
826                }
827                Tok::LogXor => {
828                    let _ = self.next_tok();
829                    self.logand_expr();
830                    self.builder.emit(
831                        Op::CallBuiltin(crate::vm_helper::BUILTIN_ARITH_LOGXOR, 2),
832                        0,
833                    );
834                }
835                _ => break,
836            }
837        }
838    }
839
840    /// c:Src/math.c z_prec[DAND] = 12.
841    fn logand_expr(&mut self) {
842        self.equality_expr();
843        loop {
844            let (tok, _) = self.peek_tok();
845            if tok == Tok::LogAnd {
846                let _ = self.next_tok();
847                let skip = self.builder.emit(Op::JumpIfFalseKeep(0), 0);
848                self.builder.emit(Op::Pop, 0);
849                self.equality_expr();
850                self.builder.patch_jump(skip, self.builder.current_pos());
851            } else {
852                break;
853            }
854        }
855    }
856
857    /// c:Src/math.c z_prec[DEQ] = z_prec[NEQ] = 11.
858    fn equality_expr(&mut self) {
859        self.comparison_expr();
860        loop {
861            let (tok, _) = self.peek_tok();
862            match tok {
863                Tok::Eq => {
864                    let _ = self.next_tok();
865                    self.comparison_expr();
866                    self.builder.emit(Op::NumEq, 0);
867                }
868                Tok::Neq => {
869                    let _ = self.next_tok();
870                    self.comparison_expr();
871                    self.builder.emit(Op::NumNe, 0);
872                }
873                _ => break,
874            }
875        }
876    }
877
878    /// c:Src/math.c z_prec[LES..GEQ] = 10.
879    fn comparison_expr(&mut self) {
880        self.additive_expr();
881        loop {
882            let (tok, _) = self.peek_tok();
883            let op = match tok {
884                Tok::Lt => Op::NumLt,
885                Tok::Gt => Op::NumGt,
886                Tok::Leq => Op::NumLe,
887                Tok::Geq => Op::NumGe,
888                _ => break,
889            };
890            let _ = self.next_tok();
891            self.additive_expr();
892            self.builder.emit(op, 0);
893        }
894    }
895
896    /// c:Src/math.c z_prec[PLUS] = z_prec[MINUS] = 9.
897    fn additive_expr(&mut self) {
898        self.multiplicative_expr();
899        loop {
900            let (tok, _) = self.peek_tok();
901            let op = match tok {
902                Tok::Plus => Op::Add,
903                Tok::Minus => Op::Sub,
904                _ => break,
905            };
906            let _ = self.next_tok();
907            self.multiplicative_expr();
908            self.builder.emit(op, 0);
909        }
910    }
911
912    /// c:Src/math.c z_prec[MUL] = z_prec[DIV] = z_prec[MOD] = 8.
913    fn multiplicative_expr(&mut self) {
914        self.pow_expr();
915        loop {
916            let (tok, _) = self.peek_tok();
917            match tok {
918                Tok::Mul => {
919                    let _ = self.next_tok();
920                    self.pow_expr();
921                    self.builder.emit(Op::Mul, 0);
922                }
923                Tok::Div => {
924                    let _ = self.next_tok();
925                    self.pow_expr();
926                    self.emit_binop(BinOp::Div);
927                }
928                Tok::Mod => {
929                    let _ = self.next_tok();
930                    self.pow_expr();
931                    self.emit_binop(BinOp::Mod);
932                }
933                _ => break,
934            }
935        }
936    }
937
938    /// c:Src/math.c z_prec[POWER] = 7, `type[POWER] = RL|OP_A2` — right-
939    /// associative, and it binds LOOSER than `| ^ &` in zsh (tighter in C).
940    fn pow_expr(&mut self) {
941        self.bitor_expr();
942        let (tok, _) = self.peek_tok();
943        if tok == Tok::Pow {
944            let _ = self.next_tok();
945            self.pow_expr(); // right-associative
946            self.emit_binop(BinOp::Pow);
947        }
948    }
949
950    /// c:Src/math.c z_prec[OR] = 6.
951    fn bitor_expr(&mut self) {
952        self.bitxor_expr();
953        loop {
954            let (tok, _) = self.peek_tok();
955            if tok == Tok::BitOr {
956                let _ = self.next_tok();
957                self.bitxor_expr();
958                self.builder.emit(Op::BitOr, 0);
959            } else {
960                break;
961            }
962        }
963    }
964
965    /// c:Src/math.c z_prec[XOR] = 5.
966    fn bitxor_expr(&mut self) {
967        self.bitand_expr();
968        loop {
969            let (tok, _) = self.peek_tok();
970            if tok == Tok::BitXor {
971                let _ = self.next_tok();
972                self.bitand_expr();
973                self.builder.emit(Op::BitXor, 0);
974            } else {
975                break;
976            }
977        }
978    }
979
980    /// c:Src/math.c z_prec[AND] = 4.
981    fn bitand_expr(&mut self) {
982        self.shift_expr();
983        loop {
984            let (tok, _) = self.peek_tok();
985            if tok == Tok::BitAnd {
986                let _ = self.next_tok();
987                self.shift_expr();
988                self.builder.emit(Op::BitAnd, 0);
989            } else {
990                break;
991            }
992        }
993    }
994
995    /// c:Src/math.c z_prec[SHLEFT] = z_prec[SHRIGHT] = 3 — in zsh the
996    /// shifts bind tighter than every other binary operator.
997    fn shift_expr(&mut self) {
998        self.unary_expr();
999        loop {
1000            let (tok, _) = self.peek_tok();
1001            let op = match tok {
1002                Tok::Shl => Op::Shl,
1003                Tok::Shr => Op::Shr,
1004                _ => break,
1005            };
1006            let _ = self.next_tok();
1007            self.unary_expr();
1008            self.builder.emit(op, 0);
1009        }
1010    }
1011
1012    /// c:Src/math.c z_prec[NOT..UMINUS] = 2, `type[] = RL`.
1013    fn unary_expr(&mut self) {
1014        let (tok, _) = self.peek_tok();
1015        match tok {
1016            Tok::Minus => {
1017                let _ = self.next_tok();
1018                self.unary_expr();
1019                self.builder.emit(Op::Negate, 0);
1020            }
1021            Tok::Plus => {
1022                let _ = self.next_tok();
1023                self.unary_expr();
1024                // c:Src/math.c:1189 UPLUS — `c = b`, a no-op on numbers.
1025            }
1026            Tok::LogNot => {
1027                let _ = self.next_tok();
1028                self.unary_expr();
1029                self.builder.emit(Op::LogNot, 0);
1030            }
1031            Tok::BitNot => {
1032                let _ = self.next_tok();
1033                self.unary_expr();
1034                self.builder.emit(Op::BitNot, 0);
1035            }
1036            Tok::PreInc => {
1037                let _ = self.next_tok();
1038                let (_, var_name) = self.next_tok();
1039                let slot = self.slot_for(&var_name);
1040                self.assigned.insert(var_name.clone());
1041                // c:Src/math.c:1431-1437 PREPLUS — `u.d++` for a float,
1042                // `u.l++` otherwise, then setmathvar. `Op::Inc` truncated a
1043                // float to an integer (`float f=1.5; (( ++f ))` gave 2).
1044                self.emit_getmathparam(&var_name);
1045                self.builder.emit(Op::LoadInt(1), 0);
1046                self.builder.emit(Op::Add, 0);
1047                self.emit_setmathvar(&var_name, slot, true);
1048            }
1049            Tok::PreDec => {
1050                let _ = self.next_tok();
1051                let (_, var_name) = self.next_tok();
1052                let slot = self.slot_for(&var_name);
1053                self.assigned.insert(var_name.clone());
1054                self.emit_getmathparam(&var_name);
1055                self.builder.emit(Op::LoadInt(1), 0);
1056                self.builder.emit(Op::Sub, 0);
1057                self.emit_setmathvar(&var_name, slot, true);
1058            }
1059            _ => self.primary_expr(),
1060        }
1061    }
1062
1063    fn primary_expr(&mut self) {
1064        let (tok, name) = self.next_tok();
1065        match tok {
1066            Tok::Num(n) => {
1067                self.builder.emit(Op::LoadInt(n), 0);
1068            }
1069            Tok::Float(f) => {
1070                self.builder.emit(Op::LoadFloat(f), 0);
1071            }
1072            Tok::Ident => {
1073                let slot = self.slot_for(&name);
1074                self.emit_getmathparam(&name);
1075
1076                // c:Src/math.c:112-113 POSTPLUS / POSTMINUS — the value of
1077                // the expression is the OLD one; the variable keeps the new.
1078                let (post_tok, _) = self.peek_tok();
1079                match post_tok {
1080                    Tok::PreInc => {
1081                        let _ = self.next_tok();
1082                        self.assigned.insert(name.clone());
1083                        self.builder.emit(Op::Dup, 0); // keep old value
1084                        // c:Src/math.c:1394-1401 POSTPLUS — float stays float.
1085                        self.builder.emit(Op::LoadInt(1), 0);
1086                        self.builder.emit(Op::Add, 0);
1087                        self.emit_setmathvar(&name, slot, true);
1088                        self.builder.emit(Op::Pop, 0);
1089                    }
1090                    Tok::PreDec => {
1091                        let _ = self.next_tok();
1092                        self.assigned.insert(name.clone());
1093                        self.builder.emit(Op::Dup, 0);
1094                        self.builder.emit(Op::LoadInt(1), 0);
1095                        self.builder.emit(Op::Sub, 0);
1096                        self.emit_setmathvar(&name, slot, true);
1097                        self.builder.emit(Op::Pop, 0);
1098                    }
1099                    _ => {}
1100                }
1101            }
1102            Tok::LParen => {
1103                self.expr();
1104                let _ = self.next_tok(); // consume RParen
1105            }
1106            _ => {
1107                // Unexpected token — push 0
1108                self.builder.emit(Op::LoadInt(0), 0);
1109            }
1110        }
1111    }
1112
1113    /// Emit one binary operator.
1114    ///
1115    /// `+ - * & | ^ << >>` are single fusevm ops whose int/float behaviour
1116    /// already matches `Src/math.c` (int result when both operands are
1117    /// integers, float otherwise; the bitwise/shift set coerces to int,
1118    /// which is C's `OP_A2IO`).
1119    ///
1120    /// `/ % **` are NOT: `Op::Div` always produces a float (zsh does
1121    /// integer division when both operands are integers, c:1243-1256),
1122    /// `Op::Mod` returns 0 for a zero divisor instead of raising
1123    /// "division by zero" (c:1260), and `Op::Pow` is `powf` rather than
1124    /// c:1343-1344's integer power loop. Those three route to a builtin
1125    /// that ports the C arm exactly — one indirect call per OCCURRENCE of
1126    /// the operator, versus the whole-expression re-lex the runtime
1127    /// evaluator costs on every evaluation.
1128    fn emit_binop(&mut self, op: BinOp) {
1129        match op {
1130            BinOp::Add => self.builder.emit(Op::Add, 0),
1131            BinOp::Sub => self.builder.emit(Op::Sub, 0),
1132            BinOp::Mul => self.builder.emit(Op::Mul, 0),
1133            BinOp::BitAnd => self.builder.emit(Op::BitAnd, 0),
1134            BinOp::BitOr => self.builder.emit(Op::BitOr, 0),
1135            BinOp::BitXor => self.builder.emit(Op::BitXor, 0),
1136            BinOp::Shl => self.builder.emit(Op::Shl, 0),
1137            BinOp::Shr => self.builder.emit(Op::Shr, 0),
1138            BinOp::Div => self
1139                .builder
1140                .emit(Op::CallBuiltin(crate::vm_helper::BUILTIN_ARITH_DIV, 2), 0),
1141            BinOp::Mod => self
1142                .builder
1143                .emit(Op::CallBuiltin(crate::vm_helper::BUILTIN_ARITH_MOD, 2), 0),
1144            BinOp::Pow => self
1145                .builder
1146                .emit(Op::CallBuiltin(crate::vm_helper::BUILTIN_ARITH_POW, 2), 0),
1147            BinOp::LogAnd => self
1148                .builder
1149                .emit(Op::CallBuiltin(crate::vm_helper::BUILTIN_ARITH_LOGAND, 2), 0),
1150            BinOp::LogOr => self
1151                .builder
1152                .emit(Op::CallBuiltin(crate::vm_helper::BUILTIN_ARITH_LOGOR, 2), 0),
1153            BinOp::LogXor => self
1154                .builder
1155                .emit(Op::CallBuiltin(crate::vm_helper::BUILTIN_ARITH_LOGXOR, 2), 0),
1156        };
1157    }
1158}
1159
1160/// c:Src/math.c:190 `c_prec` — the table `setopt c_precedences` selects.
1161/// Indexed by the same token numbering as `Z_PREC`.
1162///
1163/// The compiled path always parses with zsh's DEFAULT precedence. That is
1164/// only safe for an expression whose operators keep their relative order
1165/// in both tables, which `prec_tables_agree` decides; anything else goes
1166/// to the runtime evaluator, which reads `prec` live.
1167const C_PREC: &[(Tok, i32)] = &[
1168    (Tok::LogNot, 2),
1169    (Tok::BitNot, 2),
1170    (Tok::PreInc, 2),
1171    (Tok::PreDec, 2),
1172    (Tok::BitAnd, 9),
1173    (Tok::BitXor, 10),
1174    (Tok::BitOr, 11),
1175    (Tok::Mul, 4),
1176    (Tok::Div, 4),
1177    (Tok::Mod, 4),
1178    (Tok::Plus, 5),
1179    (Tok::Minus, 5),
1180    (Tok::Shl, 6),
1181    (Tok::Shr, 6),
1182    (Tok::Lt, 7),
1183    (Tok::Leq, 7),
1184    (Tok::Gt, 7),
1185    (Tok::Geq, 7),
1186    (Tok::Eq, 8),
1187    (Tok::Neq, 8),
1188    (Tok::LogAnd, 12),
1189    (Tok::LogOr, 14),
1190    (Tok::LogXor, 13),
1191    (Tok::Quest, 15),
1192    (Tok::Colon, 16),
1193    (Tok::Pow, 3),
1194    (Tok::Comma, 18),
1195];
1196
1197/// c:Src/math.c:250 `z_prec` — zsh's default table, the one the cascade
1198/// in this file implements.
1199const Z_PREC: &[(Tok, i32)] = &[
1200    (Tok::LogNot, 2),
1201    (Tok::BitNot, 2),
1202    (Tok::PreInc, 2),
1203    (Tok::PreDec, 2),
1204    (Tok::Shl, 3),
1205    (Tok::Shr, 3),
1206    (Tok::BitAnd, 4),
1207    (Tok::BitXor, 5),
1208    (Tok::BitOr, 6),
1209    (Tok::Pow, 7),
1210    (Tok::Mul, 8),
1211    (Tok::Div, 8),
1212    (Tok::Mod, 8),
1213    (Tok::Plus, 9),
1214    (Tok::Minus, 9),
1215    (Tok::Lt, 10),
1216    (Tok::Leq, 10),
1217    (Tok::Gt, 10),
1218    (Tok::Geq, 10),
1219    (Tok::Eq, 11),
1220    (Tok::Neq, 11),
1221    (Tok::LogAnd, 12),
1222    (Tok::LogOr, 13),
1223    (Tok::LogXor, 13),
1224    (Tok::Quest, 14),
1225    (Tok::Colon, 15),
1226    (Tok::Comma, 17),
1227];
1228
1229fn prec_of(table: &[(Tok, i32)], tok: Tok) -> Option<i32> {
1230    table.iter().find(|(t, _)| *t == tok).map(|(_, p)| *p)
1231}
1232
1233/// Whether `Z_PREC` and `C_PREC` bracket this expression identically.
1234///
1235/// Only the RELATIVE order of the operators actually present matters to a
1236/// precedence-climbing parse, and associativity is the same in both
1237/// tables, so it is enough to check every pair of operators in the
1238/// expression for an order inversion between the two tables. `(( a << b
1239/// ))` is safe (one operator); `(( a << b & c ))` is not (`<<` is tighter
1240/// than `&` in zsh, looser in C).
1241fn prec_tables_agree(ops: &[Tok]) -> bool {
1242    for (i, x) in ops.iter().enumerate() {
1243        for y in &ops[i + 1..] {
1244            let (Some(zx), Some(zy)) = (prec_of(Z_PREC, *x), prec_of(Z_PREC, *y)) else {
1245                continue;
1246            };
1247            let (Some(cx), Some(cy)) = (prec_of(C_PREC, *x), prec_of(C_PREC, *y)) else {
1248                continue;
1249            };
1250            if (zx - zy).signum() != (cx - cy).signum() {
1251                return false;
1252            }
1253        }
1254    }
1255    true
1256}
1257
1258/// Why an expression cannot be compiled, or `None` when it can.
1259///
1260/// Everything listed here needs state the compiled path does not carry:
1261/// a subscript needs `getvalue`'s full parser, a base-tagged literal sets
1262/// `lastbase` for the ASSIGNMENT that follows it (c:Src/math.c
1263/// `lexconstant`), a math function needs `callmathfunc`, and a
1264/// `${…}` form other than a bare name needs word expansion first.
1265/// Each falls back to `BUILTIN_ARITH_EVAL`, which is what every
1266/// expression used to do.
1267pub fn arith_uncompilable_reason(expr: &str) -> Option<&'static str> {
1268    let b = expr.as_bytes();
1269    for (i, &c) in b.iter().enumerate() {
1270        match c {
1271            // Array subscript — `getmathparam` parses the whole
1272            // `name[...]` form including subscript flags.
1273            b'[' => return Some("array subscript"),
1274            // Quoted identifier — c:Src/math.c treats `"abc"` as a name.
1275            b'"' | 0x9e => return Some("quoted identifier"),
1276            // `n#ddd` base literal / `#\c` char constant: both set
1277            // `lastbase`, which the following assignment inherits.
1278            b'#' => return Some("base literal or char constant"),
1279            b'$' => {
1280                // `$name` and `${name}` name a parameter, which is what a
1281                // bare `name` means in arithmetic anyway, so both lex as
1282                // plain identifiers and read through `getmathparam`.
1283                //
1284                // A POSITIONAL (`$1`, `${2}`) does not: c:Src/params.c
1285                // keeps the positionals in `pparams`, not in `paramtab`,
1286                // so `getmathparam("1")` finds nothing and reads 0 —
1287                // `(( $1 % 15 == 0 ))` was true for every argument. Those,
1288                // and every other `${…}` form, need word expansion first
1289                // and go to the runtime evaluator.
1290                let rest = &b[i + 1..];
1291                let mut name = rest.iter().copied();
1292                let first = match name.next() {
1293                    Some(b'{') => {
1294                        let body: Vec<u8> =
1295                            rest.iter().skip(1).take_while(|&&x| x != b'}').copied().collect();
1296                        if body.is_empty()
1297                            || !body[1..].iter().all(|&x| x.is_ascii_alphanumeric() || x == b'_')
1298                        {
1299                            return Some("parameter expansion");
1300                        }
1301                        body[0]
1302                    }
1303                    Some(x) => x,
1304                    None => return Some("parameter expansion"),
1305                };
1306                if !(first.is_ascii_alphabetic() || first == b'_') {
1307                    return Some("parameter expansion");
1308                }
1309            }
1310            _ => {}
1311        }
1312    }
1313    // c:Src/math.c:872 — a bare 3-letter identifier spelled `inf` or `nan` in
1314    // any case is the Inf/NaN constant, not a parameter (except under sh
1315    // emulation). The compiled path loads every identifier as a variable, so
1316    // `INF=999999; (( v == INF ))` succeeded where zsh compares against
1317    // infinity and fails (examples/demos/267_floyd_warshall.zsh printed ∞ for
1318    // every finite 999999). `$INF` / `${INF}` stay parameter references.
1319    let mut i = 0;
1320    while i < b.len() {
1321        if b[i].is_ascii_alphabetic() || b[i] == b'_' {
1322            let start = i;
1323            while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
1324                i += 1;
1325            }
1326            let after_ref = start > 0 && matches!(b[start - 1], b'$' | b'{');
1327            if !after_ref
1328                && (expr[start..i].eq_ignore_ascii_case("inf") || expr[start..i].eq_ignore_ascii_case("nan"))
1329            {
1330                return Some("Inf/NaN constant");
1331            }
1332        } else {
1333            i += 1;
1334        }
1335    }
1336    // Base-tagged literals — see `#` above; these are the 0x/0b spellings.
1337    let lower = expr.to_ascii_lowercase();
1338    if lower.contains("0x") || lower.contains("0b") {
1339        return Some("base-tagged literal");
1340    }
1341    // A leading-zero literal (`012`). c:Src/math.c:489-505 decides octal from
1342    // `isset(OCTALZEROES)` when the literal is LEXED, and an octal literal sets
1343    // `lastbase = 8` for the assignment that follows. The compiled path is
1344    // built before the statements that run `setopt octalzeroes`, so it cannot
1345    // know the option; the runtime evaluator reads it live.
1346    if b.windows(2).enumerate().any(|(i, w)| {
1347        w[0] == b'0'
1348            && w[1].is_ascii_digit()
1349            && (i == 0 || !(b[i - 1].is_ascii_alphanumeric() || b[i - 1] == b'_' || b[i - 1] == b'.'))
1350    }) {
1351        return Some("leading-zero literal");
1352    }
1353    // Exponent form (`1e3`) — `read_number` stops at the `e`, so the
1354    // exponent would be parsed as a separate identifier.
1355    if b.windows(2)
1356        .any(|w| w[0].is_ascii_digit() && (w[1] | 0x20) == b'e')
1357    {
1358        return Some("float exponent");
1359    }
1360    // A math function call — `name(` with a name in front of the paren.
1361    let mut prev_ident = false;
1362    for &c in b {
1363        if c == b'(' && prev_ident {
1364            return Some("math function call");
1365        }
1366        prev_ident = c.is_ascii_alphanumeric() || c == b'_';
1367    }
1368    // Precedence-table divergence under `setopt c_precedences`.
1369    let mut ac = ArithCompiler::new(expr);
1370    let mut ops: Vec<Tok> = Vec::new();
1371    // c:Src/math.c:1631 — an `ID` operand is pushed without a value and
1372    // read by `getmathparam` only when its operator pops it (c:1375), so
1373    // an operand whose name is MODIFIED later in the expression is read
1374    // AFTER that modification: `x=1; (( y = x + (x = 5) ))` is 10. The
1375    // compiled path reads in operand position (`emit_getmathparam`), which
1376    // only agrees with C while no such later modification exists.
1377    let mut toks: Vec<(Tok, String)> = Vec::new();
1378    loop {
1379        let (tok, text) = ac.next_tok();
1380        if tok == Tok::Eoi {
1381            break;
1382        }
1383        toks.push((tok, text.clone()));
1384        // c:Src/math.c:1456-1476 `bop` and c:1321-1331 `op` — `&&=` / `||=`
1385        // raise `noeval` over the right operand when the left one already
1386        // decides the result, and coerce both to integer (type[] BOOL|OP_E2IO,
1387        // c:297). `^^=` is `RL|OP_A2IO` (c:297): it has no OP_E2 bit, so C
1388        // computes the xor and never stores it. The compiled `assign_expr`
1389        // lowered all three like `+=` (eager RHS, float operands, store), so
1390        // `x=0; (( x &&= y++ ))` bumped y and `(( x ^^= 1 ))` overwrote x.
1391        // The runtime evaluator is the port of op()/bop() itself.
1392        if matches!(tok, Tok::LogAndAssign | Tok::LogOrAssign | Tok::LogXorAssign) {
1393            return Some("logical compound assignment");
1394        }
1395        if prec_of(Z_PREC, tok).is_some() {
1396            ops.push(tok);
1397        }
1398    }
1399    if !prec_tables_agree(&ops) {
1400        return Some("c_precedences-sensitive operator mix");
1401    }
1402    let modifies = |i: usize| {
1403        let next = toks.get(i + 1).map(|t| t.0);
1404        let prev = i.checked_sub(1).map(|p| toks[p].0);
1405        matches!(next, Some(Tok::Assign | Tok::PreInc | Tok::PreDec))
1406            || next.and_then(compound_assign_op).is_some()
1407            || matches!(prev, Some(Tok::PreInc | Tok::PreDec))
1408    };
1409    for (i, (tok, name)) in toks.iter().enumerate() {
1410        if *tok != Tok::Ident || modifies(i) {
1411            continue;
1412        }
1413        let modified_later = toks[i + 1..]
1414            .iter()
1415            .enumerate()
1416            .any(|(j, (t, n))| *t == Tok::Ident && n == name && modifies(i + 1 + j));
1417        if modified_later {
1418            return Some("operand modified later in the expression");
1419        }
1420    }
1421    None
1422}
1423
1424/// The binary operators `emit_binop` can lower. Separate from `Tok` because
1425/// `x op= y` and `x op y` share the operator but not the token.
1426#[derive(Debug, Clone, Copy, PartialEq)]
1427enum BinOp {
1428    Add,
1429    Sub,
1430    Mul,
1431    Div,
1432    Mod,
1433    Pow,
1434    BitAnd,
1435    BitOr,
1436    BitXor,
1437    Shl,
1438    Shr,
1439    LogAnd,
1440    LogOr,
1441    LogXor,
1442}
1443
1444/// The operator a compound-assignment token applies —
1445/// c:Src/math.c:1364-1372, where `PLUSEQ` runs the `PLUS` arm and then
1446/// assigns. `None` for a token that is not a compound assignment.
1447fn compound_assign_op(tok: Tok) -> Option<BinOp> {
1448    Some(match tok {
1449        Tok::PlusAssign => BinOp::Add,
1450        Tok::MinusAssign => BinOp::Sub,
1451        Tok::MulAssign => BinOp::Mul,
1452        Tok::DivAssign => BinOp::Div,
1453        Tok::ModAssign => BinOp::Mod,
1454        Tok::AndAssign => BinOp::BitAnd,
1455        Tok::OrAssign => BinOp::BitOr,
1456        Tok::XorAssign => BinOp::BitXor,
1457        Tok::ShlAssign => BinOp::Shl,
1458        Tok::ShrAssign => BinOp::Shr,
1459        Tok::PowAssign => BinOp::Pow,
1460        Tok::LogAndAssign => BinOp::LogAnd,
1461        Tok::LogOrAssign => BinOp::LogOr,
1462        Tok::LogXorAssign => BinOp::LogXor,
1463        _ => return None,
1464    })
1465}
1466
1467// ═══════════════════════════════════════════════════════════════════════════
1468// Tests — pure-expression evaluation via ArithCompiler → fusevm::VM.
1469//
1470// These tests pin the compiler's emitted bytecode by *running* the result and
1471// asserting the numeric output. Variable / identifier paths need executor
1472// pre-loading and are exercised by integration tests in tests/zshrs_shell.rs
1473// — the unit tests below stay literal-only so they need no shell context.
1474// ═══════════════════════════════════════════════════════════════════════════
1475
1476#[cfg(test)]
1477mod tests {
1478    use super::*;
1479    use fusevm::{VMResult, Value};
1480
1481    /// Compile + run an arithmetic expression. Panics on VM error so a
1482    /// regression surfaces with the expression text.
1483    fn eval(expr: &str) -> Value {
1484        let chunk = ArithCompiler::new(expr).compile();
1485        let mut vm = fusevm::VM::new(chunk);
1486        // `/ % **` lower to builtin calls because no fusevm op has
1487        // `Src/math.c`'s int/float semantics for them (see `emit_binop`).
1488        // Register the same three arms the shell registers so these
1489        // literal-only tests exercise the real operator bodies.
1490        vm.register_builtin(crate::vm_helper::BUILTIN_ARITH_DIV, |vm, _argc| {
1491            let b = crate::fusevm_bridge::arith_pop_mnumber(vm);
1492            let a = crate::fusevm_bridge::arith_pop_mnumber(vm);
1493            crate::fusevm_bridge::arith_push(crate::fusevm_bridge::arith_div(a, b))
1494        });
1495        vm.register_builtin(crate::vm_helper::BUILTIN_ARITH_MOD, |vm, _argc| {
1496            let b = crate::fusevm_bridge::arith_pop_mnumber(vm);
1497            let a = crate::fusevm_bridge::arith_pop_mnumber(vm);
1498            crate::fusevm_bridge::arith_push(crate::fusevm_bridge::arith_mod(a, b))
1499        });
1500        vm.register_builtin(crate::vm_helper::BUILTIN_ARITH_POW, |vm, _argc| {
1501            let b = crate::fusevm_bridge::arith_pop_mnumber(vm);
1502            let a = crate::fusevm_bridge::arith_pop_mnumber(vm);
1503            crate::fusevm_bridge::arith_push(crate::fusevm_bridge::arith_pow(a, b))
1504        });
1505        match vm.run() {
1506            VMResult::Ok(v) => v,
1507            VMResult::Halted => Value::Undef,
1508            VMResult::Error(e) => panic!("VM error evaluating {expr:?}: {e}"),
1509        }
1510    }
1511
1512    fn eval_int(expr: &str) -> i64 {
1513        eval(expr).to_int()
1514    }
1515
1516    fn eval_float(expr: &str) -> f64 {
1517        eval(expr).to_float()
1518    }
1519
1520    // ── Integer literals ─────────────────────────────────────────────────
1521    #[test]
1522    fn literal_zero() {
1523        assert_eq!(eval_int("0"), 0);
1524    }
1525
1526    #[test]
1527    fn literal_small_positive() {
1528        assert_eq!(eval_int("42"), 42);
1529    }
1530
1531    #[test]
1532    fn literal_large() {
1533        assert_eq!(eval_int("1000000000"), 1_000_000_000);
1534    }
1535
1536    #[test]
1537    fn literal_hex_lowercase() {
1538        assert_eq!(eval_int("0xff"), 255);
1539    }
1540
1541    #[test]
1542    fn literal_hex_uppercase() {
1543        assert_eq!(eval_int("0XDEAD"), 0xDEAD);
1544    }
1545
1546    #[test]
1547    fn literal_hex_mixed() {
1548        assert_eq!(eval_int("0xCaFe"), 0xCAFE);
1549    }
1550
1551    #[test]
1552    fn literal_octal() {
1553        // zsh DEFAULT: `017` is decimal 17, NOT octal — `setopt
1554        // OCTAL_ZEROES` is required to enable C-style 0NNN octals
1555        // (per Src/options.c default_opts[]). Verified against
1556        // `zsh -fc 'let x=017; print $x'` → "17".
1557        assert_eq!(eval_int("017"), 17);
1558    }
1559
1560    #[test]
1561    fn literal_octal_zero_prefix_only() {
1562        // `0` alone is decimal zero; the octal path requires a digit AFTER `0`.
1563        assert_eq!(eval_int("0"), 0);
1564    }
1565
1566    // ── Float literals ───────────────────────────────────────────────────
1567    #[test]
1568    fn literal_float_simple() {
1569        assert!((eval_float("3.14") - 3.14).abs() < 1e-9);
1570    }
1571
1572    #[test]
1573    fn literal_float_no_fractional_digits() {
1574        // `5.` parses as 5.0 per read_number()'s `.` handling.
1575        assert!((eval_float("5.") - 5.0).abs() < 1e-9);
1576    }
1577
1578    // ── Addition / subtraction ───────────────────────────────────────────
1579    #[test]
1580    fn add_two_ints() {
1581        assert_eq!(eval_int("40 + 2"), 42);
1582    }
1583
1584    #[test]
1585    fn sub_two_ints() {
1586        assert_eq!(eval_int("50 - 8"), 42);
1587    }
1588
1589    #[test]
1590    fn sub_into_negative() {
1591        assert_eq!(eval_int("5 - 10"), -5);
1592    }
1593
1594    #[test]
1595    fn add_chain_left_associative() {
1596        assert_eq!(eval_int("1 + 2 + 3 + 4"), 10);
1597    }
1598
1599    #[test]
1600    fn sub_chain_left_associative() {
1601        // (((100 - 1) - 2) - 3) = 94, NOT 100 - (1 - 2 - 3) = 104
1602        assert_eq!(eval_int("100 - 1 - 2 - 3"), 94);
1603    }
1604
1605    // ── Multiplication / division / modulo ──────────────────────────────
1606    #[test]
1607    fn mul_two_ints() {
1608        assert_eq!(eval_int("6 * 7"), 42);
1609    }
1610
1611    #[test]
1612    fn div_two_ints() {
1613        assert_eq!(eval_int("84 / 2"), 42);
1614    }
1615
1616    #[test]
1617    fn mod_two_ints() {
1618        assert_eq!(eval_int("17 % 5"), 2);
1619    }
1620
1621    #[test]
1622    fn mod_evenly_divides() {
1623        assert_eq!(eval_int("10 % 5"), 0);
1624    }
1625
1626    // ── Precedence ───────────────────────────────────────────────────────
1627    #[test]
1628    fn precedence_mul_over_add() {
1629        assert_eq!(eval_int("2 + 3 * 4"), 14);
1630    }
1631
1632    #[test]
1633    fn precedence_div_over_sub() {
1634        assert_eq!(eval_int("20 - 10 / 2"), 15);
1635    }
1636
1637    #[test]
1638    fn precedence_parens_override() {
1639        assert_eq!(eval_int("(2 + 3) * 4"), 20);
1640    }
1641
1642    #[test]
1643    fn precedence_nested_parens() {
1644        assert_eq!(eval_int("((1 + 2) * (3 + 4))"), 21);
1645    }
1646
1647    #[test]
1648    fn precedence_pow_over_mul() {
1649        // 2 * 3 ** 2 = 2 * 9 = 18, not (2*3)**2 = 36
1650        assert_eq!(eval_int("2 * 3 ** 2"), 18);
1651    }
1652
1653    #[test]
1654    fn pow_right_associative() {
1655        // 2 ** 3 ** 2 = 2 ** 9 = 512, not (2**3)**2 = 64
1656        assert_eq!(eval_int("2 ** 3 ** 2"), 512);
1657    }
1658
1659    // ── Unary operators ──────────────────────────────────────────────────
1660    #[test]
1661    fn unary_minus_literal() {
1662        assert_eq!(eval_int("-5"), -5);
1663    }
1664
1665    #[test]
1666    fn unary_minus_expr() {
1667        assert_eq!(eval_int("-(3 + 4)"), -7);
1668    }
1669
1670    #[test]
1671    fn unary_plus_is_noop() {
1672        assert_eq!(eval_int("+42"), 42);
1673    }
1674
1675    #[test]
1676    fn double_negation_requires_separator() {
1677        // `--5` parses as pre-decrement (the `--` token), NOT two unary minuses.
1678        // Real double negation needs a space or parens between the two `-`s.
1679        assert_eq!(eval_int("- -5"), 5);
1680        assert_eq!(eval_int("-(-5)"), 5);
1681    }
1682
1683    #[test]
1684    fn unary_minus_binds_tighter_than_pow() {
1685        // Our grammar: unary_expr → unary_expr (right-recursive), then primary.
1686        // `-2 ** 2` parses as `(-2) ** 2` = 4 with this layout (pow is below
1687        // unary). Pinning current behavior — if it changes, this test surfaces
1688        // the change explicitly.
1689        assert_eq!(eval_int("-2 ** 2"), 4);
1690    }
1691
1692    // ── Bitwise ──────────────────────────────────────────────────────────
1693    #[test]
1694    fn bitand_basic() {
1695        assert_eq!(eval_int("0xFF & 0x0F"), 0x0F);
1696    }
1697
1698    #[test]
1699    fn bitor_basic() {
1700        assert_eq!(eval_int("0x10 | 0x01"), 0x11);
1701    }
1702
1703    #[test]
1704    fn bitxor_basic() {
1705        assert_eq!(eval_int("0xFF ^ 0x0F"), 0xF0);
1706    }
1707
1708    #[test]
1709    fn bitnot_zero_is_minus_one() {
1710        assert_eq!(eval_int("~0"), -1);
1711    }
1712
1713    #[test]
1714    fn bitnot_one() {
1715        // ~1 = -2 (two's-complement)
1716        assert_eq!(eval_int("~1"), -2);
1717    }
1718
1719    #[test]
1720    fn bitwise_precedence_and_over_or() {
1721        // & binds tighter than | → 1 | (2 & 0) = 1 | 0 = 1
1722        assert_eq!(eval_int("1 | 2 & 0"), 1);
1723    }
1724
1725    // ── Shifts ───────────────────────────────────────────────────────────
1726    #[test]
1727    fn shl_basic() {
1728        assert_eq!(eval_int("1 << 4"), 16);
1729    }
1730
1731    #[test]
1732    fn shr_basic() {
1733        assert_eq!(eval_int("16 >> 2"), 4);
1734    }
1735
1736    #[test]
1737    fn shl_chain() {
1738        assert_eq!(eval_int("1 << 1 << 2"), 8);
1739    }
1740
1741    // ── Comparison ───────────────────────────────────────────────────────
1742    #[test]
1743    fn cmp_eq_true() {
1744        assert_eq!(eval_int("5 == 5"), 1);
1745    }
1746
1747    #[test]
1748    fn cmp_eq_false() {
1749        assert_eq!(eval_int("5 == 6"), 0);
1750    }
1751
1752    #[test]
1753    fn cmp_ne_true() {
1754        assert_eq!(eval_int("5 != 6"), 1);
1755    }
1756
1757    #[test]
1758    fn cmp_lt_true() {
1759        assert_eq!(eval_int("3 < 5"), 1);
1760    }
1761
1762    #[test]
1763    fn cmp_lt_false_on_equal() {
1764        assert_eq!(eval_int("5 < 5"), 0);
1765    }
1766
1767    #[test]
1768    fn cmp_le_true_on_equal() {
1769        assert_eq!(eval_int("5 <= 5"), 1);
1770    }
1771
1772    #[test]
1773    fn cmp_gt_true() {
1774        assert_eq!(eval_int("5 > 3"), 1);
1775    }
1776
1777    #[test]
1778    fn cmp_ge_true_on_equal() {
1779        assert_eq!(eval_int("5 >= 5"), 1);
1780    }
1781
1782    // ── Logical ──────────────────────────────────────────────────────────
1783    #[test]
1784    fn logand_true_true() {
1785        assert_eq!(eval_int("1 && 1"), 1);
1786    }
1787
1788    #[test]
1789    fn logand_short_circuits_on_false() {
1790        // 0 && X — short-circuit means RHS doesn't matter.
1791        assert_eq!(eval_int("0 && 99"), 0);
1792    }
1793
1794    #[test]
1795    fn logor_true_short_circuits() {
1796        // 1 || X — short-circuit yields 1 regardless of RHS.
1797        assert_eq!(eval_int("1 || 0"), 1);
1798    }
1799
1800    #[test]
1801    fn logor_both_false() {
1802        assert_eq!(eval_int("0 || 0"), 0);
1803    }
1804
1805    #[test]
1806    fn lognot_true() {
1807        assert_eq!(eval_int("!0"), 1);
1808    }
1809
1810    #[test]
1811    fn lognot_false() {
1812        assert_eq!(eval_int("!1"), 0);
1813    }
1814
1815    #[test]
1816    fn lognot_double() {
1817        // !!5 == truthy(5) == 1
1818        assert_eq!(eval_int("!!5"), 1);
1819    }
1820
1821    // ── Ternary ──────────────────────────────────────────────────────────
1822    #[test]
1823    fn ternary_true_branch() {
1824        assert_eq!(eval_int("1 ? 10 : 20"), 10);
1825    }
1826
1827    #[test]
1828    fn ternary_false_branch() {
1829        assert_eq!(eval_int("0 ? 10 : 20"), 20);
1830    }
1831
1832    #[test]
1833    fn ternary_condition_is_expression() {
1834        assert_eq!(eval_int("(3 < 5) ? 100 : 200"), 100);
1835    }
1836
1837    #[test]
1838    fn ternary_nested_in_true_branch() {
1839        assert_eq!(eval_int("1 ? (0 ? 1 : 2) : 3"), 2);
1840    }
1841
1842    // ── Float arithmetic ─────────────────────────────────────────────────
1843    #[test]
1844    fn float_add() {
1845        assert!((eval_float("1.5 + 2.5") - 4.0).abs() < 1e-9);
1846    }
1847
1848    #[test]
1849    fn float_div_does_not_truncate() {
1850        // 1.0 / 4.0 keeps fractional part. C zsh: `(( 1.0 / 4.0 ))` = 0.25.
1851        assert!((eval_float("1.0 / 4.0") - 0.25).abs() < 1e-9);
1852    }
1853
1854    // ── collect_identifiers (pure helper) ────────────────────────────────
1855    #[test]
1856    fn collect_identifiers_bare() {
1857        let c = ArithCompiler::new("");
1858        let names = c.collect_identifiers("a + b * c");
1859        assert_eq!(names, vec!["a", "b", "c"]);
1860    }
1861
1862    #[test]
1863    fn collect_identifiers_dollar_prefixed() {
1864        let c = ArithCompiler::new("");
1865        let names = c.collect_identifiers("$x + 1");
1866        assert_eq!(names, vec!["x"]);
1867    }
1868
1869    #[test]
1870    fn collect_identifiers_braced() {
1871        let c = ArithCompiler::new("");
1872        let names = c.collect_identifiers("${count} * 2");
1873        assert_eq!(names, vec!["count"]);
1874    }
1875
1876    #[test]
1877    fn collect_identifiers_positional() {
1878        let c = ArithCompiler::new("");
1879        let names = c.collect_identifiers("$1 + $2");
1880        assert_eq!(names, vec!["1", "2"]);
1881    }
1882
1883    #[test]
1884    fn collect_identifiers_dedups() {
1885        let c = ArithCompiler::new("");
1886        let names = c.collect_identifiers("a + a + b + a");
1887        assert_eq!(names, vec!["a", "b"]);
1888    }
1889
1890    #[test]
1891    fn collect_identifiers_ignores_pure_numeric() {
1892        let c = ArithCompiler::new("");
1893        let names = c.collect_identifiers("42 + 7 * 3");
1894        assert!(names.is_empty(), "got names: {names:?}");
1895    }
1896
1897    // ── slot_for (variable slot allocator) ───────────────────────────────
1898    #[test]
1899    fn slot_for_first_var_gets_slot_zero() {
1900        let mut c = ArithCompiler::new("");
1901        assert_eq!(c.slot_for("x"), 0);
1902    }
1903
1904    #[test]
1905    fn slot_for_second_var_gets_slot_one() {
1906        let mut c = ArithCompiler::new("");
1907        let _ = c.slot_for("x");
1908        assert_eq!(c.slot_for("y"), 1);
1909    }
1910
1911    #[test]
1912    fn slot_for_repeated_name_returns_same_slot() {
1913        let mut c = ArithCompiler::new("");
1914        let s1 = c.slot_for("a");
1915        let s2 = c.slot_for("a");
1916        assert_eq!(s1, s2);
1917    }
1918
1919    // ── Chunk shape ──────────────────────────────────────────────────────
1920    #[test]
1921    fn compile_emits_pushframe_and_returnvalue_brackets() {
1922        let chunk = ArithCompiler::new("1").compile();
1923        assert!(
1924            matches!(chunk.ops.first(), Some(Op::PushFrame)),
1925            "first op should be PushFrame, got {:?}",
1926            chunk.ops.first()
1927        );
1928        assert!(
1929            matches!(chunk.ops.last(), Some(Op::ReturnValue)),
1930            "last op should be ReturnValue, got {:?}",
1931            chunk.ops.last()
1932        );
1933    }
1934
1935    #[test]
1936    fn compile_sets_source_marker() {
1937        let chunk = ArithCompiler::new("1").compile();
1938        assert_eq!(chunk.source, "$((...))");
1939    }
1940}