fix(tokenizer): add the missing 'whitespace' char class and EndStatemend to the 'lower' group - #581
Merged
msarson merged 1 commit intoSep 17, 2026
Conversation
…ent to the 'lower' group PatternMatcher.getCharClass returns 'whitespace' for ' ' and '\t', but charClassGroups had no such key, so the per-position lookup in ClarionTokenizer.tokenizeLines fell back to testing all 33 token patterns. Whitespace is roughly a third of a Clarion source file and the tokenizer never skips it (it advances one character when nothing matches), so 92.5% of the 5,666,201 regex exec calls in one tokenize of a 770 KB / 14,512-line file started at a whitespace position; only 1.29% of all exec calls matched at index 0. The fallback was also masking a second gap: TokenType.EndStatement was listed in the 'upper' char-class group but not in 'lower'. A lowercase `end` only tokenized correctly when indented, because the all-patterns fallback matched EndStatement's /^\s*(END)\b/ at the preceding space. A column-0 lowercase `end` had no pattern that accepted it and lost its leading character to the single-character fallback advance, tokenizing as Variable "nd". Adding the 'whitespace' group alone therefore regressed 9 of 241 files; the two changes go together. Fix: 'whitespace': [] (nothing needs to match AT a whitespace position: LineContinuation re-matches at the '|' itself, and the MODULE and TOOLBAR structure patterns re-match at the keyword through their zero-width leading \s*), and EndStatement added to 'lower'. Measured: lex-only 587.8 ms -> ~80 ms on the same file (full tokenize 633 -> 170 ms); 0 token-stream differences (type, value, line, start) across 241 files / 14.3 MB. Tests: ClarionTokenizer.LowercaseEndCharClass.test.ts (5). Without the fix 3 of them fail (column-0 lowercase `end` -> Variable "nd"; EndStatement missing from 'lower'; no 'whitespace' group). With it the full suite is 3073 passing / 0 failing / 4 pending. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
msarson
added a commit
that referenced
this pull request
Sep 17, 2026
#584) `ACCESS_TOKEN:END = INSTRING(...)` produced an END statement and closed the enclosing LOOP: the keyword branch skips a match preceded by ':' or '.' (nts:case, obj.case), but the EndStatement pattern had no such check. Found reviewing #581, which lets lower case reach the END pattern and would have moved a real closure in libcurldropbox.clw (IF at 92 closing at 94, not 98). Two places: - ClarionTokenizer: the END word of the EndStatement pattern is skipped when preceded by ':' or '.'. A period terminator is not a word and is unaffected. - DocumentStructure's same-line terminator scan (#536) counted any token whose value is END, so `IF access_token:end > 0` read as a one-line IF closed by its own condition. It now counts END statement tokens (and periods) only. Red: 2 failing (ACCESS_TOKEN:END extra END statements; access_token:end LOOP closing at the IF's END). Guards (Tok:End, Obj.End, real END in either case and after ';') pass. Green: 5 passing; server suite 3080. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
msarson
added a commit
that referenced
this pull request
Sep 17, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner
|
Merged, thanks Geir — a big win: across our 7,237-file corpus tokenizing dropped from 67 s to 19 s, and the longest event-loop block while loading the DirectSystems solution from 1.4 s to 0.9 s. One thing the full-corpus comparison turned up: letting lower case reach the END pattern also turned |
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.
fix(tokenizer): add the missing 'whitespace' char class and EndStatement to the 'lower' group
Branch
fix/lexer-whitespace-charclass, targetsversion-1.0.4.What happens
Tokenizing a large source file is dominated by the lexer rather than the structure pass: on a 770 KB / 14,512-line module, lexing took 587.8 ms of a 633 ms full tokenize. Every retokenize while typing pays that, and every feature that needs fresh tokens waits for it.
Separately, a lowercase
endat column 0 tokenizes asVariable "nd"— the leadingeis dropped — whileENDat column 0 and an indented lowercaseendboth tokenize correctly asEndStatement.Root cause
PatternMatcher.getCharClassclassifies' 'and'\t'as'whitespace', butcharClassGroupshad no'whitespace'entry. The per-position lookup inClarionTokenizer.tokenizeLines:fell through
undefinedtotypes— all 33 token patterns — at every whitespace position. Whitespace is roughly a third of the characters in a Clarion source file, and the tokenizer never skips it (it advances one character when nothing matches), so instrumented on the file above, 92.5% of the 5,666,201 regexexeccalls in one tokenize started at a whitespace position, and only 1.29% of allexeccalls matched at index 0.That fallback was masking a second gap.
TokenType.EndStatementis listed in the'upper'char-class group but was missing from'lower':A lowercase
endonly tokenized correctly when indented, because the all-patterns fallback at the preceding space matchedEndStatement's/^\s*(END)\b/alternative there. At column 0 there is no preceding space, no pattern in'lower'accepted the word, and the single-character fallback advance consumed thee. Adding the'whitespace'group on its own therefore regressed 9 of 241 files (every indented lowercaseend), which is why both changes ship together.The fix
server/src/tokenizer/PatternMatcher.ts:An empty whitespace group is sound: nothing needs to match at a whitespace position.
LineContinuationre-matches at the|itself, and theMODULE(/^\s*MODULE\b/) andTOOLBAR(/^[ \t]*TOOLBAR\b.../) structure patterns re-match at the keyword through their zero-width leading\s*.Measured
type|value|line|startfor every token across 241 source files (14.3 MB, including this repository's own test fixtures): 0 differences.Testing
New
server/src/test/ClarionTokenizer.LowercaseEndCharClass.test.ts(5 tests): column-0 lowercaseendis anEndStatement; column-0ENDand indented lowercaseendstill are;EndStatementis present in both the'upper'and'lower'groups; a'whitespace'group is defined so the lookup can never fall back to every pattern again.Without the fix, 3 of the 5 fail — the column-0 lowercase
endcase with exactlyVariable "nd", plus the two group assertions. With the fix the full suite is 3073 passing, 0 failing, 4 pending.Scope
Only the two char-class table entries. The patterns themselves are unchanged. A separate, smaller opportunity remains and is deliberately not touched here: nearly every token pattern is unanchored and executed against
line.slice(position)with the result discarded unlessmatch.index === 0, so the engine scans to end of line before the result is thrown away; replaying the recorded call sequence with sticky (/y) patterns was 81% faster. After this change the remaining lex cost is small enough that it is not worth the semantic risk in the same PR — going sticky against the full line would activate the currently inert(?<![:\w.])lookbehinds, which the tokenizer re-implements by hand today.