fix(core): iteratively clean rejected typed syntax depth
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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]";
|
||||
|
||||
Reference in New Issue
Block a user