Skip to main content

kaish_kernel/
arithmetic.rs

1//! Arithmetic expression evaluation for shell-style `$(( ))` expressions.
2//!
3//! Supports:
4//! - Integer arithmetic: `+`, `-`, `*`, `/`, `%`
5//! - Comparison operators: `>`, `<`, `>=`, `<=`, `==`, `!=` (return 1 or 0)
6//! - Parentheses for grouping: `(expr)`
7//! - Variable references: `$VAR` or bare `VAR`
8//! - Integer literals
9//!
10//! Does NOT support:
11//! - Floating point (pipe to `jq` for float math)
12//! - Bitwise operations (shell-ism we're skipping)
13//! - Assignment within expressions (confusing)
14
15use crate::interpreter::Scope;
16use crate::ast::{Value, VarPath, VarSegment};
17use anyhow::{bail, Context, Result};
18
19/// Evaluate an arithmetic expression string.
20///
21/// The expression should be the content between `$((` and `))`.
22///
23/// # Example
24/// ```ignore
25/// let scope = Scope::new();
26/// scope.set("X", Value::Int(5));
27/// let result = eval_arithmetic("X + 3", &scope)?;
28/// assert_eq!(result, 8);
29/// ```
30pub fn eval_arithmetic(expr: &str, scope: &Scope) -> Result<i64> {
31    let mut parser = ArithParser::new(expr, scope);
32    let result = parser.parse_comparison()?;
33    parser.expect_end()?;
34    Ok(result)
35}
36
37/// Simple recursive descent parser for arithmetic expressions.
38struct ArithParser<'a> {
39    input: &'a str,
40    pos: usize,
41    scope: &'a Scope,
42}
43
44impl<'a> ArithParser<'a> {
45    fn new(input: &'a str, scope: &'a Scope) -> Self {
46        Self { input, pos: 0, scope }
47    }
48
49    fn skip_whitespace(&mut self) {
50        while self.pos < self.input.len() {
51            let ch = self.input.as_bytes()[self.pos];
52            if ch == b' ' || ch == b'\t' {
53                self.pos += 1;
54            } else {
55                break;
56            }
57        }
58    }
59
60    fn peek(&mut self) -> Option<char> {
61        self.skip_whitespace();
62        self.input[self.pos..].chars().next()
63    }
64
65    fn advance(&mut self) -> Option<char> {
66        self.skip_whitespace();
67        let ch = self.input[self.pos..].chars().next()?;
68        self.pos += ch.len_utf8();
69        Some(ch)
70    }
71
72    /// Peek at the character n positions ahead (0 = current after whitespace skip).
73    fn peek_ahead(&mut self, n: usize) -> Option<char> {
74        self.skip_whitespace();
75        self.input[self.pos..].chars().nth(n)
76    }
77
78    fn expect_end(&mut self) -> Result<()> {
79        self.skip_whitespace();
80        if self.pos < self.input.len() {
81            bail!("unexpected characters at end of arithmetic expression: {:?}",
82                  &self.input[self.pos..]);
83        }
84        Ok(())
85    }
86
87    /// Parse comparison operators (lowest precedence): >, <, >=, <=, ==, !=
88    /// Returns 1 for true, 0 for false.
89    fn parse_comparison(&mut self) -> Result<i64> {
90        let mut left = self.parse_expr()?;
91
92        loop {
93            self.skip_whitespace();
94            match (self.peek_ahead(0), self.peek_ahead(1)) {
95                // Two-character operators must be checked first
96                (Some('>'), Some('=')) => {
97                    self.advance(); // consume '>'
98                    self.advance(); // consume '='
99                    let right = self.parse_expr()?;
100                    left = if left >= right { 1 } else { 0 };
101                }
102                (Some('<'), Some('=')) => {
103                    self.advance(); // consume '<'
104                    self.advance(); // consume '='
105                    let right = self.parse_expr()?;
106                    left = if left <= right { 1 } else { 0 };
107                }
108                (Some('='), Some('=')) => {
109                    self.advance(); // consume '='
110                    self.advance(); // consume '='
111                    let right = self.parse_expr()?;
112                    left = if left == right { 1 } else { 0 };
113                }
114                (Some('!'), Some('=')) => {
115                    self.advance(); // consume '!'
116                    self.advance(); // consume '='
117                    let right = self.parse_expr()?;
118                    left = if left != right { 1 } else { 0 };
119                }
120                // Single-character operators
121                (Some('>'), _) => {
122                    self.advance(); // consume '>'
123                    let right = self.parse_expr()?;
124                    left = if left > right { 1 } else { 0 };
125                }
126                (Some('<'), _) => {
127                    self.advance(); // consume '<'
128                    let right = self.parse_expr()?;
129                    left = if left < right { 1 } else { 0 };
130                }
131                _ => break,
132            }
133        }
134
135        Ok(left)
136    }
137
138    /// Parse an expression: handles + and - (lowest precedence)
139    fn parse_expr(&mut self) -> Result<i64> {
140        let mut left = self.parse_term()?;
141
142        loop {
143            match self.peek() {
144                Some('+') => {
145                    self.advance();
146                    let right = self.parse_term()?;
147                    left = left.checked_add(right)
148                        .context("arithmetic overflow in addition")?;
149                }
150                Some('-') => {
151                    self.advance();
152                    let right = self.parse_term()?;
153                    left = left.checked_sub(right)
154                        .context("arithmetic overflow in subtraction")?;
155                }
156                _ => break,
157            }
158        }
159
160        Ok(left)
161    }
162
163    /// Parse a term: handles * / % (higher precedence)
164    fn parse_term(&mut self) -> Result<i64> {
165        let mut left = self.parse_unary()?;
166
167        loop {
168            match self.peek() {
169                Some('*') => {
170                    self.advance();
171                    let right = self.parse_unary()?;
172                    left = left.checked_mul(right)
173                        .context("arithmetic overflow in multiplication")?;
174                }
175                Some('/') => {
176                    self.advance();
177                    let right = self.parse_unary()?;
178                    if right == 0 {
179                        bail!("division by zero");
180                    }
181                    left = left.checked_div(right)
182                        .context("arithmetic overflow in division")?;
183                }
184                Some('%') => {
185                    self.advance();
186                    let right = self.parse_unary()?;
187                    if right == 0 {
188                        bail!("modulo by zero");
189                    }
190                    left = left.checked_rem(right)
191                        .context("arithmetic overflow in modulo")?;
192                }
193                _ => break,
194            }
195        }
196
197        Ok(left)
198    }
199
200    /// Parse unary operators: + and - prefix
201    fn parse_unary(&mut self) -> Result<i64> {
202        match self.peek() {
203            Some('+') => {
204                self.advance();
205                self.parse_unary()
206            }
207            Some('-') => {
208                self.advance();
209                let val = self.parse_unary()?;
210                val.checked_neg().context("arithmetic overflow in negation")
211            }
212            _ => self.parse_primary(),
213        }
214    }
215
216    /// Parse primary: numbers, variables, parenthesized expressions
217    fn parse_primary(&mut self) -> Result<i64> {
218        self.skip_whitespace();
219
220        match self.peek() {
221            Some('(') => {
222                self.advance(); // consume '('
223                let val = self.parse_expr()?;
224                match self.peek() {
225                    Some(')') => {
226                        self.advance();
227                        Ok(val)
228                    }
229                    _ => bail!("expected ')' in arithmetic expression"),
230                }
231            }
232            Some('$') => {
233                // $VAR, ${VAR}, $?, $$, ${?}, ${$} syntax
234                self.advance(); // consume '$'
235
236                // Special case: $? (last exit code)
237                if self.peek() == Some('?') {
238                    self.advance(); // consume '?'
239                    return Ok(self.scope.last_result().code);
240                }
241
242                // Special case: $$ (current PID)
243                if self.peek() == Some('$') {
244                    self.advance(); // consume second '$'
245                    return Ok(self.scope.pid() as i64);
246                }
247
248                let var_name = if self.peek() == Some('{') {
249                    self.advance(); // consume '{'
250
251                    // Special case: ${?} (last exit code, braced form)
252                    if self.peek() == Some('?') {
253                        self.advance(); // consume '?'
254                        if self.peek() != Some('}') {
255                            bail!("expected '}}' after ${{?}} in arithmetic");
256                        }
257                        self.advance(); // consume '}'
258                        return Ok(self.scope.last_result().code);
259                    }
260
261                    // Special case: ${$} (current PID, braced form)
262                    if self.peek() == Some('$') {
263                        self.advance(); // consume '$'
264                        if self.peek() != Some('}') {
265                            bail!("expected '}}' after ${{$}} in arithmetic");
266                        }
267                        self.advance(); // consume '}'
268                        return Ok(self.scope.pid() as i64);
269                    }
270
271                    let name = self.parse_identifier()?;
272                    // Collection subscript path: `${p[port]}`, `${a[b][0]}`.
273                    if self.peek() == Some('[') {
274                        return self.eval_braced_path(&name);
275                    }
276                    if self.peek() != Some('}') {
277                        bail!("expected '}}' after variable name in arithmetic");
278                    }
279                    self.advance(); // consume '}'
280                    name
281                } else {
282                    self.parse_identifier()?
283                };
284                self.get_var_value(&var_name)
285            }
286            Some(c) if c.is_ascii_digit() => {
287                self.parse_number()
288            }
289            Some(c) if c.is_ascii_alphabetic() || c == '_' => {
290                // Bare variable name (bash allows this in $(( )))
291                let var_name = self.parse_identifier()?;
292                if self.peek() == Some('[') {
293                    // Bare subscript path `xs[i]` — decision B.
294                    return self.eval_bare_subscript_path(&var_name);
295                }
296                self.get_var_value(&var_name)
297            }
298            Some(c) => bail!("unexpected character in arithmetic expression: {:?}", c),
299            None => bail!("unexpected end of arithmetic expression"),
300        }
301    }
302
303    fn parse_number(&mut self) -> Result<i64> {
304        let start = self.pos;
305        while self.pos < self.input.len() {
306            let ch = self.input.as_bytes()[self.pos];
307            if ch.is_ascii_digit() {
308                self.pos += 1;
309            } else {
310                break;
311            }
312        }
313        let num_str = &self.input[start..self.pos];
314        num_str.parse().context("invalid number in arithmetic expression")
315    }
316
317    fn parse_identifier(&mut self) -> Result<String> {
318        let start = self.pos;
319        while self.pos < self.input.len() {
320            let ch = self.input.as_bytes()[self.pos];
321            if ch.is_ascii_alphanumeric() || ch == b'_' {
322                self.pos += 1;
323            } else {
324                break;
325            }
326        }
327        if start == self.pos {
328            bail!("expected identifier in arithmetic expression");
329        }
330        Ok(self.input[start..self.pos].to_string())
331    }
332
333    fn get_var_value(&self, name: &str) -> Result<i64> {
334        // Check for positional parameters ($0, $1, $2, ... $9, etc.)
335        // Name is just the digits when called from `$1` or `${1}` parsing
336        if let Ok(index) = name.parse::<usize>() {
337            if let Some(pos_val) = self.scope.get_positional(index) {
338                return pos_val.parse().with_context(|| {
339                    format!("${} has non-numeric value: {:?}", index, pos_val)
340                });
341            }
342            return Ok(0); // Unset positional defaults to 0
343        }
344
345        // Regular variable lookup
346        match self.scope.get(name).cloned() {
347            Some(value) => self.value_to_arith(&value, name),
348            None => Ok(0), // Unset variables default to 0 in arithmetic
349        }
350    }
351
352    /// Resolve a subscripted variable path (`${p[port]}`) and coerce to an
353    /// integer. Reuses the real path resolver, so scalar unwrap and the loud
354    /// path errors are identical to `${p[port]}` outside arithmetic.
355    fn eval_braced_path(&mut self, root: &str) -> Result<i64> {
356        let mut brackets = String::new();
357        while self.peek() == Some('[') {
358            brackets.push('[');
359            self.advance(); // consume '['
360            let mut depth = 1;
361            while depth > 0 {
362                match self.advance() {
363                    Some('[') => {
364                        depth += 1;
365                        brackets.push('[');
366                    }
367                    Some(']') => {
368                        depth -= 1;
369                        brackets.push(']');
370                    }
371                    Some(c) => brackets.push(c),
372                    None => bail!("unterminated subscript in arithmetic"),
373                }
374            }
375        }
376        if self.peek() != Some('}') {
377            bail!("expected '}}' after subscripted variable in arithmetic");
378        }
379        self.advance(); // consume '}'
380
381        let raw = format!("${{{root}{brackets}}}");
382        let path = crate::parser::parse_varpath(&raw);
383        let value = self.scope.resolve_path(&path).map_err(|e| match e {
384            crate::interpreter::PathError::UndefinedRoot(_) => {
385                anyhow::anyhow!("undefined variable in arithmetic: {root}")
386            }
387            crate::interpreter::PathError::Absence(msg)
388            | crate::interpreter::PathError::Shape(msg) => anyhow::anyhow!(msg),
389        })?;
390        self.value_to_arith(&value, root)
391    }
392
393    /// Resolve a BARE subscripted path in arithmetic (`xs[i]`, `xs[0]`,
394    /// `xs[i+1]`, `xs[-1]`) and coerce to an integer.
395    ///
396    /// Decision B: inside `$(( … ))` a bracket's contents are a numeric
397    /// expression, so a bareword subscript is the VARIABLE `i` (evaluated here),
398    /// NOT the literal key — the exact opposite of the interpolation form
399    /// `${xs[i]}`, which stays a literal key via `eval_braced_path`. Each
400    /// subscript is evaluated as a nested arithmetic expression to an integer
401    /// index; chained subscripts (`grid[i][j]`) walk left to right.
402    fn eval_bare_subscript_path(&mut self, root: &str) -> Result<i64> {
403        let mut segments = vec![VarSegment::Field(root.to_string())];
404        while self.peek() == Some('[') {
405            self.advance(); // consume '['
406            let index = self.parse_comparison()?; // the inner is a numeric expr
407            self.skip_whitespace();
408            if self.peek() != Some(']') {
409                bail!("expected ']' to close subscript in arithmetic");
410            }
411            self.advance(); // consume ']'
412            segments.push(VarSegment::Index(index));
413        }
414        let path = VarPath { segments };
415        let value = self.scope.resolve_path(&path).map_err(|e| match e {
416            crate::interpreter::PathError::UndefinedRoot(_) => {
417                anyhow::anyhow!("undefined variable in arithmetic: {root}")
418            }
419            crate::interpreter::PathError::Absence(msg)
420            | crate::interpreter::PathError::Shape(msg) => anyhow::anyhow!(msg),
421        })?;
422        self.value_to_arith(&value, root)
423    }
424
425    /// Coerce a resolved value to an integer for arithmetic.
426    fn value_to_arith(&self, value: &Value, name: &str) -> Result<i64> {
427        match value {
428            Value::Int(n) => Ok(*n),
429            Value::String(s) => {
430                // Try to parse string as integer
431                s.parse().with_context(|| format!(
432                    "variable '{}' has non-numeric value: {:?}", name, s
433                ))
434            }
435            Value::Float(f) => Ok(*f as i64),
436            Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
437            Value::Null => Ok(0), // null coerces to 0 in arithmetic
438            Value::Json(_) => anyhow::bail!("variable '{}' is JSON, not a number", name),
439            Value::Bytes(_) => anyhow::bail!("variable '{}' is binary data, not a number", name),
440        }
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    fn eval(expr: &str) -> i64 {
449        let scope = Scope::new();
450        eval_arithmetic(expr, &scope).expect("eval should succeed")
451    }
452
453    fn eval_with_var(expr: &str, name: &str, value: i64) -> i64 {
454        let mut scope = Scope::new();
455        scope.set(name, Value::Int(value));
456        eval_arithmetic(expr, &scope).expect("eval should succeed")
457    }
458
459    #[test]
460    fn test_simple_integers() {
461        assert_eq!(eval("42"), 42);
462        assert_eq!(eval("0"), 0);
463        assert_eq!(eval("12345"), 12345);
464    }
465
466    // ── Decision B: a bare subscript in arithmetic is a numeric expression ──
467    // `$(( xs[i] ))` reads variable `i` (the opposite of `${xs[i]}`, a literal
468    // key). Each bracket's inner is evaluated arithmetically to an index.
469
470    fn eval_with_scope(expr: &str, setup: impl FnOnce(&mut Scope)) -> Result<i64> {
471        let mut scope = Scope::new();
472        setup(&mut scope);
473        eval_arithmetic(expr, &scope)
474    }
475
476    #[test]
477    fn bare_subscript_index_is_a_variable() {
478        // xs = [10, 20, 30]; i = 1  →  xs[i] == 20
479        let r = eval_with_scope("xs[i]", |s| {
480            s.set("xs", Value::Json(serde_json::json!([10, 20, 30])));
481            s.set("i", Value::Int(1));
482        })
483        .expect("bare xs[i] should resolve via variable i");
484        assert_eq!(r, 20);
485    }
486
487    #[test]
488    fn bare_subscript_literal_index() {
489        let r = eval_with_scope("xs[0] + 1", |s| {
490            s.set("xs", Value::Json(serde_json::json!([10, 20, 30])));
491        })
492        .expect("xs[0] + 1");
493        assert_eq!(r, 11);
494    }
495
496    #[test]
497    fn bare_subscript_inner_is_an_expression() {
498        // xs[i + 1] with i = 0  →  xs[1] == 20
499        let r = eval_with_scope("xs[i + 1]", |s| {
500            s.set("xs", Value::Json(serde_json::json!([10, 20, 30])));
501            s.set("i", Value::Int(0));
502        })
503        .expect("xs[i + 1]");
504        assert_eq!(r, 20);
505    }
506
507    #[test]
508    fn bare_subscript_negative_index() {
509        let r = eval_with_scope("xs[-1]", |s| {
510            s.set("xs", Value::Json(serde_json::json!([10, 20, 30])));
511        })
512        .expect("xs[-1]");
513        assert_eq!(r, 30);
514    }
515
516    #[test]
517    fn bare_subscript_out_of_bounds_is_loud() {
518        let r = eval_with_scope("xs[9]", |s| {
519            s.set("xs", Value::Json(serde_json::json!([10, 20])));
520        });
521        assert!(r.is_err(), "out-of-bounds index must be a loud error");
522    }
523
524    #[test]
525    fn test_addition() {
526        assert_eq!(eval("1 + 2"), 3);
527        assert_eq!(eval("10 + 20 + 30"), 60);
528    }
529
530    #[test]
531    fn test_subtraction() {
532        assert_eq!(eval("10 - 3"), 7);
533        assert_eq!(eval("100 - 50 - 25"), 25);
534    }
535
536    #[test]
537    fn test_multiplication() {
538        assert_eq!(eval("3 * 4"), 12);
539        assert_eq!(eval("2 * 3 * 4"), 24);
540    }
541
542    #[test]
543    fn test_division() {
544        assert_eq!(eval("10 / 2"), 5);
545        assert_eq!(eval("100 / 10 / 2"), 5);
546    }
547
548    #[test]
549    fn test_modulo() {
550        assert_eq!(eval("10 % 3"), 1);
551        assert_eq!(eval("17 % 5"), 2);
552    }
553
554    #[test]
555    fn test_precedence() {
556        assert_eq!(eval("2 + 3 * 4"), 14); // Not 20
557        assert_eq!(eval("10 - 6 / 2"), 7); // Not 2
558    }
559
560    #[test]
561    fn test_parentheses() {
562        assert_eq!(eval("(2 + 3) * 4"), 20);
563        assert_eq!(eval("((1 + 2) * (3 + 4))"), 21);
564    }
565
566    #[test]
567    fn test_unary_minus() {
568        assert_eq!(eval("-5"), -5);
569        assert_eq!(eval("10 + -3"), 7);
570        assert_eq!(eval("--5"), 5);
571    }
572
573    #[test]
574    fn test_unary_plus() {
575        assert_eq!(eval("+5"), 5);
576        assert_eq!(eval("++5"), 5);
577    }
578
579    #[test]
580    fn test_whitespace() {
581        assert_eq!(eval("  1  +  2  "), 3);
582        assert_eq!(eval("1+2"), 3);
583    }
584
585    #[test]
586    fn test_variable_dollar() {
587        assert_eq!(eval_with_var("$X", "X", 10), 10);
588        assert_eq!(eval_with_var("$X + 5", "X", 10), 15);
589    }
590
591    #[test]
592    fn test_variable_dollar_braces() {
593        assert_eq!(eval_with_var("${X}", "X", 10), 10);
594        assert_eq!(eval_with_var("${X} * 2", "X", 10), 20);
595    }
596
597    #[test]
598    fn test_variable_bare() {
599        assert_eq!(eval_with_var("X", "X", 10), 10);
600        assert_eq!(eval_with_var("X + Y", "X", 10), 10); // Y is unset = 0
601    }
602
603    #[test]
604    fn test_unset_variable() {
605        let scope = Scope::new();
606        let result = eval_arithmetic("UNDEFINED", &scope).expect("should succeed");
607        assert_eq!(result, 0); // Unset variables default to 0
608    }
609
610    #[test]
611    fn test_division_by_zero() {
612        let scope = Scope::new();
613        let result = eval_arithmetic("10 / 0", &scope);
614        assert!(result.is_err());
615    }
616
617    #[test]
618    fn test_modulo_by_zero() {
619        let scope = Scope::new();
620        let result = eval_arithmetic("10 % 0", &scope);
621        assert!(result.is_err());
622    }
623
624    #[test]
625    fn test_complex_expression() {
626        assert_eq!(eval("(1 + 2) * (3 + 4) - 5"), 16);
627    }
628
629    // Comparison operator tests
630    #[test]
631    fn test_greater_than() {
632        assert_eq!(eval("5 > 3"), 1);
633        assert_eq!(eval("3 > 5"), 0);
634        assert_eq!(eval("5 > 5"), 0);
635    }
636
637    #[test]
638    fn test_less_than() {
639        assert_eq!(eval("3 < 5"), 1);
640        assert_eq!(eval("5 < 3"), 0);
641        assert_eq!(eval("5 < 5"), 0);
642    }
643
644    #[test]
645    fn test_greater_or_equal() {
646        assert_eq!(eval("5 >= 3"), 1);
647        assert_eq!(eval("5 >= 5"), 1);
648        assert_eq!(eval("3 >= 5"), 0);
649    }
650
651    #[test]
652    fn test_less_or_equal() {
653        assert_eq!(eval("3 <= 5"), 1);
654        assert_eq!(eval("5 <= 5"), 1);
655        assert_eq!(eval("5 <= 3"), 0);
656    }
657
658    #[test]
659    fn test_equal() {
660        assert_eq!(eval("5 == 5"), 1);
661        assert_eq!(eval("5 == 3"), 0);
662    }
663
664    #[test]
665    fn test_not_equal() {
666        assert_eq!(eval("5 != 3"), 1);
667        assert_eq!(eval("5 != 5"), 0);
668    }
669
670    #[test]
671    fn test_comparison_with_arithmetic() {
672        assert_eq!(eval("(2 + 3) > 4"), 1);
673        assert_eq!(eval("10 / 2 == 5"), 1);
674        assert_eq!(eval("3 * 4 >= 12"), 1);
675        assert_eq!(eval("10 - 5 < 6"), 1);
676    }
677
678    #[test]
679    fn test_comparison_with_variables() {
680        assert_eq!(eval_with_var("X > 5", "X", 10), 1);
681        assert_eq!(eval_with_var("X == 10", "X", 10), 1);
682        assert_eq!(eval_with_var("X <= 10", "X", 10), 1);
683    }
684
685    #[test]
686    fn test_chained_comparison() {
687        // Note: chained comparisons work left-to-right, not mathematically
688        // (5 > 3) > 2 = 1 > 2 = 0
689        assert_eq!(eval("5 > 3 > 2"), 0);
690        // (5 > 3) == 1 = 1 == 1 = 1
691        assert_eq!(eval("5 > 3 == 1"), 1);
692    }
693}