From 1b1c3e3abb6fc3d0934c9dbfbd21fddc8ae4f594 Mon Sep 17 00:00:00 2001 From: "sorokin.oleg.edu" Date: Tue, 22 Sep 2026 12:00:06 -0700 Subject: [PATCH] fix(core): iteratively clean rejected typed syntax depth --- .../src/language/analysis_source/syntax.rs | 41 ++++++++++++++++++ .../src/language/analysis_source/tests.rs | 43 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/crates/oa-core/src/language/analysis_source/syntax.rs b/crates/oa-core/src/language/analysis_source/syntax.rs index 70ac42b7..63166d01 100644 --- a/crates/oa-core/src/language/analysis_source/syntax.rs +++ b/crates/oa-core/src/language/analysis_source/syntax.rs @@ -349,6 +349,11 @@ impl Cursor<'_, '_> { _ => 0, }; if child_depth >= crate::analysis::MAX_EXPRESSION_DEPTH { + // The rejected parent still owns accepted, potentially depth-128 + // children. Recursive derived cleanup here runs beneath the parser + // frames and can exhaust the bounded native stack before returning + // the depth error. Consume those children with an explicit worklist. + drop_rejected_kind(kind); return Err(error(span, "Expression logical depth exceeds 128.")); } Ok(Expression { @@ -399,6 +404,42 @@ impl Cursor<'_, '_> { self.node(kind, span) } } +fn drop_rejected_kind(kind: ExpressionKind) { + let mut pending = vec![kind]; + while let Some(kind) = pending.pop() { + match kind { + ExpressionKind::Unary { operand, .. } => pending.push(operand.kind), + ExpressionKind::Binary { left, right, .. } => { + pending.push(left.kind); + pending.push(right.kind); + } + ExpressionKind::If { + condition, + when_true, + when_false, + } => { + pending.push(condition.kind); + pending.push(when_true.kind); + pending.push(when_false.kind); + } + ExpressionKind::List(values) => pending.extend(values.into_iter().map(|v| v.kind)), + ExpressionKind::Call { arguments, .. } => { + pending.extend(arguments.into_iter().map(|a| a.value.kind)); + } + ExpressionKind::Sum { values, initial } => { + pending.push(values.kind); + pending.push(initial.kind); + } + ExpressionKind::Power { base, .. } => pending.push(base.kind), + ExpressionKind::AsKind { value, .. } => pending.push(value.kind), + ExpressionKind::Boolean(_) + | ExpressionKind::Number(_) + | ExpressionKind::Quantity { .. } + | ExpressionKind::Property(_) + | ExpressionKind::Parameter(_) => {} + } + } +} fn comparison(op: BinaryOp) -> bool { matches!( op, diff --git a/crates/oa-core/src/language/analysis_source/tests.rs b/crates/oa-core/src/language/analysis_source/tests.rs index b1c9bc3f..20fee6ac 100644 --- a/crates/oa-core/src/language/analysis_source/tests.rs +++ b/crates/oa-core/src/language/analysis_source/tests.rs @@ -108,6 +108,49 @@ fn flat_binary_depth_and_malformed_constructor_retain_bounded_completed_children assert_eq!(number(&expression("1")), "1"); } +#[test] +fn every_parent_shape_rejects_depth129_and_cleans_children_on_128k_stack() { + std::thread::Builder::new() + .name("analysis-syntax-rejection-128k".into()) + .stack_size(128 * 1024) + .spawn(|| { + // Rotate through every recursive syntax shape, so the rejected + // parent's complete child tree also exercises every cleanup arm. + let wrap = |shape, child: &str| match shape { + 0 => format!("abs({child})"), + 1 => format!("({child}) + 1"), + 2 => format!("if(true, {child}, 0)"), + 3 => format!("[{child}]"), + 4 => format!("@f(a: {child})"), + 5 => format!("sum({child}, 0)"), + 6 => format!("power({child}, 1)"), + 7 => format!("as_kind(@k, {child})"), + _ => unreachable!(), + }; + let mut deep = "1".to_owned(); + for layer in 0..127 { + deep = wrap(layer % 8, &deep); + } + assert_eq!(expression(&deep).depth, 128); + for shape in 0..8 { + let source = wrap(shape, &deep); + let failure = parse(&source, &mut budget()).unwrap_err(); + assert_eq!(failure.message, "Expression logical depth exceeds 128."); + assert_eq!( + failure.span, + Span { + start: 0, + end: source.len() + } + ); + assert_eq!(number(&expression("1")), "1"); + } + }) + .unwrap() + .join() + .unwrap(); +} + #[test] fn significant_ratio_and_quantity_trivia_preserve_original_spans() { let source = "ratio // explanation café\n ( - // sign\n 3, // denominator\n 4 ) // measurement\n [@millisecond]";