Skip to content

Commit ede6774

Browse files
committed
feat(interpreter): implement recursive variable deref and array access in arithmetic
Closes #361 - Recursive variable dereferencing: bare names in $((...)) are recursively resolved (b=a; a=3; $((b+1)) -> 4) - Expression re-evaluation: variable values containing expressions are evaluated as arithmetic sub-expressions (x='1+2'; $((x*3)) -> 9) - Array element access: arr[expr] in arithmetic evaluates the index expression and looks up the array element - Quoted substitution: "$x" in arithmetic strips quotes, behaves as $x - THREAT[TM-DOS-026]: depth limit prevents infinite recursion
1 parent 3cca974 commit ede6774

File tree

2 files changed

+82
-10
lines changed

2 files changed

+82
-10
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5774,8 +5774,49 @@ impl Interpreter {
57745774
self.parse_arithmetic_impl(&expanded, 0)
57755775
}
57765776

5777+
/// Recursively resolve a variable value in arithmetic context.
5778+
/// In bash arithmetic, bare variable names are recursively evaluated:
5779+
/// if b=a and a=3, then $((b)) evaluates b -> "a" -> 3.
5780+
/// If x='1 + 2', then $((x)) evaluates x -> "1 + 2" -> 3 (as sub-expression).
5781+
/// THREAT[TM-DOS-026]: `depth` prevents infinite recursion.
5782+
fn resolve_arith_var(&self, value: &str, depth: usize) -> String {
5783+
if depth >= Self::MAX_ARITHMETIC_DEPTH {
5784+
return "0".to_string();
5785+
}
5786+
let trimmed = value.trim();
5787+
if trimmed.is_empty() {
5788+
return "0".to_string();
5789+
}
5790+
// If value is a simple integer, return it directly
5791+
if trimmed.parse::<i64>().is_ok() {
5792+
return trimmed.to_string();
5793+
}
5794+
// If value looks like a variable name, recursively dereference
5795+
if Self::is_valid_var_name(trimmed) {
5796+
let inner = self.expand_variable(trimmed);
5797+
return self.resolve_arith_var(&inner, depth + 1);
5798+
}
5799+
// Value contains an expression (e.g. "1 + 2") — expand vars in it
5800+
// and wrap in parens to preserve grouping
5801+
let expanded = self.expand_arithmetic_vars_depth(trimmed, depth + 1);
5802+
format!("({})", expanded)
5803+
}
5804+
57775805
/// Expand variables in arithmetic expression (no $ needed in $((...)))
57785806
fn expand_arithmetic_vars(&self, expr: &str) -> String {
5807+
self.expand_arithmetic_vars_depth(expr, 0)
5808+
}
5809+
5810+
/// Inner implementation with depth tracking for recursive expansion.
5811+
/// THREAT[TM-DOS-026]: `depth` prevents stack overflow via recursive variable values.
5812+
fn expand_arithmetic_vars_depth(&self, expr: &str, depth: usize) -> String {
5813+
if depth >= Self::MAX_ARITHMETIC_DEPTH {
5814+
return "0".to_string();
5815+
}
5816+
5817+
// Strip double quotes — "$x" in arithmetic is the same as $x
5818+
let expr = expr.replace('"', "");
5819+
57795820
let mut result = String::new();
57805821
let mut chars = expr.chars().peekable();
57815822
// Track whether we're in a numeric literal context (after # or 0x)
@@ -5794,6 +5835,8 @@ impl Interpreter {
57945835
}
57955836
}
57965837
if !name.is_empty() {
5838+
// $var is direct text substitution — no recursive arithmetic eval.
5839+
// Only bare names (without $) get recursive resolution.
57975840
let value = self.expand_variable(&name);
57985841
if value.is_empty() {
57995842
result.push('0');
@@ -5833,12 +5876,46 @@ impl Interpreter {
58335876
break;
58345877
}
58355878
}
5836-
// Expand the variable
5837-
let value = self.expand_variable(&name);
5838-
if value.is_empty() {
5839-
result.push('0');
5879+
// Check for array access: name[expr]
5880+
if chars.peek() == Some(&'[') {
5881+
chars.next(); // consume '['
5882+
let mut index_expr = String::new();
5883+
let mut bracket_depth = 1;
5884+
while let Some(&c) = chars.peek() {
5885+
chars.next();
5886+
if c == '[' {
5887+
bracket_depth += 1;
5888+
index_expr.push(c);
5889+
} else if c == ']' {
5890+
bracket_depth -= 1;
5891+
if bracket_depth == 0 {
5892+
break;
5893+
}
5894+
index_expr.push(c);
5895+
} else {
5896+
index_expr.push(c);
5897+
}
5898+
}
5899+
// Evaluate the index expression as arithmetic
5900+
let idx = self.evaluate_arithmetic(&index_expr);
5901+
// Look up array element
5902+
if let Some(arr) = self.arrays.get(&name) {
5903+
let idx_usize: usize = idx.try_into().unwrap_or(0);
5904+
let value = arr.get(&idx_usize).cloned().unwrap_or_default();
5905+
result.push_str(&self.resolve_arith_var(&value, depth));
5906+
} else {
5907+
// Not an array — treat as scalar (index 0 returns the var value)
5908+
let value = self.expand_variable(&name);
5909+
if idx == 0 {
5910+
result.push_str(&self.resolve_arith_var(&value, depth));
5911+
} else {
5912+
result.push('0');
5913+
}
5914+
}
58405915
} else {
5841-
result.push_str(&value);
5916+
// Expand the variable with recursive arithmetic resolution
5917+
let value = self.expand_variable(&name);
5918+
result.push_str(&self.resolve_arith_var(&value, depth));
58425919
}
58435920
} else {
58445921
in_numeric_literal = false;

crates/bashkit/tests/spec_cases/bash/arith-dynamic.test.sh

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ echo $(( x + 2 * 3 ))
1212

1313
### arith_dyn_var_expression
1414
# Variable containing expression is re-evaluated
15-
### skip: TODO arithmetic variable re-evaluation of expressions not implemented
1615
x='1 + 2'
1716
echo $(( x * 3 ))
1817
### expect
@@ -29,7 +28,6 @@ echo $(( $x * 3 ))
2928

3029
### arith_dyn_quoted_substitution
3130
# "$x" in arithmetic
32-
### skip: TODO double-quoted substitution in arithmetic not implemented
3331
x='1 + 2'
3432
echo $(( "$x" * 3 ))
3533
### expect
@@ -38,7 +36,6 @@ echo $(( "$x" * 3 ))
3836

3937
### arith_dyn_nested_var
4038
# Nested variable reference in arithmetic
41-
### skip: TODO recursive variable dereferencing in arithmetic not implemented
4239
a=3
4340
b=a
4441
echo $(( b + 1 ))
@@ -48,7 +45,6 @@ echo $(( b + 1 ))
4845

4946
### arith_dyn_array_index
5047
# Dynamic array index
51-
### skip: TODO array access in arithmetic expressions not implemented
5248
arr=(10 20 30 40)
5349
i=2
5450
echo $(( arr[i] ))
@@ -58,7 +54,6 @@ echo $(( arr[i] ))
5854

5955
### arith_dyn_array_index_expr
6056
# Expression as array index
61-
### skip: TODO array access in arithmetic expressions not implemented
6257
arr=(10 20 30 40)
6358
echo $(( arr[1+1] ))
6459
### expect

0 commit comments

Comments
 (0)