Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 55 additions & 7 deletions src/bin/antlr4-rust-gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5184,9 +5184,12 @@ fn render_generated_rule_dispatch_with_rule_names(
.copied()
.unwrap_or_default()
{
// The interpreted fast path never consults the depth cap, so a
// configured bound overrides the ATN preference: correctness of
// the resource limit beats the long-call-chain optimization.
writeln!(
out,
" {index} if self.generated_only() => Some(self.parse_generated_rule_{index}_dispatch(precedence, allow_fallback)),"
" {index} if self.generated_only() || self.base.has_rule_depth_cap() => Some(self.parse_generated_rule_{index}_dispatch(precedence, allow_fallback)),"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the stale ATN-preferred dispatch test

This changes the emitted arm to include || self.base.has_rule_depth_cap(), but renders_atn_preferred_dispatch_only_for_generated_only_mode still requires the old substring without that condition. Consequently, cargo test --locked --features codegen --bin antlr4-rust-gen renders_atn_preferred_dispatch_only_for_generated_only_mode fails at src/bin/antlr4-rust-gen.rs:12593; update the assertion and its description to match the new guarded dispatch.

Useful? React with 👍 / 👎.

)
.expect("writing to a string cannot fail");
} else {
Expand Down Expand Up @@ -5224,10 +5227,18 @@ fn render_generated_rule_dispatch_with_rule_names(
};
// Rule nesting maps onto native call depth; sample remaining stack
// capacity at the shared dispatch boundary so deeply nested input
// grows onto a segmented stack instead of aborting the process.
// grows onto a segmented stack instead of aborting the process. The
// optional depth-cap probe stays inline-cheap (one `Option` check
// when unset) and its error, while absorbed by rule-level recovery
// like any rule failure, stays sticky until the top-level entry
// drains it — that pairing is what actually enforces the abort.
// Plain `if let` keeps generated output edition-2021 compatible.
writeln!(
out,
" if self.base.generated_rule_stack_check_due() {{\n \
" if let Some(error) = self.base.rule_depth_cap_violation() {{\n \
return Err(GeneratedRuleError::Fatal(error));\n \
}}\n \
if self.base.generated_rule_stack_check_due() {{\n \
antlr4_runtime::grow_generated_rule_stack(|| {target_call})\n \
}} else {{\n {target_call}\n }}"
)
Expand Down Expand Up @@ -5836,9 +5847,11 @@ fn render_generated_step(
{
// ATN-preferred child: route through `parse_rule_precedence_from_generated`.
// The rule's `parse_generated_rule` dispatch arm is guarded by
// `generated_only()`, so in normal mode the generated probe returns
// `None` and the wrapper parses the child on the INTERPRETED path
// (preserving the ATN-preferred optimization).
// `generated_only() || has_rule_depth_cap()`: in normal uncapped
// mode the generated probe returns `None` and the wrapper parses
// the child on the INTERPRETED path (preserving the ATN-preferred
// optimization); a configured depth cap flips it to the generated
// body, which is the only path that enforces the cap.
from_generated_call
} else {
generated_child_call
Expand Down Expand Up @@ -6928,6 +6941,19 @@ fn render_generated_left_recursive_loop(
)
.expect("writing to a string cannot fail");
}
// Each operator iteration deepens the tree without a rule frame; probe
// the depth cap BEFORE the expansion push so the boundary matches the
// dispatch site (which checks before its rule-frame push): frames and
// expansions are both admitted up to the cap, and token-only operator
// alternatives (no nested rule dispatch) still abort promptly instead of
// at the top-level drain.
writeln!(
out,
"{pad} if let Some(__depth_error) = self.base.rule_depth_cap_violation() {{\n\
{pad} return Err(__depth_error);\n\
{pad} }}"
)
.expect("writing to a string cannot fail");
writeln!(
out,
"{pad} self.base.push_new_recursion_context_with_previous({entry_state}isize, {rule_index}, &mut __ctx);"
Expand Down Expand Up @@ -9746,6 +9772,10 @@ where
// are preserved so a generated parent can surface a recovered child's
// fail-loud coordinate at this boundary.
self.base.reset_unknown_semantic_hits();
// Likewise drop a stale depth-cap violation: entry rules share one
// parser instance, and the sticky flag must not poison the next
// parse when the previous one exited through an error path.
let _ = self.base.take_rule_depth_error();
}}
let __rule_start = antlr4_runtime::IntStream::index(self.base.input());
let __generated_only = self.generated_only();
Expand All @@ -9766,6 +9796,14 @@ where
if let Some(semantic_error) = self.base.take_unknown_semantic_error() {{
return Err(semantic_error);
}}
// The depth-cap violation wins over an error derived
// from it (e.g. a sync failure after recovery absorbed
// the capped rule): the caller must learn the resource
// bound was hit, and draining un-poisons the instance
// for the next entry-rule call.
if let Some(depth_error) = self.base.take_rule_depth_error() {{
return Err(depth_error);
}}
}}
return Err(error.into_error());
}}
Expand All @@ -9790,6 +9828,12 @@ where
if let Some(error) = self.base.take_unknown_semantic_error() {{
return Err(error);
}}
// A depth-cap violation is a resource bound, not a syntax error:
// rule-level recovery may have produced a tree anyway, but the
// parse must still fail (and a reused parser must start clean).
if let Some(error) = self.base.take_rule_depth_error() {{
return Err(error);
}}
}}
Ok(__tree)
}}
Expand Down Expand Up @@ -9862,6 +9906,8 @@ where
fn set_report_diagnostic_errors(&mut self, report: bool) {{ self.base.set_report_diagnostic_errors(report); }}
fn prediction_mode(&self) -> antlr4_runtime::PredictionMode {{ self.base.prediction_mode() }}
fn set_prediction_mode(&mut self, mode: antlr4_runtime::PredictionMode) {{ self.base.set_prediction_mode(mode); }}
fn max_rule_depth(&self) -> Option<usize> {{ self.base.max_rule_depth() }}
fn set_max_rule_depth(&mut self, depth: Option<usize>) {{ self.base.set_max_rule_depth(depth); }}
}}
{generated_footer}"#
))
Expand Down Expand Up @@ -12552,8 +12598,10 @@ mod tests {
None,
);

// A configured depth cap overrides the ATN preference: only generated
// bodies enforce the bound, so the guard admits either trigger.
assert!(rendered.contains(
"0 if self.generated_only() => Some(self.parse_generated_rule_0_dispatch(precedence, allow_fallback))"
"0 if self.generated_only() || self.base.has_rule_depth_cap() => Some(self.parse_generated_rule_0_dispatch(precedence, allow_fallback))"
));
assert!(!rendered.contains(
"0 => Some(self.parse_generated_rule_0_dispatch(precedence, allow_fallback))"
Expand Down
138 changes: 138 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,29 @@ pub trait Parser: Recognizer {

/// Sets the prediction strategy for subsequent rule calls.
fn set_prediction_mode(&mut self, _mode: PredictionMode) {}

/// Maximum rule-nesting depth accepted before the parse aborts, or `None`
/// for unlimited (the default).
fn max_rule_depth(&self) -> Option<usize> {
None
}

/// Bounds the rule-nesting depth for subsequent rule calls.
///
/// Deeply nested input is parsed safely regardless (rule recursion grows
/// onto a segmented stack), but each nesting level still costs CPU and
/// tree memory. Callers parsing untrusted input can cap that work: when
/// the limit is exceeded the parse stops with a positioned syntax error
/// instead of consuming unbounded resources. The measure counts rule
/// frames plus left-recursive operator expansions, matching what an
/// upstream-ANTLR rule-entry listener observes.
///
/// The cap is enforced by generated recursive-descent rule bodies. When
/// one is set, generated dispatch routes ATN-preferred rules through
/// their generated bodies too, trading that fast path for enforcement.
/// Rules the generator emitted no body for (interpreter-only fallback)
/// do not check the cap.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
fn set_max_rule_depth(&mut self, _depth: Option<usize>) {}
}

#[derive(Debug)]
Expand Down Expand Up @@ -1180,6 +1203,25 @@ pub struct BaseParser<S, H = NoSemanticHooks> {
/// recovering (ANTLR's `BailErrorStrategy`). Generated recognizers set it
/// through `set_error_handler(BailErrorStrategy::new())`.
bail_on_error: bool,
/// Optional cap on rule-nesting depth for adversarial-input hardening.
/// `None` (default) parses unbounded nesting; `Some(n)` aborts the parse
/// with a positioned syntax error once `n` rule frames are exceeded.
max_rule_depth: Option<usize>,
/// Sticky depth-cap violation. Rule-level recovery would otherwise absorb
/// the error and keep parsing; once set, every subsequent rule entry fails
/// immediately and the top-level entry returns this error even when
/// recovery produced a tree.
rule_depth_error: Option<AntlrError>,
/// Left-recursive expansions currently deepening the parse tree. Each
/// operator iteration wraps the previous context one level deeper without
/// pushing a rule frame, so the depth cap must count these separately —
/// upstream ANTLR fires a rule-entry listener event for exactly this case
/// (`Parser.pushNewRecursionContext` → `triggerEnterRuleEvent`).
recursion_expansions: usize,
/// Per-invocation snapshots of [`Self::recursion_expansions`], pushed by
/// `enter_recursion_rule` and restored by `unroll_recursion_context`, so a
/// finished left-recursive rule releases the depth its expansions added.
recursion_expansion_marks: Vec<usize>,
/// How to evaluate predicate coordinates missing from the active
/// predicate table. Set from [`ParserRuntimeOptions`] at each parse entry.
unknown_predicate_policy: UnknownSemanticPolicy,
Expand Down Expand Up @@ -4793,6 +4835,10 @@ where
precedence_stack: vec![0],
invoked_predicates: Vec::new(),
bail_on_error: false,
max_rule_depth: None,
rule_depth_error: None,
recursion_expansions: 0,
recursion_expansion_marks: Vec::new(),
unknown_predicate_policy: UnknownSemanticPolicy::default(),
unknown_predicate_hits: Vec::new(),
unhandled_action_hits: Vec::new(),
Expand Down Expand Up @@ -4852,6 +4898,9 @@ where
self.decision_override_generation = 0;
self.unknown_predicate_hits.clear();
self.unhandled_action_hits.clear();
self.rule_depth_error = None;
self.recursion_expansions = 0;
self.recursion_expansion_marks.clear();
self.reset_per_parse_caches();
self.fast_first_set_prefilter = true;
self.fast_recovery_enabled = true;
Expand Down Expand Up @@ -5715,6 +5764,75 @@ where
.is_multiple_of(GENERATED_RULE_STACK_CHECK_INTERVAL)
}

/// Returns the positioned error to abort with when the configured
/// rule-nesting depth cap would be exceeded by one more level, or `None`
/// to keep parsing.
///
/// Generated rule dispatch calls this before deepening — ahead of the
/// rule-frame push at the dispatch boundary and ahead of each
/// left-recursive expansion — letting callers parsing untrusted input
/// bound CPU and tree memory ([`Parser::set_max_rule_depth`]). The
/// inline fast path is one `Option` check when no cap is set (the
/// default) and one addition plus compare when one is; only an actual
/// violation leaves the inline path.
///
/// The violation is sticky: rule-level recovery absorbs the returned
/// error like any other rule failure and would otherwise keep spending
/// the very resources the cap exists to bound, so every check after the
/// first violation fails until [`Self::take_rule_depth_error`] drains it
/// at the top-level entry.
#[inline]
pub fn rule_depth_cap_violation(&mut self) -> Option<AntlrError> {
let max = self.max_rule_depth?;
// Left-recursive operator iterations deepen the tree without pushing
// a rule frame, so they count alongside the rule-context stack.
if self.rule_depth_error.is_none()
&& self.rule_context_stack.len() + self.recursion_expansions < max
{
return None;
}
Some(self.rule_depth_cap_violation_cold(max))
}

#[cold]
fn rule_depth_cap_violation_cold(&mut self, max: usize) -> AntlrError {
if let Some(error) = &self.rule_depth_error {
return error.clone();
}
let (line, column) = self
.input
.lt(1)
.map_or((0, 0), |token| (token.line(), token.column()));
let error = AntlrError::ParserError {
line,
column,
message: format!("rule nesting depth limit of {max} exceeded"),
};
self.rule_depth_error = Some(error.clone());
error
}

/// Drains the sticky depth-cap violation recorded by
/// [`Self::rule_depth_cap_violation`], if any.
///
/// Generated top-level rule entries call this after recognition so a
/// recovered parse that crossed the cap still fails, and so a reused
/// parser starts its next parse clean.
pub const fn take_rule_depth_error(&mut self) -> Option<AntlrError> {
self.rule_depth_error.take()
}

/// Reports whether a rule-nesting depth cap is configured.
///
/// Generated dispatch consults this when selecting between the guarded
/// recursive-descent body and the ATN-preferred interpreted fast path:
/// only the generated body enforces the cap, so a configured bound
/// overrides the performance preference.
#[must_use]
pub const fn has_rule_depth_cap(&self) -> bool {
self.max_rule_depth.is_some()
}

/// Enters a generated parser rule and returns the context object the
/// generated method should populate.
pub fn enter_rule(&mut self, state: isize, rule_index: usize) -> ParserRuleContext {
Expand Down Expand Up @@ -5945,6 +6063,8 @@ where
precedence: i32,
) -> ParserRuleContext {
self.precedence_stack.push(precedence);
self.recursion_expansion_marks
.push(self.recursion_expansions);
self.enter_rule(state, rule_index)
}

Expand All @@ -5955,6 +6075,9 @@ where
rule_index: usize,
) -> ParserRuleContext {
self.set_state(state);
// Counts toward the depth cap: upstream treats this as rule entry
// (`Parser.pushNewRecursionContext` fires `triggerEnterRuleEvent`).
self.recursion_expansions += 1;
ParserRuleContext::new(rule_index, state)
}

Expand All @@ -5967,6 +6090,10 @@ where
current: &mut ParserRuleContext,
) {
self.set_state(state);
// Counts toward the depth cap: each operator iteration deepens the
// parse tree one level without pushing a rule frame, and upstream
// fires a rule-entry listener event for it.
self.recursion_expansions += 1;
if let Some(stop) = self
.rule_stop_token_index(self.input.index(), false)
.and_then(|index| self.token_id_at(index))
Expand All @@ -5991,6 +6118,9 @@ where
if self.precedence_stack.len() > 1 {
self.precedence_stack.pop();
}
if let Some(mark) = self.recursion_expansion_marks.pop() {
self.recursion_expansions = mark;
}
self.exit_rule();
}

Expand Down Expand Up @@ -12512,6 +12642,14 @@ where
fn set_prediction_mode(&mut self, mode: PredictionMode) {
self.prediction_mode = mode;
}

fn max_rule_depth(&self) -> Option<usize> {
self.max_rule_depth
}

fn set_max_rule_depth(&mut self, depth: Option<usize>) {
self.max_rule_depth = depth;
}
}

#[cfg(test)]
Expand Down
Loading
Loading