Skip to content

Surface lexer errors instead of misreporting them as EOF#281

Closed
git-hulk wants to merge 3 commits into
masterfrom
fix/surface-lexer-errors
Closed

Surface lexer errors instead of misreporting them as EOF#281
git-hulk wants to merge 3 commits into
masterfrom
fix/surface-lexer-errors

Conversation

@git-hulk

@git-hulk git-hulk commented Jul 5, 2026

Copy link
Copy Markdown
Member

Summary

Dozens of parser call sites discard consumeToken's error (_ = p.lexer.consumeToken()). When the lexer failed mid-statement, the current token stayed nil and the parser read that as end of input:

Input Before After
SELECT 0x line 1:8 unexpected token kind: <eof> line 1:8 invalid number
SELECT 'unterminated misleading EOF error invalid string
SELECT \unclosed` misleading EOF error unclosed quoted identifier
SELECT 1 'oops parsed successfully as SELECT 1 invalid string
SELECT 1; SELECT 'bad; SELECT 2 parsed successfully, dropped the tail invalid string

Approach

Rather than threading error returns through every call site (huge churn), the lexer records its first failure + offset in lexerState:

  • consumeToken is now a thin wrapper that stashes the error before returning it.
  • Because the fields live in lexerState, the existing saveState/restoreState backtracking automatically clears failures that were only hit during rolled-back lookahead — no false positives on successful parses.
  • wrapError prefers the stashed lexer error over the downstream symptom the parser tripped on, reporting the real cause at the offset it happened (correct line/column/caret).
  • Statements that previously "succeeded" despite a trailing lexer error now fail via the re-lex at the top of the ParseStmts loop.

No golden fixture changed; new tests in error_test.go cover the messages, the reported position, and lookahead rollback.

Complements #280 (which makes the lexer produce errors for unterminated comments / non-ASCII bytes; this PR makes the parser report them).

🤖 Generated with Claude Code

Dozens of parser call sites discard consumeToken's error
(`_ = p.lexer.consumeToken()`). When the lexer failed mid-statement
(invalid number, unterminated string, unclosed quoted identifier), the
current token stayed nil and the parser read that as end of input,
producing misleading errors:

    SELECT 0x  =>  line 1:8 unexpected token kind: <eof>

or, worse, silent success when the statement happened to look complete
at the point of failure: `SELECT 1 'oops` parsed cleanly as SELECT 1.

Rather than threading error returns through every call site, the lexer
now records its first failure (error + offset) in lexerState:

- consumeToken stashes the error; the existing save/restore backtracking
  clears failures that were only hit during rolled-back lookahead, since
  the fields live in lexerState.
- wrapError prefers the stashed lexer error over whatever downstream
  symptom the parser tripped on, reporting the real cause at the offset
  it happened:

    SELECT 0x  =>  line 1:8 invalid number

- Statements that previously succeeded despite a trailing lexer error
  now fail via the re-lex at the top of the ParseStmts loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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: 82319d4a9c

ℹ️ 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 parser/parser_common.go
CI's golangci-lint predates Go 1.21 builtins and fails typecheck on
min() even though go.mod declares go 1.21. Clamp with a plain
comparison instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@git-hulk
git-hulk requested a review from aftership-abas July 6, 2026 07:37

@aftership-abas aftership-abas 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.

Changes Requested. The main correctness risk is already covered by the existing inline thread: recorded lexer failures still need to be checked before accepting an otherwise successful parse, so malformed trailing input cannot be silently accepted. I reviewed the lexer/parser error propagation changes, the regression tests, existing PR feedback, and CI status; I did not run local tests because CI owns automated checks for this flash review.

Review follow-up: lastError was only consulted through wrapError, which
only runs when the parser itself fails. A failed lex can leave the
offset advanced - consumeIdent skips the opening backtick before
discovering the quote is unclosed - so with `SELECT 1 `bad` the
retained input re-lexes cleanly to EOF, parsing reaches the success
path, and the recorded error was silently dropped.

- ParseStmts now fails if lexerState still carries an error when
  parsing reaches end of input.
- peekToken restores the saved state on the error path too, so a failed
  lookahead neither leaves the offset advanced nor records an error for
  input the parser has not actually reached.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@git-hulk git-hulk closed this Jul 6, 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.

2 participants