Skip to content

feat(runtime)!: expose resolved byte spans to error listeners - #257

Merged
tinovyatkin merged 4 commits into
mainfrom
codex/issue-251-error-spans
Jul 29, 2026
Merged

feat(runtime)!: expose resolved byte spans to error listeners#257
tinovyatkin merged 4 commits into
mainfrom
codex/issue-251-error-spans

Conversation

@tinovyatkin

@tinovyatkin tinovyatkin commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the positional ErrorListener::syntax_error callback with a non-exhaustive SyntaxErrorEvent
  • resolve parser-token and lexer-failure spans as half-open UTF-8 byte ranges
  • return optional byte boundaries and spans from Token and TokenStore, keeping compact sentinels internal
  • separate scalar TokenSpec::with_span data from explicitly resolved with_byte_span data
  • preserve unknown locations for custom streams, custom token sources, synthetic recovery tokens, and EOF
  • update generated code, integrations, snapshots, README guidance, and migration notes

Why

Rich-diagnostic consumers currently have to retain a second copy of the source,
convert line/column pairs back to byte offsets, and scrape formatted error
messages to recover lexer-error widths. The runtime already knows these
locations.

TokenSourceError now carries the span captured by BaseLexer, while parser
diagnostics derive it from the offending TokenView. Streams and token sources
that cannot provide exact byte offsets return None rather than a fabricated
range. ByteStream retains exact byte ranges.

This intentionally changes the listener and token byte-offset APIs in the
pre-1.0 crate instead of retaining parallel legacy paths. The migration guide
documents the callback and custom-token-source changes, and the durable commit
message carries a BREAKING CHANGE footer.

The listener's Send + 'static bound and the RecognizerData: Send + Sync
guarantee remain unchanged.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --locked --all-targets --all-features -- -D warnings
  • cargo test --locked --all-features --workspace (345 + 3 + 835 + 48 passed)
  • RUSTDOCFLAGS='-D warnings -A rustdoc::private-intra-doc-links' cargo doc --locked --all-features --no-deps --lib

Closes #251

Summary by CodeRabbit

  • New Features

    • Added UTF-8 byte spans to lexer and parser diagnostics when offsets are available.
    • Introduced a unified syntax error event containing location, message, offending token, and optional span details.
    • Token byte offsets now clearly indicate when source positions are unavailable.
  • Bug Fixes

    • Improved error highlighting for multibyte UTF-8 input.
    • Preserved accurate spans for token recognition and parser recovery errors.
  • Documentation

    • Documented diagnostic span behavior and byte-offset requirements in the complete example.

Replace the positional ErrorListener callback with SyntaxErrorEvent so consumers can use resolved parser and lexer spans without retaining source text or scraping diagnostic messages. Record UTF-8 byte ranges on TokenSourceError and preserve them through generated lexer dispatch while keeping the Send + Sync listener guarantee.

Closes #251
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The runtime introduces SyntaxErrorEvent with optional UTF-8 byte spans, makes token byte offsets optional when unresolved, propagates events through lexer and parser diagnostics, updates integrations and generated code, and adds span-focused regression tests and documentation.

Changes

Structured error spans

Layer / File(s) Summary
Diagnostic event and token span contracts
src/errors.rs, src/token.rs, src/lib.rs
SyntaxErrorEvent, optional token byte spans, TokenSourceError.span, and the public re-export define the structured diagnostic data model.

Lexer span resolution

Layer / File(s) Summary
Lexer span resolution
src/lexer.rs, src/atn/lexer.rs
Scalar lexer ranges are mapped to UTF-8 byte ranges when available, while unknown boundaries remain absent; lexer tests cover multibyte and empty spans.

Recognizer and parser dispatch

Layer / File(s) Summary
Recognizer and parser dispatch
src/recognizer.rs, src/parser.rs
Recognizer and parser paths construct, forward, and record SyntaxErrorEvent values instead of positional diagnostic arguments.

Integration and generated-code migration

Layer / File(s) Summary
Integration and generated-code migration
src/bin/antlr4-rust-gen.rs, src/bin_support/grammar/frontend.rs, tests/antlr4_rust_gen_cli.rs, src/atn/lexer_dfa.rs, src/bin_support/grammar/atn/interp_test.rs, src/tree.rs, README.md
Generated code, frontend diagnostics, fixtures, snapshots, tests, and documentation adopt event-based callbacks and optional byte-span assertions.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Lexer
  participant Parser
  participant Recognizer
  participant ErrorListener
  Lexer->>Lexer: resolve scalar range to byte span
  Lexer->>Recognizer: notify_error_listeners(source_error.into())
  Parser->>Recognizer: notify_error_listeners(SyntaxErrorEvent)
  Recognizer->>ErrorListener: forward structured event
  ErrorListener->>ErrorListener: consume line, column, message, and span
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Core span plumbing landed, but the issue required a defaulted listener path so existing ErrorListener impls keep compiling, and this PR makes it a breaking change. Add a defaulted event-based callback or otherwise preserve the existing syntax_error signature so current listeners continue compiling.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes stay focused on error-event spans, token byte-span plumbing, tests, and related documentation.
Docstring Coverage ✅ Passed Docstring coverage is 85.92% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main breaking change: exposing resolved byte spans to error listeners.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-251-error-spans

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Code Review in progress

  • Read the full PR diff (git diff origin/main...HEAD)
  • Review the SyntaxErrorEvent / TokenSourceError contract (src/errors.rs, src/recognizer.rs)
  • Review lexer byte-span resolution (src/lexer.rs, src/atn/lexer.rs)
  • Review compact token store sentinel + Option accessors (src/token.rs)
  • Review parser dispatch + generated-code/doc migration
  • Cross-check against ANTLR v4.13.2 upstream where behavior is ported
  • Verify candidate findings (build/tests/targeted probes)
  • Post consolidated review

View job run · branch codex/issue-251-error-spans

@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

📊 Source Code Metrics (this PR vs main)

File Cyclomatic Cognitive Functions LLOC MI
src/parser.rs 2170 (main: 2169) 🔴 1422 (main: 1423) 🟢 702 (main: 699) 🔴 4715 (main: 4704) 🔴 0 ⚪
src/lexer.rs 336 (main: 326) 🔴 76 (main: 70) 🔴 206 (main: 201) 🔴 570 (main: 547) 🔴 0 ⚪
src/atn/lexer.rs 467 (main: 464) 🔴 347 ⚪ 120 (main: 118) 🔴 1030 (main: 1017) 🔴 0 ⚪
src/token.rs 182 (main: 178) 🔴 49 (main: 44) 🔴 94 (main: 91) 🔴 202 (main: 189) 🔴 0 ⚪
src/bin_support/grammar/frontend.rs 227 ⚪ 91 ⚪ 68 ⚪ 311 (main: 309) 🔴 0 ⚪
src/recognizer.rs 53 ⚪ 2 ⚪ 39 ⚪ 77 ⚪ 6.87 (main: 6.17) 🟢
src/errors.rs 6 (main: 4) 🔴 0 ⚪ 2 (main: 1) 🔴 2 (main: 1) 🔴 33.06 (main: 36.55) 🔴
src/lib.rs 4 ⚪ 3 ⚪ 1 ⚪ 7 ⚪ 32.47 (main: 32.49) 🔴

Generated by mehen v1.7.0 — the code quality watcher.

@tinovyatkin tinovyatkin changed the title feat(runtime): expose resolved byte spans to error listeners feat(runtime)!: expose resolved byte spans to error listeners Jul 29, 2026
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.86466% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/bin_support/grammar/frontend.rs 63.63% 4 Missing ⚠️
src/parser.rs 94.59% 4 Missing ⚠️
src/token.rs 95.23% 2 Missing ⚠️
src/lexer.rs 98.63% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: decd497e01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/parser.rs Outdated
Comment thread src/parser.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/parser.rs`:
- Around line 5271-5283: Extract the shared SyntaxErrorEvent construction used
by report_unrecovered_parser_error and dispatch_parser_diagnostic into a private
helper on the same parser type. Have the helper resolve the optional offending
token through token_store(), derive span from the resolved token, and accept the
line, column, message, and error values; update both callers to delegate to it
while preserving their existing event behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9e8170fd-5ffb-4858-880c-f8ae9d8857a1

📥 Commits

Reviewing files that changed from the base of the PR and between b7af582 and decd497.

⛔ Files ignored due to path filters (8)
  • src/bin_support/grammar/generated/antlr_v4_lexer.rs is excluded by !**/generated/**
  • src/snapshots/antlr4_runtime__parser__tests__failed_interpreted_parse_notifies_error_listener.snap is excluded by !**/*.snap
  • src/snapshots/antlr4_runtime__parser__tests__parser_dispatches_recovery_diagnostics_through_registered_listeners.snap is excluded by !**/*.snap
  • src/snapshots/antlr4_runtime__parser__tests__recovery_diagnostics_expose_the_offending_token_to_listeners.snap is excluded by !**/*.snap
  • src/snapshots/antlr4_runtime__recognizer__tests__recognizers_replace_the_default_console_error_listener.snap is excluded by !**/*.snap
  • src/xpath/generated/x_path_lexer.rs is excluded by !**/generated/**
  • tests/snapshots/antlr4_rust_gen_cli__fatal_entry_preserves_prior_recovery_diagnostics.snap is excluded by !**/*.snap
  • tests/snapshots/antlr4_rust_gen_cli__semantic_override_does_not_leak_prior_recovery_diagnostics.snap is excluded by !**/*.snap
📒 Files selected for processing (11)
  • README.md
  • src/atn/lexer.rs
  • src/bin/antlr4-rust-gen.rs
  • src/bin_support/grammar/frontend.rs
  • src/errors.rs
  • src/lexer.rs
  • src/lib.rs
  • src/parser.rs
  • src/recognizer.rs
  • src/token.rs
  • tests/antlr4_rust_gen_cli.rs

Comment thread src/parser.rs
Reserve the compact token boundary sentinel for missing byte positions so parser diagnostics do not expose fabricated spans from custom token sources or synthetic recovery tokens. Make Token::byte_span optional, retain exact ByteStream ranges, and centralize parser event construction.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab93f785ec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/parser.rs
Custom CharStream implementations may expose scalar positions without a UTF-8 byte mapping. Store the reserved unknown boundary on synthetic EOF tokens instead of treating the scalar index as a byte offset.

Cover a non-ASCII scalar-only stream so parser diagnostics at EOF continue to report no span.
Separate scalar and byte TokenSpec builders so custom token sources cannot fabricate UTF-8 ranges. Keep compact sentinels internal while exposing optional byte boundaries and spans from Token and TokenStore.

Document the listener and token migration, make diagnostic event structs non-exhaustive, and expose explicit scalar-span lexer diagnostics.

BREAKING CHANGE: ErrorListener::syntax_error now receives SyntaxErrorEvent; Token byte accessors return Option values; TokenSourceError includes span and diagnostic structs are non-exhaustive.
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

Addressed the review in eeb797a46:

  • TokenSpec::with_span now changes only Unicode-scalar coordinates; exact UTF-8 coordinates require with_byte_span, and the parser regression now covers a custom token with scalar coordinates but no byte mapping.
  • Token::{start_byte, stop_byte, byte_span} and the matching TokenStore APIs expose Option values, so the compact sentinel no longer crosses the public accessor boundary. The grammar frontend now reports unavailable spans directly instead of misclassifying them as offsets over 4 GiB.
  • Added the migration section and a durable BREAKING CHANGE commit footer.
  • Marked SyntaxErrorEvent and TokenSourceError non-exhaustive.
  • Made record_error_for_scalar_span public with its mapping contract documented, documented record_error's current-token derivation, clarified the defensive lexer-width clamp, and replaced the awkward conversion call with SyntaxErrorEvent::from.

Validation on the new head:

  • cargo fmt --all -- --check
  • cargo clippy --locked --all-targets --all-features -- -D warnings
  • cargo test --locked --all-features --workspace (345 + 3 + 835 + 48 passed)
  • strict library rustdoc with the repository's pre-existing private-link lint allowed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/antlr4_rust_gen_cli.rs`:
- Around line 1285-1292: Add #[allow(clippy::disallowed_methods)] to the
combined_literal_tokens_are_public_and_lexable test or its enclosing test scope
in tests/antlr4_rust_gen_cli.rs (anchor site 1285-1292), and to the relevant mod
tests or affected test function in src/tree.rs (sibling site 1558). Ensure both
scopes using insta assertion macros carry the allowance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2b349a65-47b8-47c7-9169-7065173aa39a

📥 Commits

Reviewing files that changed from the base of the PR and between decd497 and eeb797a.

⛔ Files ignored due to path filters (1)
  • docs/migration.md is excluded by !**/docs/**
📒 Files selected for processing (10)
  • src/atn/lexer.rs
  • src/atn/lexer_dfa.rs
  • src/bin_support/grammar/atn/interp_test.rs
  • src/bin_support/grammar/frontend.rs
  • src/errors.rs
  • src/lexer.rs
  • src/parser.rs
  • src/token.rs
  • src/tree.rs
  • tests/antlr4_rust_gen_cli.rs

Comment thread tests/antlr4_rust_gen_cli.rs
@tinovyatkin
tinovyatkin merged commit b91b4ca into main Jul 29, 2026
16 of 18 checks passed
@tinovyatkin
tinovyatkin deleted the codex/issue-251-error-spans branch July 29, 2026 22:54
@ophiarch ophiarch Bot mentioned this pull request Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DX: error listeners should receive a resolved byte span (consumers reconstruct it from line/column and by scraping our message text)

1 participant