From 5b908c04f8b00ce4f9628bee76598cfeadba566b Mon Sep 17 00:00:00 2001 From: Higgs32584 Date: Wed, 21 Oct 2026 20:24:15 -0400 Subject: [PATCH] core: iterative number coercion for nested parentheses (#2180) Parenthesised negatives were parsed by recursing once per level, so runtime text such as REPT("(",32767)&"1"&REPT(")",32767) overflowed the stack. Strip the parentheses in a loop, toggling the sign per level; results are unchanged. Co-Authored-By: Claude Opus 5.5 --- crates/core/src/parse.rs | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/crates/core/src/parse.rs b/crates/core/src/parse.rs index b465beaf9..3be6d1f0b 100644 --- a/crates/core/src/parse.rs +++ b/crates/core/src/parse.rs @@ -50,13 +50,22 @@ pub fn parse_number_text_in(s: &str, sys: DateSystem) -> Option { } fn parse_numeric(t: &str, sys: DateSystem) -> Option<(f64, Option<&'static str>)> { + // Parenthesised negatives: (100) = -100. Each level negates, so ((100)) = 100. Stripped in a + // loop, not by recursion: runtime text (e.g. from REPT) can nest thousands deep (#2180). + let mut t = t; + let mut neg = false; + while let Some(inner) = t.strip_prefix('(').and_then(|x| x.strip_suffix(')')) { + t = inner.trim(); + neg = !neg; + } + let (n, f) = parse_unparenthesised(t, sys)?; + Some((if neg { -n } else { n }, f)) +} + +fn parse_unparenthesised(t: &str, sys: DateSystem) -> Option<(f64, Option<&'static str>)> { if t.is_empty() { return None; } - // Parenthesised negatives: (100) = -100. - if let Some(inner) = t.strip_prefix('(').and_then(|x| x.strip_suffix(')')) { - return parse_numeric(inner.trim(), sys).map(|(n, f)| (-n, f)); - } if let Some(r) = parse_plain(t) { return Some(r); } @@ -352,6 +361,24 @@ mod tests { assert_eq!(p(".5").value, Value::Number(0.5)); } + #[test] + fn deeply_nested_parentheses_do_not_overflow() { + // #2180: coercion used to recurse once per parenthesis and overflow the stack. + let depth = 100_000; + let s = format!("{}1{}", "(".repeat(depth), ")".repeat(depth)); + assert_eq!(parse_number_text(&s), Some(1.0)); + let odd = format!("{}1{}", "(".repeat(depth + 1), ")".repeat(depth + 1)); + assert_eq!(parse_number_text(&odd), Some(-1.0)); + let unbalanced = format!("{}1{}", "(".repeat(depth), ")".repeat(depth - 1)); + assert_eq!(parse_number_text(&unbalanced), None); + assert_eq!(p(&s).value, Value::Number(1.0)); + // Existing results are unchanged. + assert_eq!(parse_number_text("(100)"), Some(-100.0)); + assert_eq!(parse_number_text("((100))"), Some(100.0)); + assert_eq!(parse_number_text("( $1,200.50 )"), Some(-1200.5)); + assert_eq!(parse_number_text("()"), None); + } + #[test] fn texts_and_specials() { assert_eq!(p("hello").value, Value::text("hello"));