Skip to content
Open
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
64 changes: 59 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,21 @@ 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 15. Generated recognizers now
The bundled generator currently emits revision 16. Rules with an authored
`catch [...]` or `finally { ... }` clause now expand through two additional
generated-rule lifecycle sections: an `exception` slot that either keeps the
default report-and-recover handler or replaces it with the authored handler
(bound to the recognition error, matching ANTLR's generated catch
replacement), and a `propagate` slot that still runs the authored `finally`
body when a fatal error abandons the generated attempt. Rules without
exception clauses emit the unchanged four-section form. Alongside the
lifecycle change, every authored target-code section (named actions and
exception clauses) now receives a deterministic disposition in the
`semantics.json` `sections` rows, `--require-full-semantics` rejects
unsupported sections, and embedded generation emits `@header` bodies at the
top of the generated module and `@definitions` bodies at module scope.

Revision 15 generated recognizers
embed their static data tables — the ahead-of-time compiled lexer DFA, the
packed parser ATN, and the serialized lexer ATN inside `GrammarMetadata` — as
versioned encoded blobs defined by the runtime's `encoded` module: LEB128
Expand All @@ -192,11 +206,13 @@ also omits the same redundant frames. SLL accuracy is preserved by default, and
the reduced-accuracy parser mode is available only through an explicit
simulator constructor.

Revision 12 to 14 generated recognizers remain accepted because the runtime
still provides their source API — integer-array `GrammarMetadata` ATN data,
Revision 12 to 15 generated recognizers remain accepted because the runtime
still provides their source API — the four-section generated-rule form,
integer-array `GrammarMetadata` ATN data,
`CompiledLexerDfa::from_serialized`, and `ParserAtn::from_static` — and reads
packed parser ATN formats 1 through 3. Regenerate them with revision 15 to
emit the compact encoded representation.
packed parser ATN formats 1 through 3. Regenerate them with revision 16 to
execute authored `catch`/`finally` clauses and emit the compact encoded
representation.

Revision 13 moved the iterative generated listener tree-walk engine into
`antlr4_runtime::generated::walk_generated`. Generated parsers retain their
Expand Down Expand Up @@ -696,6 +712,44 @@ Acknowledged options have the `hooked` disposition. Unacknowledged target
options have the `unsupported` disposition and make
`--require-full-semantics` fail.

The manifest also inventories every source-owned target-code *section* under
each grammar's `sections` array: grammar-level named actions (`@header`,
`@definitions`, `@members`, scoped variants), rule-level named actions
(`@init`, `@after`), and rule exception clauses (`catch [...] { ... }`,
`finally { ... }`). Sections have no ATN coordinate, so these rows are what
makes them auditable. Each row carries the section kind, scope, owning rule,
source position, body, and a disposition: `embedded` (the body is spliced
into generated Rust), `translated` (the section's behavior lowers into
generated metadata without splicing the body, e.g. `@members` state owned by
`[[member]]` declarations in `--sem-patterns`), or `unsupported`. Declaring
`[[member]]` slots for a recognizer is the caller's explicit acknowledgment
that the pattern file owns that recognizer's `@members` state: the
target-language body is replaced wholesale, not parsed or partially matched,
exactly like `--option-hook` acknowledges an option's target behavior.
Unsupported sections warn on every run and fail generation under
`--require-full-semantics` with a source-positioned diagnostic.

Under `--actions embedded`, supported sections execute:

- `@header` bodies are emitted once at the top of the generated module,
before the generated imports; `@definitions` bodies are emitted once at
module scope after the `@members` module items. Both are translated with
the same token-alias machinery as `@members`. In a combined grammar the
unscoped sections belong to the parser module; `@lexer::`-scoped sections
are currently unsupported.
- A single `catch [name] { ... }` clause per rule replaces the default
report-and-recover handler, with the recognition error bound to `name`
(a plain or raw Rust identifier). Typed clauses such as
`catch [RecognitionException e]` are rejected: the handler receives every
recognition error, and this backend models no target-language exception
types. The rule then completes normally, like ANTLR's generated catch
replacement. Multiple catch clauses are unsupported.
- `finally { ... }` runs exactly once on every completed path — after
`@after` on success, after default recovery or an authored catch handler,
and before a fatal propagated error abandons the rule — and never during
speculative adaptive-retry unwinds, whose re-entry runs the rule from the
top. `@after` remains success-only.

Unknown coordinates are governed by `--sem-unknown`:

```bash
Expand Down
76 changes: 70 additions & 6 deletions crates/antlr-rust-codegen/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ use crate::optimization::OptimizationPlan;
use crate::parser::{ParserRenderOptions, render_parser_with_decision_report};
use crate::rust_support::{self, PreparedRustSupport};
use crate::semantics::{
DecisionReportGrammar, GrammarOptionEntry, SemUnknownPolicy, SemanticsEntry,
collect_lexer_semantics, collect_parser_semantics_for_mode, collect_structural_grammar_options,
enforce_require_full_options, enforce_require_full_semantics, enforce_sem_unknown,
DecisionReportGrammar, GrammarOptionEntry, SectionEntry, SectionInventoryOptions,
SemUnknownPolicy, SemanticsEntry, collect_lexer_semantics, collect_parser_semantics_for_mode,
collect_recognizer_sections, collect_structural_grammar_options, enforce_require_full_options,
enforce_require_full_sections, enforce_require_full_semantics, enforce_sem_unknown,
grammar_option_warning_messages, render_decisions_manifest, render_semantics_manifest,
section_warning_messages,
};
use crate::test_rig::{
MAIN_PATH as TEST_RIG_MAIN_PATH, TestRigLexer, TestRigParser, render_test_rig,
Expand Down Expand Up @@ -77,7 +79,14 @@ pub(crate) fn generate(
}

let mut grammar_options = Vec::new();
let mut manifest_grammars: Vec<(&'static str, String, Vec<SemanticsEntry>)> = Vec::new();
let mut manifest_grammars: Vec<(&'static str, String, Vec<SemanticsEntry>, Vec<SectionEntry>)> =
Vec::new();
// Section strictness is scoped per recognizer, like coordinate and
// option strictness: --require-full-semantics covers every recognizer,
// and a Rust-support bundle covers only its own recognizers. Enforcement
// runs once, after the loop, so one strict run reports every unsupported
// section instead of stopping at the first failing recognizer.
let mut strict_sections: Vec<SectionEntry> = Vec::new();
let mut decision_report_grammars: Vec<DecisionReportGrammar> = Vec::new();
let mut rendered_modules = BTreeMap::<PathBuf, String>::new();
let mut emitted_lexers = BTreeSet::new();
Expand Down Expand Up @@ -117,6 +126,35 @@ pub(crate) fn generate(
)?;
enforce_sem_unknown(sem_unknown, &entries)?;
enforce_require_full_semantics(require_full_semantics, &entries)?;
// Unscoped grammar actions of a split combined grammar belong to
// the parser half (ANTLR's default scope); an authored lexer
// grammar owns its own unscoped actions.
let owns_unscoped_actions = root.parser.is_none_or(|parser_grammar| {
compilation.parser(parser_grammar).is_none_or(|parser| {
parser.semantic.unit.source != compiled.semantic.unit.source
})
});
let sections = collect_recognizer_sections(
&data,
SectionInventoryOptions {
embedded: embedded_actions,
patterns: &args.sem_patterns,
owns_unscoped_actions,
// Parser-scoped sections were cloned into this lexer unit
// only when it was split from a combined grammar — the
// same condition under which the parser owns the
// unscoped sections.
counterpart_covers_scoped: !owns_unscoped_actions,
},
)?;
let section_warnings = section_warning_messages(&sections);
for warning in &section_warnings {
report(warning).map_err(Error::generation)?;
}
warnings.extend(section_warnings);
if require_full_semantics {
strict_sections.extend(sections.iter().cloned());
}
let grammar_name = compiled.semantic.recognizer.name.clone();
let render_model = LexerRenderModel::new(
&grammar_name,
Expand All @@ -132,7 +170,7 @@ pub(crate) fn generate(
if args.test_rig.is_some() {
test_rig_lexers.push(TestRigLexer::new(grammar_name.clone()));
}
manifest_grammars.push(("lexer", grammar_name, entries));
manifest_grammars.push(("lexer", grammar_name, entries, sections));
}

if let Some(grammar) = root.parser
Expand Down Expand Up @@ -165,6 +203,31 @@ pub(crate) fn generate(
)?;
enforce_sem_unknown(sem_unknown, &entries)?;
enforce_require_full_semantics(require_full_semantics, &entries)?;
// A lexer generated from this same source unit (split combined
// grammar) is the only counterpart that receives this unit's
// lexer-scoped sections.
let counterpart_covers_scoped = root.lexer.is_some_and(|lexer_grammar| {
compilation.lexer(lexer_grammar).is_some_and(|lexer| {
lexer.semantic.unit.source == compiled.semantic.unit.source
})
});
let sections = collect_recognizer_sections(
&data,
SectionInventoryOptions {
embedded: embedded_actions,
patterns: &args.sem_patterns,
owns_unscoped_actions: true,
counterpart_covers_scoped,
},
)?;
let section_warnings = section_warning_messages(&sections);
for warning in &section_warnings {
report(warning).map_err(Error::generation)?;
}
warnings.extend(section_warnings);
if require_full_semantics {
strict_sections.extend(sections.iter().cloned());
}
let grammar_name = compiled.semantic.recognizer.name.clone();
let (mut module, decision_report_rows) = render_parser_with_decision_report(
&grammar_name,
Expand Down Expand Up @@ -192,7 +255,7 @@ pub(crate) fn generate(
rule_names: data.rule_names.clone(),
rows: decision_report_rows,
});
manifest_grammars.push(("parser", grammar_name, entries));
manifest_grammars.push(("parser", grammar_name, entries, sections));
}
}

Expand All @@ -203,6 +266,7 @@ pub(crate) fn generate(
}
warnings.extend(option_warnings);
enforce_require_full_options(args.require_full_semantics, &grammar_options)?;
enforce_require_full_sections(!strict_sections.is_empty(), &strict_sections)?;
let manifest_policy = if prepared_support.all_roots_supported() {
SemUnknownPolicy::Error
} else {
Expand Down
2 changes: 1 addition & 1 deletion crates/antlr-rust-codegen/src/embedded/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ mod tests {
fn model(rules: Vec<RuleModel>) -> EmbeddedModel {
EmbeddedModel {
rules,
parser_members: MembersModel::default(),
..EmbeddedModel::default()
}
}

Expand Down
13 changes: 13 additions & 0 deletions crates/antlr-rust-codegen/src/embedded/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ pub(crate) struct RuleModel {
pub(crate) arg_names: Vec<String>,
pub(crate) init_body: Option<String>,
pub(crate) after_body: Option<String>,
/// Supported authored `catch [...] { ... }` clause: the Rust binding name
/// derived from the argument, plus the handler body. `None` when the rule
/// has no catch clause or the clause is unsupported (the section
/// inventory reports the latter).
pub(crate) catch_clause: Option<(String, String)>,
/// Authored `finally { ... }` body (non-empty).
pub(crate) finally_body: Option<String>,
pub(crate) alts: Vec<AltModel>,
}

Expand Down Expand Up @@ -289,6 +296,12 @@ pub(crate) struct EmbeddedModel {
/// Parser rules keyed by parser rule index (grammar order).
pub(crate) rules: Vec<RuleModel>,
pub(crate) parser_members: MembersModel,
/// `@header` bodies emitted verbatim (after token-alias translation) at
/// the top of the generated module, before generated imports.
pub(crate) header_items: Vec<MemberItem>,
/// `@definitions` bodies emitted at module scope after the embedded
/// `@members` module items.
pub(crate) definitions_items: Vec<MemberItem>,
}

/// Where an action body executes, which changes how `$text` translates.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ expression: "generated_module_header.replace(env!(\"CARGO_PKG_VERSION\"),\n\"<ge
---
// @generated by antlr-rust-codegen v<generator-version> - do not edit
// project: https://github.com/ophi-dev/antlr-rust-runtime
antlr4_runtime::__antlr4_rust_require_codegen_api!(15, "<generator-version>");
antlr4_runtime::__antlr4_rust_require_codegen_api!(16, "<generator-version>");
#[allow(warnings, missing_docs, clippy::all, clippy::pedantic, clippy::nursery)]
#[rustfmt::skip]
mod __antlr4_rust_generated {
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ expression: manifest
"disposition": "assume-true",
"template": null
}
]
],
"sections": []
}
]
}
33 changes: 30 additions & 3 deletions crates/antlr-rust-codegen/src/generator/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1218,6 +1218,8 @@ fn embedded_rules_never_use_atn_preferred_fallback() {
let rule_has_attrs = vec![false; rules.len()];
let init_entry = BTreeMap::new();
let after = BTreeMap::new();
let catch_clauses = BTreeMap::new();
let finally_bodies = BTreeMap::new();
let call_args = BTreeMap::new();
let rule_arg0 = vec![None; rules.len()];

Expand All @@ -1236,6 +1238,8 @@ fn embedded_rules_never_use_atn_preferred_fallback() {
rule_has_attrs: &rule_has_attrs,
init_entry: &init_entry,
after: &after,
catch_clauses: &catch_clauses,
finally_bodies: &finally_bodies,
call_args: &call_args,
rule_arg0: &rule_arg0,
}),
Expand Down Expand Up @@ -1987,7 +1991,7 @@ fn antlr4rust_compat_accessors_reserve_legacy_method_names() {
..embedded::RuleModel::default()
},
],
parser_members: embedded::MembersModel::default(),
..embedded::EmbeddedModel::default()
};
let mut child_cardinalities = BTreeMap::from([
(
Expand Down Expand Up @@ -5839,7 +5843,7 @@ fn semantics_manifest_renders_coordinates_and_policy() {
let manifest = render_semantics_manifest(
SemUnknownPolicy::AssumeTrue,
&[],
&[("parser", "SParser".to_owned(), entries)],
&[("parser", "SParser".to_owned(), entries, Vec::new())],
);

insta::assert_snapshot!("semantics_manifest_with_untranslated_predicate", manifest);
Expand Down Expand Up @@ -5904,15 +5908,38 @@ fn indexed_action_overrides_precede_portable_boolean_lowering() {
}
}

#[test]
fn exception_catch_bindings_follow_rust_identifier_rules() {
use crate::semantics::exception_catch_binding;
assert_eq!(exception_catch_binding("error").as_deref(), Some("error"));
assert_eq!(exception_catch_binding(" error ").as_deref(), Some("error"));
// Rust identifiers are XID-based, not ASCII-only, and raw identifiers
// are bindable except for the path keywords `r#` cannot rescue.
assert_eq!(exception_catch_binding("é").as_deref(), Some("é"));
assert_eq!(exception_catch_binding("r#type").as_deref(), Some("r#type"));
assert_eq!(exception_catch_binding("r#self"), None);
// Java-style typed clauses cannot narrow by exception type (the handler
// receives every recognition error), so no type token is recognized.
assert_eq!(exception_catch_binding("RecognitionException e"), None);
assert_eq!(exception_catch_binding("FailedPredicateException e"), None);
// `_` is a wildcard pattern, not an identifier the macro can bind.
assert_eq!(exception_catch_binding("_"), None);
assert_eq!(exception_catch_binding("fn"), None);
assert_eq!(exception_catch_binding("a b c"), None);
assert_eq!(exception_catch_binding(""), None);
assert_eq!(exception_catch_binding("1x"), None);
}

#[test]
fn semantics_manifest_renders_empty_inventory() {
let manifest = render_semantics_manifest(
SemUnknownPolicy::AssumeTrue,
&[],
&[("parser", "SParser".to_owned(), Vec::new())],
&[("parser", "SParser".to_owned(), Vec::new(), Vec::new())],
);

assert!(manifest.contains("\"coordinates\": []"));
assert!(manifest.contains("\"sections\": []"));
}

#[test]
Expand Down
15 changes: 6 additions & 9 deletions crates/antlr-rust-codegen/src/parser/ir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ pub(crate) struct EmbeddedStepRender<'a> {
pub(crate) rule_has_attrs: &'a [bool],
pub(crate) init_entry: &'a BTreeMap<usize, String>,
pub(crate) after: &'a BTreeMap<usize, String>,
/// rule -> (binding name, translated authored `catch` handler body).
pub(crate) catch_clauses: &'a BTreeMap<usize, (String, String)>,
/// rule -> translated authored `finally` body.
pub(crate) finally_bodies: &'a BTreeMap<usize, String>,
pub(crate) call_args: &'a BTreeMap<usize, String>,
pub(crate) rule_arg0: &'a [Option<String>],
}
Expand Down Expand Up @@ -217,10 +221,7 @@ impl<'a> DecisionRoutingRender<'a> {
/// LOOK(1) arms (plain mode only; embedded mode renders its Java-parity
/// switch through [`EmbeddedStepRender`]). Both are pre-restricted to
/// sync-no-op lookahead and render through the same dispatch shape.
pub(crate) fn static_dispatch_table(
self,
decision: usize,
) -> Option<&'a FixedLookaheadTable> {
pub(crate) fn static_dispatch_table(self, decision: usize) -> Option<&'a FixedLookaheadTable> {
self.fixed_lookahead_tables
.and_then(|tables| tables.get(&decision))
.or_else(|| {
Expand Down Expand Up @@ -711,11 +712,7 @@ pub(crate) fn parser_rule_callers_reaching(
return graph_nodes_reaching(&graph, target_rules);
}
let atn = data.parser_atn();
atn_rule_callers_reaching(
atn,
target_rules,
data.rule_names.len(),
)
atn_rule_callers_reaching(atn, target_rules, data.rule_names.len())
}

pub(crate) fn atn_rule_callers_reaching(
Expand Down
Loading
Loading