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
26 changes: 14 additions & 12 deletions src/bin/antlr4-rust-gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5216,20 +5216,22 @@ fn render_generated_rule_dispatch_with_rule_names(
"\n #[allow(dead_code)]\n fn parse_generated_rule_{index}_dispatch(&mut self, precedence: i32, allow_fallback: bool) -> Result<antlr4_runtime::ParseTree, GeneratedRuleError> {{"
)
.expect("writing to a string cannot fail");
if rule.left_recursive {
writeln!(
out,
" self.parse_generated_rule_{index}_precedence(precedence, allow_fallback)"
)
.expect("writing to a string cannot fail");
let target_call = if rule.left_recursive {
format!("self.parse_generated_rule_{index}_precedence(precedence, allow_fallback)")
} else {
writeln!(out, " let _ = precedence;").expect("writing to a string cannot fail");
writeln!(
out,
" self.parse_generated_rule_{index}(precedence, allow_fallback)"
)
.expect("writing to a string cannot fail");
}
format!("self.parse_generated_rule_{index}(precedence, allow_fallback)")
};
// 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.
writeln!(
out,
" if self.base.generated_rule_stack_check_due() {{\n \
antlr4_runtime::grow_generated_rule_stack(|| {target_call})\n \
}} else {{\n {target_call}\n }}"
)
.expect("writing to a string cannot fail");
writeln!(out, " }}").expect("writing to a string cannot fail");
render_generated_rule_method(&mut out, rule, step_render_context);
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub use parser::{
BailErrorStrategy, BaseParser, ExpectedTokenSet, NoSemanticHooks, Parser, ParserAction,
ParserMemberAction, ParserPredicate, ParserReturnAction, ParserRuleArg, ParserRuntimeOptions,
ParserSemCtx, ParserSemanticAction, ParserSemanticPredicate, ParserSemantics, PredictionMode,
RecognitionArenaStats, SemanticHooks, UnknownSemanticPolicy,
RecognitionArenaStats, SemanticHooks, UnknownSemanticPolicy, grow_generated_rule_stack,
};
#[cfg(feature = "perf-counters")]
pub use perf::{dump as dump_prediction_perf_counters, reset as reset_prediction_perf_counters};
Expand Down
31 changes: 31 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,26 @@ const RECOGNITION_DEPTH_LIMIT: usize = 32_768;
const FAST_RECOGNIZE_STACK_CHECK_INTERVAL: usize = 8;
const FAST_RECOGNIZE_RED_ZONE: usize = 1024 * 1024;
const FAST_RECOGNIZE_STACK_SIZE: usize = 4 * 1024 * 1024;
/// Generated recursive-descent rule methods map grammar-rule nesting onto
/// native call depth. Their `_dispatch` boundary samples remaining stack
/// capacity once per this many rule-context frames, so between two samples at
/// most this many rule bodies of native growth can occur — far below the
/// red zone.
const GENERATED_RULE_STACK_CHECK_INTERVAL: usize = 8;
/// Whole-rule direct adaptive execution is allowed to give up and fall back to
/// the existing recognizer. Keep the guard at the same order of magnitude as
/// speculative recognition so malformed cyclic ATNs cannot spin forever.
const ADAPTIVE_DIRECT_STEP_LIMIT: usize = RECOGNITION_DEPTH_LIMIT;

/// Runs a generated rule body after ensuring native stack capacity, growing
/// onto a segmented stack when remaining capacity enters the red zone.
///
/// Generated `parse_generated_rule_*_dispatch` methods call this when
/// [`BaseParser::generated_rule_stack_check_due`] fires so deeply nested input
/// parses (or reports a syntax error) instead of aborting the process.
pub fn grow_generated_rule_stack<R>(body: impl FnOnce() -> R) -> R {
stacker::maybe_grow(FAST_RECOGNIZE_RED_ZONE, FAST_RECOGNIZE_STACK_SIZE, body)
}
/// Probe window for deciding whether clean-pass memo entries are reusable
/// enough to keep caching. High-cardinality parses mostly produce one-shot
/// entries; compact ambiguous loops repeatedly hit the same keys.
Expand Down Expand Up @@ -5684,6 +5700,21 @@ where
}
}

/// Reports whether the generated rule dispatch should sample native stack
/// capacity before descending into the next rule body.
///
/// Generated recursive-descent methods otherwise map unbounded grammar
/// nesting straight onto native call depth; sampling every
/// [`GENERATED_RULE_STACK_CHECK_INTERVAL`] rule-context frames keeps the
/// hot path free of per-call probes while guaranteeing a check runs before
/// the red zone can be crossed.
#[must_use]
pub const fn generated_rule_stack_check_due(&self) -> bool {
self.rule_context_stack
.len()
.is_multiple_of(GENERATED_RULE_STACK_CHECK_INTERVAL)
}

/// 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
51 changes: 51 additions & 0 deletions tests/antlr4_rust_gen_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1549,3 +1549,54 @@ mod midi_tests {{
&test_source,
);
}

#[test]
fn deeply_nested_input_parses_without_native_stack_overflow() {
let temp = temporary_directory("deep-nesting");
let grammar = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/antlr4-rust-gen/deep-nesting/Nest.g4");
let out = temp.path().join("generated");

let output = run_antlr4_rust_gen(&[
grammar.as_os_str(),
OsStr::new("--out-dir"),
out.as_os_str(),
]);
assert!(
output.status.success(),
"stdout: {}\nstderr: {}",
utf8(&output.stdout),
utf8(&output.stderr)
);

// Every generated dispatch method must carry the stack guard; the rule
// chain here multiplies each `[` into ~6 native rule frames (issue #193).
let parser = fs::read_to_string(out.join("nest_parser.rs")).expect("parser should be emitted");
assert!(
parser.contains("antlr4_runtime::grow_generated_rule_stack("),
"generated dispatch must guard native stack growth\n{parser}"
);
Comment thread
tinovyatkin marked this conversation as resolved.

assert_generated_project(
temp.path(),
&["nest_lexer.rs", "nest_parser.rs"],
r#"
#[cfg(test)]
mod deep_nesting_tests {
use super::nest_lexer::NestLexer;
use super::nest_parser::{parse, NestParser};

#[test]
fn ten_thousand_levels_parse_on_the_default_test_stack() {
// Rust test threads default to a 2 MiB stack; without segmented-stack
// growth this depth aborted the process (issue #193).
let depth = 10_000;
let source = format!("{}a{}", "[".repeat(depth), "]".repeat(depth));
let parsed = parse(&source, NestLexer::new, NestParser::s)
.expect("deeply nested input should parse");
assert!(parsed.tree().as_rule().is_some());
}
}
"#,
);
}
43 changes: 43 additions & 0 deletions tests/fixtures/antlr4-rust-gen/deep-nesting/Nest.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Mirrors the failure shape from issue #193: an expression grammar whose rule
// chain multiplies input nesting into native call depth (CEL walks nine rules
// per `[`). Deeply nested input must parse without exhausting the native
// stack.
grammar Nest;

s
: expr EOF
;

expr
: disjunction
;

disjunction
: conjunction ('||' conjunction)*
;

conjunction
: relation ('&&' relation)*
;

relation
: unary
;

unary
: '!' unary
| primary
;

primary
: '[' expr ']'
| A
;

A
: 'a'
;

WS
: [ \t\r\n]+ -> skip
;
Loading