Skip to content

Commit 4b4850d

Browse files
committed
Fix exponential backtracking in parser for nested function calls
Parsing deeply nested expressions like F[F[F[...F[x]...]]] took O(2^d) time because ImplicitTimes parsed the full expression via FunctionCall, failed, then FunctionCallExtended re-parsed it. Merging implicit multiplication into FunctionCallExtended as a suffix and prioritizing it over ImplicitTimes in Term eliminates the redundant parsing. 20-level nesting now completes in ~2ms instead of ~11s.
1 parent 024b680 commit 4b4850d

File tree

3 files changed

+135
-5
lines changed

3 files changed

+135
-5
lines changed

src/syntax.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,6 +1392,12 @@ pub fn pair_to_expr(pair: Pair<Rule>) -> Expr {
13921392
let has_part_anon_suffix = inner_pairs
13931393
.iter()
13941394
.any(|p| matches!(p.as_rule(), Rule::FunctionCallPartAnonSuffix));
1395+
let has_implicit_suffix = inner_pairs
1396+
.iter()
1397+
.any(|p| matches!(p.as_rule(), Rule::FunctionCallImplicitSuffix));
1398+
let has_implicit_power = inner_pairs
1399+
.iter()
1400+
.any(|p| matches!(p.as_rule(), Rule::ImplicitPowerSuffix));
13951401

13961402
// Extract part indices if present
13971403
let part_indices: Vec<Expr> = inner_pairs
@@ -1489,6 +1495,53 @@ pub fn pair_to_expr(pair: Pair<Rule>) -> Expr {
14891495
}
14901496
};
14911497

1498+
// Helper: parse FunctionCallImplicitSuffix inner pairs into multiplication factors
1499+
// (same logic as ImplicitTimes handler)
1500+
let parse_implicit_factors =
1501+
|suffix_pair: &pest::iterators::Pair<Rule>| -> Vec<Expr> {
1502+
let inners: Vec<_> = suffix_pair.clone().into_inner().collect();
1503+
let mut factors: Vec<Expr> = Vec::new();
1504+
let mut i = 0;
1505+
while i < inners.len() {
1506+
if inners[i].as_rule() == Rule::PartIndexSuffix {
1507+
if let Some(base) = factors.pop() {
1508+
let mut result = base;
1509+
for idx_pair in inners[i].clone().into_inner() {
1510+
let index = pair_to_expr(idx_pair);
1511+
result = Expr::Part {
1512+
expr: Box::new(result),
1513+
index: Box::new(index),
1514+
};
1515+
}
1516+
factors.push(result);
1517+
}
1518+
} else if inners[i].as_rule() == Rule::ImplicitPowerSuffix {
1519+
if let Some(base) = factors.pop() {
1520+
let exponent =
1521+
pair_to_expr(inners[i].clone().into_inner().next().unwrap());
1522+
factors.push(Expr::BinaryOp {
1523+
op: BinaryOperator::Power,
1524+
left: Box::new(base),
1525+
right: Box::new(exponent),
1526+
});
1527+
}
1528+
} else {
1529+
factors.push(pair_to_expr(inners[i].clone()));
1530+
}
1531+
i += 1;
1532+
}
1533+
factors
1534+
};
1535+
1536+
// Helper: fold a base expression with implicit multiplication factors into nested Times
1537+
let fold_implicit_times = |base: Expr, factors: Vec<Expr>| -> Expr {
1538+
factors.into_iter().fold(base, |acc, f| Expr::BinaryOp {
1539+
op: BinaryOperator::Times,
1540+
left: Box::new(acc),
1541+
right: Box::new(f),
1542+
})
1543+
};
1544+
14921545
if has_part_index && has_part_anon_suffix {
14931546
// PartAnonymousFunction: f[x][[i]] op ... &[args]
14941547
let mut part_result = base_func;
@@ -1518,6 +1571,38 @@ pub fn pair_to_expr(pair: Pair<Rule>) -> Expr {
15181571
.unwrap();
15191572
let anon_brackets = extract_suffix_brackets(suffix_pair);
15201573
make_anon_func(body, anon_brackets)
1574+
} else if has_part_index && has_implicit_suffix {
1575+
// PartExtract with implicit multiplication: f[x][[i]]^2 y
1576+
let mut result = base_func;
1577+
for idx in &part_indices {
1578+
result = Expr::Part {
1579+
expr: Box::new(result),
1580+
index: Box::new(idx.clone()),
1581+
};
1582+
}
1583+
if has_implicit_power {
1584+
let exponent = pair_to_expr(
1585+
inner_pairs
1586+
.iter()
1587+
.find(|p| matches!(p.as_rule(), Rule::ImplicitPowerSuffix))
1588+
.unwrap()
1589+
.clone()
1590+
.into_inner()
1591+
.next()
1592+
.unwrap(),
1593+
);
1594+
result = Expr::BinaryOp {
1595+
op: BinaryOperator::Power,
1596+
left: Box::new(result),
1597+
right: Box::new(exponent),
1598+
};
1599+
}
1600+
let suffix_pair = inner_pairs
1601+
.iter()
1602+
.find(|p| matches!(p.as_rule(), Rule::FunctionCallImplicitSuffix))
1603+
.unwrap();
1604+
let factors = parse_implicit_factors(suffix_pair);
1605+
fold_implicit_times(result, factors)
15211606
} else if has_part_index {
15221607
// Plain PartExtract: f[x][[i]]
15231608
let mut result = base_func;
@@ -1545,6 +1630,32 @@ pub fn pair_to_expr(pair: Pair<Rule>) -> Expr {
15451630
.unwrap();
15461631
let anon_brackets = extract_suffix_brackets(suffix_pair);
15471632
make_anon_func(body, anon_brackets)
1633+
} else if has_implicit_suffix {
1634+
// Implicit multiplication after function call: f[x] g[y] or f[x]^2 y
1635+
let mut result = base_func;
1636+
if has_implicit_power {
1637+
let exponent = pair_to_expr(
1638+
inner_pairs
1639+
.iter()
1640+
.find(|p| matches!(p.as_rule(), Rule::ImplicitPowerSuffix))
1641+
.unwrap()
1642+
.clone()
1643+
.into_inner()
1644+
.next()
1645+
.unwrap(),
1646+
);
1647+
result = Expr::BinaryOp {
1648+
op: BinaryOperator::Power,
1649+
left: Box::new(result),
1650+
right: Box::new(exponent),
1651+
};
1652+
}
1653+
let suffix_pair = inner_pairs
1654+
.iter()
1655+
.find(|p| matches!(p.as_rule(), Rule::FunctionCallImplicitSuffix))
1656+
.unwrap();
1657+
let factors = parse_implicit_factors(suffix_pair);
1658+
fold_implicit_times(result, factors)
15481659
} else {
15491660
// Plain FunctionCall
15501661
base_func

src/wolfram.pest

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,23 +231,25 @@ DerivativeIdentifier = { Identifier ~ DerivativePrime }
231231
// Named suffix rules so pair_to_expr can detect them reliably.
232232
FunctionCallAnonSuffix = { "&" ~ !"&" ~ BracketArgs* }
233233
FunctionCallPartAnonSuffix = { (Operator ~ SlotTerm)* ~ "&" ~ !"&" ~ BracketArgs* }
234+
FunctionCallImplicitSuffix = { ImplicitTimesFactor+ }
234235
FunctionCallExtended = {
235236
Identifier ~ DerivativePrime? ~ BracketArgs+
236237
~ (
237-
PartIndexSuffix ~ FunctionCallPartAnonSuffix?
238+
PartIndexSuffix ~ (FunctionCallPartAnonSuffix | ImplicitPowerSuffix? ~ FunctionCallImplicitSuffix)?
238239
| FunctionCallAnonSuffix
240+
| ImplicitPowerSuffix? ~ FunctionCallImplicitSuffix
239241
)?
240242
}
241243

242244
Term = _{
243-
ImplicitTimes
245+
FunctionCallExtended
246+
| ImplicitTimes
244247
| PartAnonymousFunction
245248
| PartExtract
246249
| Constant
247250
| String
248251
| ListExtended
249252
| ParenAnonymousFunction
250-
| FunctionCallExtended
251253
| NumericValue
252254
| Unset
253255
| AddTo
@@ -288,14 +290,14 @@ SimpleTerm = _{
288290
// TermNoImplicit is like Term but without ImplicitTimes - used for function bodies
289291
// to prevent greedy matching across statement boundaries
290292
TermNoImplicit = _{
291-
ImplicitTimes
293+
FunctionCallExtended
294+
| ImplicitTimes
292295
| PartAnonymousFunction
293296
| PartExtract
294297
| Constant
295298
| String
296299
| ListExtended
297300
| ParenAnonymousFunction
298-
| FunctionCallExtended
299301
| NumericValue
300302
| Unset
301303
| AddTo

tests/parser_tests.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,21 @@ mod tests {
110110
let pair = parse(input).unwrap().next().unwrap();
111111
assert_eq!(pair.as_rule(), Rule::Program);
112112
}
113+
114+
#[test]
115+
fn test_parse_deeply_nested_function_calls() {
116+
// Regression: deeply nested function calls must parse in linear time, not O(2^d).
117+
// At 30 levels, exponential backtracking would take hours; this must finish instantly.
118+
let input = "F[".repeat(30) + "x" + &"]".repeat(30);
119+
let pair = parse(&input).unwrap().next().unwrap();
120+
assert_eq!(pair.as_rule(), Rule::Program);
121+
}
122+
123+
#[test]
124+
fn test_parse_deeply_nested_with_implicit_times() {
125+
// Ensure implicit multiplication still works correctly with deep nesting
126+
let input = "F[".repeat(20) + "x" + &"]".repeat(20) + " y";
127+
let pair = parse(&input).unwrap().next().unwrap();
128+
assert_eq!(pair.as_rule(), Rule::Program);
129+
}
113130
}

0 commit comments

Comments
 (0)