Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ Using the same package release for `antlr4-rust-gen` and
`antlr-rust-runtime` remains the recommended workflow, but matching the
generated-code API is the compile-time requirement.

The bundled generator currently emits revision 3, which exposes parameterized
rule arguments to committed parser-action hooks. The runtime also accepts
revisions 1 and 2 because their older generated recognizers use API surfaces
that remain available.

Generated modules created before this check was introduced cannot be detected
retroactively. When first upgrading to a release that includes the check,
regenerate every committed lexer and parser once. Thereafter, normal
Expand Down Expand Up @@ -494,12 +499,13 @@ Generated parsers also expose a parser-side hook escape hatch:
`MyParser::with_hooks(tokens, hooks)`, where `hooks` implements
`SemanticHooks`. Unknown parser predicates are offered to
`SemanticHooks::sempred` before the fallback policy is applied, and unhandled
parser action events are offered to `SemanticHooks::action` after the committed
parse path is selected. Predicate hooks may run speculatively during
prediction, so they must be replay-safe.
parser action events are offered to `SemanticHooks::action` at their grammar
position after the containing path is committed. Predicate hooks may run
speculatively during prediction, so they must be replay-safe; action hooks never
run on speculative or losing paths.

For helper-call predicates written as `helper()`, `this.helper()`, or
`self.helper()`, generated parsers also emit a typed hook adapter
For helper-call predicates and actions written as `helper()`, `this.helper()`,
or `self.helper()`, generated parsers also emit a typed hook adapter
(`MyParserHooks` plus `MyParserTypedHooks<T>`) that maps stable manifest
coordinates to named Rust methods. A `[[helper]]` pattern can opt into one
additional receiver spelling with `receiver = "..."`. For example, an
Expand All @@ -515,6 +521,10 @@ returns = "bool"
lower = "hook"
```

Use `kind = "parser-action"` and `returns = "unit"` for an action helper. Its
typed method runs exactly once on the committed path, before any later
predicate or nested-rule event.

Lexer callers can use `LexerSemCtx` with
`atn::lexer::next_token_with_semantic_hooks` or the
compiled-DFA variant to route lexer predicates/actions through the same
Expand Down
4 changes: 4 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ generated-code API revision that is checked against the selected runtime at
compile time, so releases that deliberately preserve the source contract can
remain compatible without exact SemVer equality.

The current generator emits revision 3 so generated parser-action hooks can
observe parameterized rule arguments. This runtime continues to accept revision
1 and 2 generated modules because their required APIs remain supported.

Generated modules created before the compatibility check was introduced carry
no enforceable revision. Regenerate every committed lexer and parser once when
first adopting a release with this mechanism. If a later build reports a
Expand Down
120 changes: 111 additions & 9 deletions src/atn/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ use crate::dfa::{
use crate::int_stream::IntStream;
use crate::prediction::{
AtnConfig, AtnConfigSet, ContextArena, ContextId, EMPTY_CONTEXT, EMPTY_RETURN_STATE,
PredictionContextStats, PredictionFxHasher, PredictionWorkspace, SemanticContext,
all_subsets_conflict, all_subsets_equal, conflicting_alt_subsets,
has_sll_conflict_terminating_prediction, single_viable_alt,
PredictionContextStats, PredictionFxHasher, PredictionPredicateCall,
PredictionSemanticProvenance, PredictionWorkspace, SemanticContext, all_subsets_conflict,
all_subsets_equal, conflicting_alt_subsets, has_sll_conflict_terminating_prediction,
single_viable_alt,
};
use crate::token::TOKEN_EOF;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::hash::BuildHasherDefault;
use std::sync::Arc;

type FxHashSet<T> = HashSet<T, BuildHasherDefault<PredictionFxHasher>>;

Expand Down Expand Up @@ -62,6 +64,17 @@ pub struct ParserAtnSimulator<'a> {
/// outcomes depend on caller-side evaluation, so any semantic transition
/// disables memoization entirely. Computed lazily on first retry.
full_context_memo_gate: Option<bool>,
/// Semantic configurations that survived the most recent prediction.
///
/// The simulator defers predicate evaluation to the parser because hooks
/// need live parser state. Keeping the surviving alternative/context pairs
/// lets the committed parser evaluate only simulator-viable paths.
prediction_semantic_candidates: Vec<ParserSemanticCandidate>,
/// Whether ATN configs retain rule-call paths for parameterized predicates.
///
/// This is enabled only by the committed parser when generated rule
/// argument metadata exists, keeping ordinary prediction configs compact.
track_prediction_rule_calls: bool,
}

#[derive(Clone, Copy, Debug)]
Expand Down Expand Up @@ -216,6 +229,13 @@ pub enum ParserAtnPredictionDiagnosticKind {
ContextSensitivity,
}

#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) struct ParserSemanticCandidate {
pub(crate) alt: usize,
pub(crate) context: SemanticContext,
pub(crate) predicate_calls: Vec<PredictionPredicateCall>,
}

#[derive(Clone, Copy)]
struct PredictionCheck {
decision: usize,
Expand Down Expand Up @@ -267,6 +287,7 @@ struct FullContextPrediction {
prediction: ParserAtnPrediction,
stop_index: usize,
resolution: FullContextResolution,
semantic_candidates: Vec<ParserSemanticCandidate>,
}

/// How the full-context loop settled, mirroring the two exits of Java's
Expand Down Expand Up @@ -294,15 +315,35 @@ fn full_context_prediction(
},
stop_index,
resolution,
semantic_candidates: semantic_prediction_candidates(configs),
}
}

fn semantic_prediction_candidates(configs: &AtnConfigSet) -> Vec<ParserSemanticCandidate> {
if !configs.has_semantic_context() {
return Vec::new();
}
let mut candidates = configs
.configs()
.iter()
.map(|config| ParserSemanticCandidate {
alt: config.alt,
context: config.semantic_context.clone(),
predicate_calls: config.prediction_predicate_calls().to_vec(),
})
.collect::<Vec<_>>();
candidates.sort();
candidates.dedup();
candidates
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct ClosureConfigKey {
state: usize,
alt: usize,
context: ContextId,
semantic_context: SemanticContext,
semantic_provenance: Option<Arc<PredictionSemanticProvenance>>,
precedence_filter_suppressed: bool,
}

Expand All @@ -313,6 +354,7 @@ impl From<&AtnConfig> for ClosureConfigKey {
alt: config.alt,
context: config.context,
semantic_context: config.semantic_context.clone(),
semantic_provenance: config.semantic_provenance.clone(),
precedence_filter_suppressed: config.precedence_filter_suppressed,
}
}
Expand Down Expand Up @@ -556,6 +598,8 @@ impl<'a> ParserAtnSimulator<'a> {
full_context_memo: HashMap::default(),
full_context_memo_len: 0,
full_context_memo_gate: None,
prediction_semantic_candidates: Vec::new(),
track_prediction_rule_calls: false,
}
}

Expand All @@ -566,6 +610,7 @@ impl<'a> ParserAtnSimulator<'a> {
self.adaptive_closure_work = 0;
self.outer_context_cache = None;
self.deferred_accept_states.clear();
self.prediction_semantic_candidates.clear();
self.workspace.reset();
}

Expand Down Expand Up @@ -695,13 +740,23 @@ impl<'a> ParserAtnSimulator<'a> {
full_context_memo: HashMap::default(),
full_context_memo_len: 0,
full_context_memo_gate: None,
prediction_semantic_candidates: Vec::new(),
track_prediction_rule_calls: false,
}
}

pub fn decision_dfas(&self) -> &[ParserDfa] {
&self.store.decision_to_dfa
}

pub(crate) fn prediction_semantic_candidates(&self) -> &[ParserSemanticCandidate] {
&self.prediction_semantic_candidates
}

pub(crate) const fn set_track_prediction_rule_calls(&mut self, track: bool) {
self.track_prediction_rule_calls = track;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
/// Returns adaptive-call and closure-work counters for stable decisions.
///
/// A call contributes only when its decision DFA was already non-empty and
Expand Down Expand Up @@ -937,6 +992,7 @@ impl<'a> ParserAtnSimulator<'a> {
input: &mut T,
merge_cache: &mut PredictionWorkspace,
) -> Result<ParserAtnPrediction, ParserAtnSimulatorError> {
self.prediction_semantic_candidates.clear();
let decision = request.decision;
let learning_revision = self
.store
Expand Down Expand Up @@ -1122,6 +1178,7 @@ impl<'a> ParserAtnSimulator<'a> {
.map(|dfa| dfa.configs(state_number).clone())
&& let Some(alt) = self.alt_that_finished_decision_entry_rule(&configs)
{
self.prediction_semantic_candidates = semantic_prediction_candidates(&configs);
return Ok(ParserAtnPrediction {
alt,
requires_full_context: false,
Expand Down Expand Up @@ -1155,12 +1212,20 @@ impl<'a> ParserAtnSimulator<'a> {
&& let Some(prediction) =
self.non_greedy_exit_prediction(decision, decision_state, state_number)
{
self.record_prediction_semantic_candidates(decision, state_number);
return Ok(Some(prediction));
}
let Some(info) = self.dfa_prediction_info(decision, state_number) else {
return Ok(None);
};
let prediction = info.prediction;
let semantic_candidates = self
.store
.decision_to_dfa
.get(decision)
.map(|dfa| semantic_prediction_candidates(dfa.configs(state_number)))
.unwrap_or_default();
self.prediction_semantic_candidates = semantic_candidates;
// SLL-probe stage: the caller only needs to know that this conflict
// requires full context; it will re-run with the real outer context.
// Returning the SLL prediction here (with requires_full_context set)
Expand Down Expand Up @@ -1191,7 +1256,7 @@ impl<'a> ParserAtnSimulator<'a> {
{
#[cfg(feature = "perf-counters")]
crate::perf::record_full_context_memo_hit(decision);
return Ok(Some(Self::full_context_retry_prediction(
return Ok(Some(self.full_context_retry_prediction(
full_context,
info.conflicting_alts,
start_index,
Expand All @@ -1208,7 +1273,7 @@ impl<'a> ParserAtnSimulator<'a> {
if memo_allowed {
self.record_full_context_memo(memo_key, start_index, input, &full_context);
}
return Ok(Some(Self::full_context_retry_prediction(
return Ok(Some(self.full_context_retry_prediction(
full_context,
info.conflicting_alts,
start_index,
Expand All @@ -1222,12 +1287,20 @@ impl<'a> ParserAtnSimulator<'a> {
/// shared by the fresh LL run and the memoized replay so both produce
/// byte-identical diagnostics.
fn full_context_retry_prediction(
&mut self,
full_context: FullContextPrediction,
sll_conflicting_alts: Vec<usize>,
start_index: usize,
sll_stop_index: usize,
) -> ParserAtnPrediction {
let (kind, exact, conflicting_alts) = match full_context.resolution {
let FullContextPrediction {
mut prediction,
stop_index,
resolution,
semantic_candidates,
} = full_context;
self.prediction_semantic_candidates = semantic_candidates;
let (kind, exact, conflicting_alts) = match resolution {
FullContextResolution::Ambiguous { exact, ref alts } => (
ParserAtnPredictionDiagnosticKind::Ambiguity,
exact,
Expand All @@ -1242,20 +1315,32 @@ impl<'a> ParserAtnSimulator<'a> {
sll_conflicting_alts,
),
};
let mut prediction = full_context.prediction;
prediction.has_semantic_context = self
.prediction_semantic_candidates
.iter()
.any(|candidate| candidate.alt == prediction.alt && !candidate.context.is_none());
if conflicting_alts.len() > 1 {
prediction.diagnostic = Some(ParserAtnPredictionDiagnostic {
kind,
start_index,
sll_stop_index,
ll_stop_index: full_context.stop_index,
ll_stop_index: stop_index,
conflicting_alts,
exact,
});
}
prediction
}

fn record_prediction_semantic_candidates(&mut self, decision: usize, state_number: DfaStateId) {
self.prediction_semantic_candidates = self
.store
.decision_to_dfa
.get(decision)
.map(|dfa| semantic_prediction_candidates(dfa.configs(state_number)))
.unwrap_or_default();
}

/// Whether full-context memoization is sound for this ATN.
///
/// Predicates make prediction outcomes depend on caller-side evaluation
Expand Down Expand Up @@ -1994,7 +2079,10 @@ impl<'a> ParserAtnSimulator<'a> {
.contexts
.parent(config.context, index)
.unwrap_or(EMPTY_CONTEXT);
let next = config.moved_to(return_state, parent, &self.store.contexts);
let mut next = config.moved_to(return_state, parent, &self.store.contexts);
if self.track_prediction_rule_calls {
next.exit_prediction_rule();
}
stack.push((next, collect_predicates));
}
handled_all_paths
Expand Down Expand Up @@ -2044,6 +2132,20 @@ impl<'a> ParserAtnSimulator<'a> {
};
let mut target = config.moved_to(transition.target(), context, &self.store.contexts);
target.semantic_context = semantic_context;
if self.track_prediction_rule_calls {
match transition_kind {
ParserTransitionKind::Rule => {
target.enter_prediction_rule(config.state, transition.arg0() as usize);
}
ParserTransitionKind::Predicate if collect_predicates => {
target.record_prediction_predicate(
transition.arg0() as usize,
transition.arg1() as usize,
);
}
_ => {}
}
}
Some(target)
}

Expand Down
Loading
Loading