Repository navigation
perf: skip avoidable lexer calls in the external scanner - #95
Merged
Merged
Conversation
Read plain ASCII identifier starts and comment characters without the Unicode-escape decoding path, reject other plain ASCII characters before advancing, call `eof` only when the lookahead is 0, and mark the end of a comment body only before a character that may end it. Tokens and trees stay the same. Co-authored-by: WillBooster (Claude Code) <agent@willbooster.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Since the scanner started to recognize Unicode escapes before tokenization, it reads every identifier, whitespace run, and comment through its decoding path, and the generated lexer only gets what the scanner rejects. On the same runtime, a sample of 200 Java files (UTF-16 input, min of 7 rounds × 5 repetitions) parses in 59.2 ms with v18.0.0 and 73.5 ms with v32.0.0;
scan_tokentakes about 16% of the parse time in a profile. Most of what it does there is per-character overhead that the decoding does not need: aneofcall before every character, a call ofmark_endafter every comment character, the decoding call for a plain ASCII letter, and, for operators and separators, reading one character only to reject it.This change removes that overhead without changing which tokens the scanner produces. Letting the generated lexer read plain identifiers was rejected: the scanner can tell that a word has no escape only after reading the whole word, so it would read every identifier twice.
Requirements
src/scanner.cand keep its token decisions, because any grammar change would regenerate the parser and needs its own proof of identical trees.Customer Summary
Java parsing is about 6% faster (73.5 → 69.2 ms on the 200-file sample; 2996 → 2822 ms on the whole
examples/corpus). Long comments parse 30–40% faster. Trees are unchanged.Technical Summary
All changes are in
src/scanner.c:scan_tokenreads a plain ASCII identifier start withoutnext_code_point, and returns false before advancing for any other plain ASCII character that starts no token it reads (anything but whitespace,/,\, and U+001A). A false return is undone by the runtime's lexer reset, so reading less before it changes no token.eofis called only when the lookahead is 0, which the runtime sets at the end of the input. The checks that follow a test for a non-zero character (hex digits, raw whitespace) drop it.skip_comment_charactersand marks its end only before a character that may end it (\, CR, LF,/, or 0). The last mark therefore lands where marking after every character put it.next_identifier_partholds the per-character step ofscan_identifier, with one call ofidentifier_part, so that the compiler inlines its ASCII path.spellreturns early once the spelling overflows, andis_keywordcompares first characters before callingstrcmp.What to check hardest: whether the early rejection in item 1 covers every character for which the following code could produce a token, and whether the lazy end marking in item 3 leaves the same token end for terminated, unterminated, and malformed-escape comments.
Testing
mainon the wholeexamples/corpus (7,940 files), on the inputs of everytest/corpuscase (166), and on 16,152 generated files. The generated files are mutated corpus inputs and example files with random Unicode escapes (\uuuu0041, escaped keywords, escaped quotes,\u000aand\u000d, escaped comment delimiters, odd and even backslash runs, lone and paired surrogates, malformed escapes, NUL, U+001A, ignorable characters, invalid UTF-8), plus random snippet mixes. About 95% of them have syntax errors.\uuu…runs, long line, block, and unterminated comments, backslash runs, whitespace, encoded whitespace, strings, surrogates, and repeated statements take about 4× as long at 4× the size, both before and after this change, with identical trees.bun run verifyandbun wb test(unit and e2e, with the wasm rebuilt from this scanner) pass.src/and finds no drift on three platforms and runs the fuzzer, and the Rust tests.