Skip to content

fix!: keep syntax errors local to the mistake - #93

Merged
exKAZUu merged 2 commits into
mainfrom
fix/local-error-recovery
Nov 3, 2026
Merged

exKAZUu merged 2 commits into
mainfrom
fix/local-error-recovery

Conversation

@exKAZUu

@exKAZUu exKAZUu commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Why

A downstream app reports syntax errors to programming beginners from this parser's ERROR/MISSING nodes. On main, many ordinary mistakes become one ERROR spanning the whole program, so the error is reported at line 1. For a missing semicolon, for example:

class Main {
  public static void main(String[] args) {
    int 回数 = 1
  }

}

yields (program (ERROR ... (multiline_string_fragment [3, 3] - [6, 0]))). Three causes, confirmed with tree-sitter parse -d:

  1. Error recovery lexes with every token valid, and the string-fragment tokens ([^"\\]{1,256}, …) match } and long spans of code, so recovery swallows the rest of the file.
  2. String interpolations closed with their own } token, distinct from the block }. After 1, the lexer returned the interpolation }, so recovery could not insert a MISSING ; before it.
  3. Whitespace was an extra token pushed onto the stack, so a MISSING token was placed after it, on the next token's line.

Requirements

  • required: syntax errors in beginner programs yield ERROR/MISSING nodes at the mistake rather than one ERROR spanning the program, so that a downstream app can report the line of the mistake
  • required: improve our fork rather than work around it downstream
  • required: valid input still parses correctly, and invalid input never crashes or hangs and parses in linear time
  • chosen: refuse string fragments only during error recovery, detected with an external sentinel that no rule accepts, because the fragments stay context-only tokens in normal parsing

Customer Summary

Syntax errors are now located at the mistake. On the downstream app's corpus, the first ERROR/MISSING node is on the mistake's line for every syntactic example except an unterminated block comment (see Notes):

Example Before After
semicolon ERROR line 1 MISSING ; line 3
for-semicolon MISSING ; line 3 MISSING ; line 3
parenthesis MISSING ) line 3 MISSING ) line 3
bracket MISSING ] line 3 MISSING ] line 3
brace (one line) MISSING } line 1 MISSING } line 1
string ERROR line 1 ERROR line 3
character ERROR line 1 ERROR line 3
empty-character ERROR line 1 ERROR line 3
comment ERROR line 1 ERROR line 1
fullwidth ERROR line 1 ERROR line 3
escape ERROR line 1 ERROR line 3
identifier MISSING identifier line 3 MISSING identifier line 3
expression ERROR line 3 ERROR line 3
assignment-target ERROR line 1 ERROR line 3
else ERROR line 1 ERROR line 3
array-dimension ERROR line 3 ERROR line 3
try ERROR line 1 ERROR line 3
catch ERROR line 1 ERROR line 3

Technical Summary

  1. src/scanner.c lexes string_fragment, _non_u_string_fragment, _multiline_string_fragment, and _non_u_multiline_string_fragment with the same character sets as the former patterns and, like them, read at most 256 characters after any leading quotes. In text blocks, one or two leading quotes (raw or Unicode-escaped) belong to a fragment and three are left to the generated lexer as the closing delimiter. When _error_sentinel is valid (error recovery), no fragment or comment end is read.
  2. string_interpolation closes with the block } token; its opening became the hidden token _string_interpolation_open, aliased to \{ as before.
  3. Raw Java whitespace is padding: a separator in extras and skipped by the scanner before its own tokens. During recovery the scanner still returns it as _java_whitespace, which lets recovery resume after an unrecognized character such as a fullwidth ;. Unicode-escaped whitespace stays a _java_whitespace token.
  4. In a single-line string literal, a raw or encoded line terminator yields an empty _error_sentinel. Otherwise the fallback lexing in error mode would read the line terminator as whitespace, which is valid between any tokens, and continue the literal on the next line.

What to check hardest: that the scanner's fragment rules match the former token patterns exactly, especially quotes in text blocks. The corpus, string/text-block unit tests, and the pinned real-world examples (script/parse-examples --check) cover this.

Testing

  • bun run verify passed.
  • bun wb test passed: all unit tests, including the native corpus and the real-world example check, and the e2e tests.
  • Added test/unit/errorLocations.test.ts, which checks that the first ERROR/MISSING node is on the mistake's line for a missing semicolon, an unterminated string literal (raw and encoded line feed), a fullwidth semicolon, a two-character character literal, an else after an empty statement, and a try without catch.
  • Added a linear-time check for an unterminated string literal on each line to test/unit/performance.test.ts.

Notes

  • Breaking: trees produced by error recovery change, so this needs a major release. Node types and fields of valid input are unchanged.
  • The unterminated block comment (/* unfinished inside main) still yields an ERROR from line 1. The comment runs to EOF and hides both closing braces. At EOF, recovery can insert only one MISSING token, and a MISSING comment end does not let EOF be shifted, so recovery wraps the program from its start. Ending the comment early and re-lexing its body as code would rescan unterminated comments repeatedly and break the linear-time guarantee.
  • 1 + 2; (the downstream corpus's statement example) parses without error because the grammar accepts any expression as a statement; it is not part of this change.

exKAZUu and others added 2 commits November 3, 2026 18:35
Lex string fragments in the external scanner and refuse them during error
recovery, which no longer swallows ordinary code such as closing braces.
Share one closing-brace token between blocks and string interpolations so
that a missing semicolon before `}` yields a MISSING node, skip raw
whitespace as padding so that such a node sits right after the preceding
token, and stop a single-line string literal at a line terminator with a
token that no rule accepts.

Co-authored-by: WillBooster (Claude Code) <agent@willbooster.com>
Co-authored-by: WillBooster (Claude Code) <agent@willbooster.com>
@exKAZUu exKAZUu self-assigned this Nov 3, 2026
@exKAZUu
exKAZUu merged commit 6c17bfe into main Nov 3, 2026
10 checks passed
@exKAZUu
exKAZUu deleted the fix/local-error-recovery branch November 3, 2026 10:22
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.

1 participant