From b469d03d376e4e841b4a4d5be78c80ec33537011 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Sat, 1 Aug 2026 14:33:47 +0200 Subject: [PATCH 1/6] feat(codegen): enforce generated runtime compatibility Define a generated-code API revision shared by the generator and runtime, and emit a compile-time check into every generated module. Unsupported revisions now report the generator version, requested and supported revisions, and both remediation paths before incidental Rust API errors obscure the cause. Cover compatible lexer/parser pairs and mixed-revision failures, migrate the checked-in recognizers, and document the consumer migration and maintainer bump policy. --- AGENTS.md | 17 +++ CLAUDE.md | 17 +++ README.md | 22 ++++ docs/migration.md | 17 ++- src/bin/antlr4-rust-gen.rs | 48 ++++---- .../grammar/generated/antlr_v4_lexer.rs | 3 +- .../grammar/generated/antlr_v4_parser.rs | 3 +- src/lib.rs | 22 ++++ src/xpath/generated/x_path_lexer.rs | 3 +- tests/antlr4_rust_gen_cli.rs | 105 ++++++++++++++++-- ...gen_cli__generated_codegen_api_checks.snap | 6 + ...rated_codegen_api_mismatch_diagnostic.snap | 13 +++ .../antlr-v4-grammar/self-hosted.sha256 | 4 +- 13 files changed, 243 insertions(+), 37 deletions(-) create mode 100644 tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_checks.snap create mode 100644 tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap diff --git a/AGENTS.md b/AGENTS.md index fc05a41d..ac4140f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,23 @@ workarounds to `src/bin/antlr4-rust-gen.rs` or runtime modules; model the behavior from ANTLR metadata/ATN structure instead, and keep language-specific guidance in docs or tests for that language. +## Generated-code API revision + +`src/lib.rs::__ANTLR4_RUST_CODEGEN_API` is the single current revision emitted +by `antlr4-rust-gen`. The accepted revisions are the literal match arms in +`src/lib.rs::__antlr4_rust_require_codegen_api!`. + +Any change that makes newly generated source require an incompatible runtime +interface, or makes existing generated source incompatible with the runtime, +MUST increment `__ANTLR4_RUST_CODEGEN_API`. When incrementing it, update the +macro's accepted revision arms and supported-revision diagnostic, the +`generated_modules_enforce_codegen_api_compatibility` integration test and +snapshots, all checked-in generated recognizers, and the compatibility +documentation. Retain an older accepted arm only while the runtime still +provides every API surface that revision's generated source needs. Package +releases that preserve the generated-source/runtime contract MUST NOT increment +the revision automatically. + ## Kotlin parser parity perf benchmark Reproduces the timings against the Kotlin grammar from `antlr/grammars-v4`. diff --git a/CLAUDE.md b/CLAUDE.md index f23f4090..5d6592e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,22 @@ # Development notes +## Generated-code API revision + +`src/lib.rs::__ANTLR4_RUST_CODEGEN_API` is the single current revision emitted +by `antlr4-rust-gen`. The accepted revisions are the literal match arms in +`src/lib.rs::__antlr4_rust_require_codegen_api!`. + +Any change that makes newly generated source require an incompatible runtime +interface, or makes existing generated source incompatible with the runtime, +MUST increment `__ANTLR4_RUST_CODEGEN_API`. When incrementing it, update the +macro's accepted revision arms and supported-revision diagnostic, the +`generated_modules_enforce_codegen_api_compatibility` integration test and +snapshots, all checked-in generated recognizers, and the compatibility +documentation. Retain an older accepted arm only while the runtime still +provides every API surface that revision's generated source needs. Package +releases that preserve the generated-source/runtime contract MUST NOT increment +the revision automatically. + ## Inner loop ```bash diff --git a/README.md b/README.md index a45b42e1..d974588f 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,28 @@ antlr4-rust-gen \ Use multiple roots when a build should emit several independent recognizers in one deterministic source-set compilation. +### Generated-source/runtime compatibility + +Every newly generated lexer and parser contains a compile-time generated-code +API check. The check records both the API revision and the +`antlr4-rust-gen` package version that produced the module. A mismatch reports +the generated file directly and asks you to either regenerate it with a +compatible generator or select a compatible `antlr-rust-runtime` dependency. + +The generated-code API revision tracks the Rust source interface between +generated recognizers and the runtime. It is independent of package SemVer and +the serialized ATN/DFA format versions: compatible package releases may share +one revision, while an incompatible source-contract change increments it. +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. + +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 +`cargo check` builds enforce compatibility for every compiled generated +module. + ## Complete Example Suppose you are using `JSON.g4` from `antlr/grammars-v4/json`. Generate both diff --git a/docs/migration.md b/docs/migration.md index e1d79b86..8d360ef1 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1,10 +1,19 @@ # Migration Notes `antlr-rust-runtime` is pre-1.0. Minor releases may include breaking runtime and -generator changes. Generated lexers and parsers must use the same release of -`antlr4-rust-gen` as the runtime. Before regenerating, run -`antlr4-rust-gen --version` and compare the reported release with the -`antlr-rust-runtime` dependency version. +generator changes. Using the same release of `antlr4-rust-gen` and +`antlr-rust-runtime` remains recommended. Newly generated modules also carry a +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. + +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 +generated-code API mismatch, either regenerate the named module with a +compatible generator or select a runtime that accepts its requested revision. +`antlr4-rust-gen --version` reports the generator package version for auditing +and reproducible regeneration. ## Structured Syntax Error Events and Byte Spans diff --git a/src/bin/antlr4-rust-gen.rs b/src/bin/antlr4-rust-gen.rs index 56ee4c7d..e7510792 100644 --- a/src/bin/antlr4-rust-gen.rs +++ b/src/bin/antlr4-rust-gen.rs @@ -52,20 +52,27 @@ use templates::{ template_sequence_bodies, }; -const GENERATED_MODULE_HEADER: &str = concat!( - "// @generated by ", - env!("CARGO_PKG_NAME"), - " v", - env!("CARGO_PKG_VERSION"), - " - do not edit\n", - "// project: ", - env!("CARGO_PKG_REPOSITORY"), - "\n", - "#[allow(warnings, missing_docs, clippy::all, clippy::pedantic, clippy::nursery)]\n", - "#[rustfmt::skip]\n", - "mod __antlr4_rust_generated {\n", - "\n", -); +fn generated_module_header() -> String { + format!( + concat!( + "// @generated by ", + env!("CARGO_PKG_NAME"), + " v", + env!("CARGO_PKG_VERSION"), + " - do not edit\n", + "// project: ", + env!("CARGO_PKG_REPOSITORY"), + "\n", + "antlr4_runtime::__antlr4_rust_require_codegen_api!({}, \"{}\");\n", + "#[allow(warnings, missing_docs, clippy::all, clippy::pedantic, clippy::nursery)]\n", + "#[rustfmt::skip]\n", + "mod __antlr4_rust_generated {{\n", + "\n", + ), + antlr4_runtime::__ANTLR4_RUST_CODEGEN_API, + env!("CARGO_PKG_VERSION") + ) +} const GENERATED_MODULE_FOOTER: &str = "\ } @@ -3885,7 +3892,7 @@ fn render_lexer( "if H::ENABLES_LEXER_LIFECYCLE {{\n {lifecycle_next_token_call}\n }} else {{\n {next_token_call}\n }}" ) }; - let generated_header = GENERATED_MODULE_HEADER; + let generated_header = generated_module_header(); let generated_footer = GENERATED_MODULE_FOOTER; Ok(format!( @@ -12955,7 +12962,7 @@ fn render_parser_with_decision_report( embedded_field_inits, embedded_impl_items, ) = embedded_render_slots(embedded_data.as_ref().or(structural_surface.as_ref())); - let generated_header = GENERATED_MODULE_HEADER; + let generated_header = generated_module_header(); let generated_footer = GENERATED_MODULE_FOOTER; let AdaptiveAtnParserRenderSlots { struct_field: adaptive_atn_preference_struct_field, @@ -15815,17 +15822,18 @@ mod tests { let parser = render_parser("TParser", &minimal_parser_data()).expect("parser should render"); - assert!(GENERATED_MODULE_HEADER.contains(concat!( + let generated_module_header = generated_module_header(); + assert!(generated_module_header.contains(concat!( "@generated by ", env!("CARGO_PKG_NAME"), " v", env!("CARGO_PKG_VERSION") ))); - assert!(GENERATED_MODULE_HEADER.contains(env!("CARGO_PKG_REPOSITORY"))); + assert!(generated_module_header.contains(env!("CARGO_PKG_REPOSITORY"))); for rendered in [&lexer, &parser] { - assert!(rendered.starts_with(GENERATED_MODULE_HEADER)); - assert!(rendered[GENERATED_MODULE_HEADER.len()..].starts_with("use antlr4_runtime::")); + assert!(rendered.starts_with(&generated_module_header)); + assert!(rendered[generated_module_header.len()..].starts_with("use antlr4_runtime::")); assert!(!rendered.contains("#![")); assert!(rendered.contains(GENERATED_MODULE_FOOTER)); assert!( diff --git a/src/bin_support/grammar/generated/antlr_v4_lexer.rs b/src/bin_support/grammar/generated/antlr_v4_lexer.rs index 9780b591..43310d21 100644 --- a/src/bin_support/grammar/generated/antlr_v4_lexer.rs +++ b/src/bin_support/grammar/generated/antlr_v4_lexer.rs @@ -1,5 +1,6 @@ -// @generated by antlr-rust-runtime v0.24.0 - do not edit +// @generated by antlr-rust-runtime v0.25.0 - do not edit // project: https://github.com/ophi-dev/antlr-rust-runtime +antlr4_runtime::__antlr4_rust_require_codegen_api!(1, "0.25.0"); #[allow(warnings, missing_docs, clippy::all, clippy::pedantic, clippy::nursery)] #[rustfmt::skip] mod __antlr4_rust_generated { diff --git a/src/bin_support/grammar/generated/antlr_v4_parser.rs b/src/bin_support/grammar/generated/antlr_v4_parser.rs index 521bf9f5..f692a2ed 100644 --- a/src/bin_support/grammar/generated/antlr_v4_parser.rs +++ b/src/bin_support/grammar/generated/antlr_v4_parser.rs @@ -1,5 +1,6 @@ -// @generated by antlr-rust-runtime v0.24.0 - do not edit +// @generated by antlr-rust-runtime v0.25.0 - do not edit // project: https://github.com/ophi-dev/antlr-rust-runtime +antlr4_runtime::__antlr4_rust_require_codegen_api!(1, "0.25.0"); #[allow(warnings, missing_docs, clippy::all, clippy::pedantic, clippy::nursery)] #[rustfmt::skip] mod __antlr4_rust_generated { diff --git a/src/lib.rs b/src/lib.rs index 9f604754..68789c63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,28 @@ extern crate self as antlr4_runtime; +/// Current generated-source/runtime contract revision emitted by the bundled generator. +#[doc(hidden)] +pub const __ANTLR4_RUST_CODEGEN_API: u32 = 1; + +/// Verifies that generated source is compatible with the selected runtime. +#[doc(hidden)] +#[macro_export] +macro_rules! __antlr4_rust_require_codegen_api { + (1, $generator_version:literal) => {}; + ($requested:literal, $generator_version:literal) => { + compile_error!(concat!( + "antlr4-rust generated-code API mismatch: antlr4-rust-gen v", + $generator_version, + " emitted generated-code API revision ", + stringify!($requested), + ", but the selected antlr-rust-runtime supports revision 1; regenerate this \ + recognizer with a compatible antlr4-rust-gen or select a compatible \ + antlr-rust-runtime dependency" + )); + }; +} + pub mod atn; pub mod byte_stream; pub mod char_stream; diff --git a/src/xpath/generated/x_path_lexer.rs b/src/xpath/generated/x_path_lexer.rs index db16cdf3..873387d6 100644 --- a/src/xpath/generated/x_path_lexer.rs +++ b/src/xpath/generated/x_path_lexer.rs @@ -1,5 +1,6 @@ -// @generated by antlr-rust-runtime v0.21.0 - do not edit +// @generated by antlr-rust-runtime v0.25.0 - do not edit // project: https://github.com/ophi-dev/antlr-rust-runtime +antlr4_runtime::__antlr4_rust_require_codegen_api!(1, "0.25.0"); #[allow(warnings, missing_docs, clippy::all, clippy::pedantic, clippy::nursery)] #[rustfmt::skip] mod __antlr4_rust_generated { diff --git a/tests/antlr4_rust_gen_cli.rs b/tests/antlr4_rust_gen_cli.rs index 2ae3d9ea..43b34e7e 100644 --- a/tests/antlr4_rust_gen_cli.rs +++ b/tests/antlr4_rust_gen_cli.rs @@ -17,6 +17,16 @@ fn assert_generated_modules_compile(temp_dir: &Path, modules: &[&str]) { } fn assert_generated_project(temp_dir: &Path, modules: &[&str], test_source: &str) { + let output = run_generated_project(temp_dir, modules, test_source); + assert!( + output.status.success(), + "generated project failed\nstdout: {}\nstderr: {}", + utf8(&output.stdout), + utf8(&output.stderr) + ); +} + +fn run_generated_project(temp_dir: &Path, modules: &[&str], test_source: &str) -> Output { let project = temp_dir.join("compile-generated"); let source = project.join("src"); let uses_insta = test_source.contains("insta"); @@ -61,7 +71,7 @@ fn assert_generated_project(temp_dir: &Path, modules: &[&str], test_source: &str .expect("generated module should be copied into the check crate"); } - let output = Command::new(env!("CARGO")) + Command::new(env!("CARGO")) .args([ if test_source.is_empty() { "check" @@ -78,19 +88,26 @@ fn assert_generated_project(temp_dir: &Path, modules: &[&str], test_source: &str ]) .env("CARGO_TARGET_DIR", project.join("target")) .output() - .expect("cargo check should run"); - assert!( - output.status.success(), - "generated project failed\nstdout: {}\nstderr: {}", - utf8(&output.stdout), - utf8(&output.stderr) - ); + .expect("cargo check should run") } fn utf8(bytes: &[u8]) -> &str { std::str::from_utf8(bytes).expect("process output should be UTF-8") } +fn normalize_current_package_version(value: &str) -> String { + const PLACEHOLDER: &str = ""; + let version = env!("CARGO_PKG_VERSION"); + let mut normalized = value.to_owned(); + let mut search_from = 0; + while let Some(relative_start) = normalized[search_from..].find(version) { + let start = search_from + relative_start; + normalized.replace_range(start..start + version.len(), PLACEHOLDER); + search_from = start + PLACEHOLDER.len(); + } + normalized +} + fn generated_parser_api(source: &str) -> Vec { let mut api = BTreeSet::new(); for line in source.lines().map(str::trim) { @@ -232,6 +249,78 @@ fn long_and_short_version_exit_successfully_on_stdout() { } } +#[allow(clippy::disallowed_methods)] // `insta` assertion macros unwrap internal I/O. +#[test] +fn generated_modules_enforce_codegen_api_compatibility() { + let temp = temporary_directory("codegen-api-compatibility"); + let grammar = temp.path().join("CodegenApi.g4"); + let out = temp.path().join("generated"); + fs::write( + &grammar, + "grammar CodegenApi;\n\ + root : ID EOF ;\n\ + ID : [a-z]+ ;\n\ + WS : [ \\t\\r\\n]+ -> skip ;\n", + ) + .expect("grammar should be writable"); + + 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) + ); + + let lexer_path = out.join("codegen_api_lexer.rs"); + let parser_path = out.join("codegen_api_parser.rs"); + let lexer = fs::read_to_string(&lexer_path).expect("lexer should be emitted"); + let parser = fs::read_to_string(&parser_path).expect("parser should be emitted"); + let lexer_check = lexer + .lines() + .find(|line| line.contains("__antlr4_rust_require_codegen_api")) + .expect("lexer should require a generated-code API"); + let parser_check = parser + .lines() + .find(|line| line.contains("__antlr4_rust_require_codegen_api")) + .expect("parser should require a generated-code API"); + let checks = format!("lexer: {lexer_check}\nparser: {parser_check}"); + insta::assert_snapshot!( + "generated_codegen_api_checks", + normalize_current_package_version(&checks) + ); + + let modules = ["codegen_api_lexer.rs", "codegen_api_parser.rs"]; + assert_generated_modules_compile(temp.path(), &modules); + + let current = format!( + "__antlr4_rust_require_codegen_api!({},", + antlr4_runtime::__ANTLR4_RUST_CODEGEN_API + ); + let unsupported = "__antlr4_rust_require_codegen_api!(999,"; + let mut incompatible_parser = parser; + let check_start = incompatible_parser + .find(¤t) + .expect("parser check should contain the current API revision"); + incompatible_parser.replace_range(check_start..check_start + current.len(), unsupported); + fs::write(parser_path, incompatible_parser).expect("parser should be writable"); + + let output = run_generated_project(temp.path(), &modules, ""); + assert!( + !output.status.success(), + "unsupported generated-code API unexpectedly compiled" + ); + assert_eq!(utf8(&output.stdout), ""); + insta::assert_snapshot!( + "generated_codegen_api_mismatch_diagnostic", + normalize_current_package_version(utf8(&output.stderr)) + ); +} + #[test] fn help_flag_as_option_value_is_not_intercepted() { let output = run_antlr4_rust_gen(&["--option-hook", "--help"]); diff --git a/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_checks.snap b/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_checks.snap new file mode 100644 index 00000000..dbb77be2 --- /dev/null +++ b/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_checks.snap @@ -0,0 +1,6 @@ +--- +source: tests/antlr4_rust_gen_cli.rs +expression: normalize_current_package_version(&checks) +--- +lexer: antlr4_runtime::__antlr4_rust_require_codegen_api!(1, ""); +parser: antlr4_runtime::__antlr4_rust_require_codegen_api!(1, ""); diff --git a/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap b/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap new file mode 100644 index 00000000..47df227d --- /dev/null +++ b/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap @@ -0,0 +1,13 @@ +--- +source: tests/antlr4_rust_gen_cli.rs +expression: normalize_current_package_version(utf8(&output.stderr)) +--- +error: antlr4-rust generated-code API mismatch: antlr4-rust-gen v emitted generated-code API revision 999, but the selected antlr-rust-runtime supports revision 1; regenerate this recognizer with a compatible antlr4-rust-gen or select a compatible antlr-rust-runtime dependency + --> src/codegen_api_parser.rs:3:1 + | +3 | antlr4_runtime::__antlr4_rust_require_codegen_api!(999, ""); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `antlr4_runtime::__antlr4_rust_require_codegen_api` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: could not compile `compile-generated` (lib) due to 1 previous error diff --git a/third_party/antlr-v4-grammar/self-hosted.sha256 b/third_party/antlr-v4-grammar/self-hosted.sha256 index 0eb063fc..018e41c4 100644 --- a/third_party/antlr-v4-grammar/self-hosted.sha256 +++ b/third_party/antlr-v4-grammar/self-hosted.sha256 @@ -2,5 +2,5 @@ d1c01af37d665bd94f318c25265bbbf07620689effeb562b460cf75d361d2ee9 third_party/an 1286e542499e4480b3ab5ff60e4a4a7faf21134ca4c4f8f1f468f30095fa25cb third_party/antlr-v4-grammar/ANTLRv4Parser.g4 c7114545a75ab294215819962e92e570383dc830fd5768463dab04e6733bcb80 third_party/antlr-v4-grammar/predefined.tokens 5803594bd2c8dd2d5180f1ca08fc70dfc80308479d18a7c4a1b743fa523b55ec third_party/antlr-v4-grammar/antlr-v4.toml -c189e9b47309e76c3427a89532ddc5b836ea477244f278685f10d241509c7b70 src/bin_support/grammar/generated/antlr_v4_lexer.rs -e07431c0f78a122fe2f771f4e28ef66130384f0ea2da0805109aa582cc8a2687 src/bin_support/grammar/generated/antlr_v4_parser.rs +7c917b2aa0464a0c844ddae1145e9640317798eb64559fbab9af46c3ae45d68e src/bin_support/grammar/generated/antlr_v4_lexer.rs +4aa336b7de3fd44f004ff1f943648c1279ea3d265b25e3631c91f953491a8884 src/bin_support/grammar/generated/antlr_v4_parser.rs From 5fb701dd8ec763c56f49ba0cf46e207ae3257101 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Sat, 1 Aug 2026 14:39:32 +0200 Subject: [PATCH 2/6] test(codegen): snapshot generated module header Pin the complete file-level header while normalizing only the package version, preserving the existing structural assertions for derived ordering properties. --- src/bin/antlr4-rust-gen.rs | 5 +++++ ..._rust_gen__tests__generated_module_file_header.snap | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 src/bin/snapshots/antlr4_rust_gen__tests__generated_module_file_header.snap diff --git a/src/bin/antlr4-rust-gen.rs b/src/bin/antlr4-rust-gen.rs index e7510792..5d68fedb 100644 --- a/src/bin/antlr4-rust-gen.rs +++ b/src/bin/antlr4-rust-gen.rs @@ -15808,6 +15808,7 @@ mod tests { ); } + #[allow(clippy::disallowed_methods)] // `insta` assertion macros unwrap internal I/O. #[test] fn generated_modules_start_with_file_level_header() { let lexer = render_lexer( @@ -15823,6 +15824,10 @@ mod tests { render_parser("TParser", &minimal_parser_data()).expect("parser should render"); let generated_module_header = generated_module_header(); + insta::assert_snapshot!( + "generated_module_file_header", + generated_module_header.replace(env!("CARGO_PKG_VERSION"), "") + ); assert!(generated_module_header.contains(concat!( "@generated by ", env!("CARGO_PKG_NAME"), diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__generated_module_file_header.snap b/src/bin/snapshots/antlr4_rust_gen__tests__generated_module_file_header.snap new file mode 100644 index 00000000..0bb3a899 --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__generated_module_file_header.snap @@ -0,0 +1,10 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "generated_module_header.replace(env!(\"CARGO_PKG_VERSION\"),\n\"\")" +--- +// @generated by antlr-rust-runtime v - do not edit +// project: https://github.com/ophi-dev/antlr-rust-runtime +antlr4_runtime::__antlr4_rust_require_codegen_api!(1, ""); +#[allow(warnings, missing_docs, clippy::all, clippy::pedantic, clippy::nursery)] +#[rustfmt::skip] +mod __antlr4_rust_generated { From 68783c849b6122f8e685f4b0d78ca47c2bd531e1 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Sat, 1 Aug 2026 14:48:26 +0200 Subject: [PATCH 3/6] docs(codegen): clarify legacy runtime failures Qualify the targeted compatibility diagnostic for runtimes that implement the handshake, and document the safe missing-macro failure and remediation for older runtimes. --- README.md | 5 ++++- docs/migration.md | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d974588f..03513d97 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,10 @@ Every newly generated lexer and parser contains a compile-time generated-code API check. The check records both the API revision and the `antlr4-rust-gen` package version that produced the module. A mismatch reports the generated file directly and asks you to either regenerate it with a -compatible generator or select a compatible `antlr-rust-runtime` dependency. +compatible generator or select a compatible `antlr-rust-runtime` dependency, +provided the selected runtime implements this check. A runtime released before +the check was introduced instead reports that the generated-code API macro is +missing; upgrade that runtime or regenerate with its matching older generator. The generated-code API revision tracks the Rust source interface between generated recognizers and the runtime. It is independent of package SemVer and diff --git a/docs/migration.md b/docs/migration.md index 8d360ef1..4a21165c 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -12,6 +12,9 @@ no enforceable revision. Regenerate every committed lexer and parser once when first adopting a release with this mechanism. If a later build reports a generated-code API mismatch, either regenerate the named module with a compatible generator or select a runtime that accepts its requested revision. +When new generated source is compiled against a runtime that predates the +check, Rust reports the missing generated-code API macro instead; upgrade that +runtime or regenerate with its matching older generator. `antlr4-rust-gen --version` reports the generator package version for auditing and reproducible regeneration. From 7bb17c934d152e0c5de7b045d1badb8b95335c26 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Sat, 1 Aug 2026 14:57:51 +0200 Subject: [PATCH 4/6] test(codegen): stabilize mismatch diagnostics Disable Cargo color in nested generated-project checks and snapshot only the first compiler diagnostic block. This keeps coverage wrappers and CI terminal settings from changing the compatibility contract snapshot. --- tests/antlr4_rust_gen_cli.rs | 7 ++++++- ...gen_cli__generated_codegen_api_mismatch_diagnostic.snap | 4 +--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/antlr4_rust_gen_cli.rs b/tests/antlr4_rust_gen_cli.rs index 43b34e7e..729132c8 100644 --- a/tests/antlr4_rust_gen_cli.rs +++ b/tests/antlr4_rust_gen_cli.rs @@ -87,6 +87,7 @@ fn run_generated_project(temp_dir: &Path, modules: &[&str], test_source: &str) - .expect("temporary path should be UTF-8"), ]) .env("CARGO_TARGET_DIR", project.join("target")) + .env("CARGO_TERM_COLOR", "never") .output() .expect("cargo check should run") } @@ -315,9 +316,13 @@ fn generated_modules_enforce_codegen_api_compatibility() { "unsupported generated-code API unexpectedly compiled" ); assert_eq!(utf8(&output.stdout), ""); + let stderr = utf8(&output.stderr); + let diagnostic = stderr + .split_once("\n\n") + .map_or(stderr, |(diagnostic, _)| diagnostic); insta::assert_snapshot!( "generated_codegen_api_mismatch_diagnostic", - normalize_current_package_version(utf8(&output.stderr)) + normalize_current_package_version(diagnostic) ); } diff --git a/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap b/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap index 47df227d..6a159edc 100644 --- a/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap +++ b/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap @@ -1,6 +1,6 @@ --- source: tests/antlr4_rust_gen_cli.rs -expression: normalize_current_package_version(utf8(&output.stderr)) +expression: normalize_current_package_version(diagnostic) --- error: antlr4-rust generated-code API mismatch: antlr4-rust-gen v emitted generated-code API revision 999, but the selected antlr-rust-runtime supports revision 1; regenerate this recognizer with a compatible antlr4-rust-gen or select a compatible antlr-rust-runtime dependency --> src/codegen_api_parser.rs:3:1 @@ -9,5 +9,3 @@ error: antlr4-rust generated-code API mismatch: antlr4-rust-gen v Date: Sat, 1 Aug 2026 17:12:49 +0200 Subject: [PATCH 5/6] test(codegen): stabilize mismatch snapshot across versions Keep the generated-code compatibility diagnostic snapshot focused on the error message and source location. Rustc caret widths depend on the package version embedded in the generated macro invocation, so pinning renderer detail made release version-length changes fail unrelated tests. --- tests/antlr4_rust_gen_cli.rs | 11 ++++++----- ...li__generated_codegen_api_mismatch_diagnostic.snap | 7 +------ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/antlr4_rust_gen_cli.rs b/tests/antlr4_rust_gen_cli.rs index 729132c8..732b4083 100644 --- a/tests/antlr4_rust_gen_cli.rs +++ b/tests/antlr4_rust_gen_cli.rs @@ -316,13 +316,14 @@ fn generated_modules_enforce_codegen_api_compatibility() { "unsupported generated-code API unexpectedly compiled" ); assert_eq!(utf8(&output.stdout), ""); - let stderr = utf8(&output.stderr); - let diagnostic = stderr - .split_once("\n\n") - .map_or(stderr, |(diagnostic, _)| diagnostic); + let diagnostic = utf8(&output.stderr) + .lines() + .take(2) + .collect::>() + .join("\n"); insta::assert_snapshot!( "generated_codegen_api_mismatch_diagnostic", - normalize_current_package_version(diagnostic) + normalize_current_package_version(&diagnostic) ); } diff --git a/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap b/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap index 6a159edc..007c5171 100644 --- a/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap +++ b/tests/snapshots/antlr4_rust_gen_cli__generated_codegen_api_mismatch_diagnostic.snap @@ -1,11 +1,6 @@ --- source: tests/antlr4_rust_gen_cli.rs -expression: normalize_current_package_version(diagnostic) +expression: normalize_current_package_version(&diagnostic) --- error: antlr4-rust generated-code API mismatch: antlr4-rust-gen v emitted generated-code API revision 999, but the selected antlr-rust-runtime supports revision 1; regenerate this recognizer with a compatible antlr4-rust-gen or select a compatible antlr-rust-runtime dependency --> src/codegen_api_parser.rs:3:1 - | -3 | antlr4_runtime::__antlr4_rust_require_codegen_api!(999, ""); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `antlr4_runtime::__antlr4_rust_require_codegen_api` (in Nightly builds, run with -Z macro-backtrace for more info) From 212432637c5b007f8eb0913bc1f32d09da0060c0 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Sat, 1 Aug 2026 17:41:22 +0200 Subject: [PATCH 6/6] test(codegen): harden compatibility diagnostics Anchor nested cargo stderr extraction on the generated-code mismatch error so unrelated dependency warnings cannot shift the snapshot. Also tie the diagnostic's supported-revision text to the runtime API constant with an explicit property assertion. --- tests/antlr4_rust_gen_cli.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/antlr4_rust_gen_cli.rs b/tests/antlr4_rust_gen_cli.rs index 732b4083..50cfc4a1 100644 --- a/tests/antlr4_rust_gen_cli.rs +++ b/tests/antlr4_rust_gen_cli.rs @@ -318,9 +318,15 @@ fn generated_modules_enforce_codegen_api_compatibility() { assert_eq!(utf8(&output.stdout), ""); let diagnostic = utf8(&output.stderr) .lines() + .skip_while(|line| !line.starts_with("error:")) .take(2) .collect::>() .join("\n"); + let supported_revision = antlr4_runtime::__ANTLR4_RUST_CODEGEN_API; + assert!( + diagnostic.contains(&format!("supports revision {supported_revision}")), + "diagnostic should name runtime revision {supported_revision}: {diagnostic}" + ); insta::assert_snapshot!( "generated_codegen_api_mismatch_diagnostic", normalize_current_package_version(&diagnostic)