Repository navigation
Conversation
xidus90
force-pushed
the
fix/glr-close-angle-width-fallback
branch
from
November 12, 2026 09:33
599d522 to
7c1eadd
Compare
…n lex mode In C++ 'A<B<T> >' and 'A<B<T>>', the shared GLR token election picks the binary '>' over the template-closing '>' for the stacks that hold B<T> as a template type. They accept both symbols, so the per-stack no-action re-lex never runs and the outer template is read as a comparison: aliases and declarations misparse, and inside a class template the parse runs out of stacks. C lexes each version with its own state's lex mode before it reduces. With several live stacks, each stack now re-reads the shared token once with its head state's lex mode and takes a same-span, action-bearing, non-keyword reading of an internal-DFA token. The DFA probe is shared with relexTokenForStackLexState.
The per-stack own-lex-mode reading also swapped tokens that a grammar tells apart by lex state alone. In the committed earthfile sample it fired hundreds of times (plain vs immediate string words, two `.` and `-` tokens), and the sample, which C and main parse cleanly, ended with an ERROR over its last 375 bytes; the counter ledger flagged new_nodes 4399 -> 6844. Take the reading only where the stacks disagreed about how to spell the same bytes: the union election scanned a same-named rival of the shared token in some live state's lex mode, and the reading has that name too. This keeps the C++ `A<B<T>>` fix and restores the earthfile sample.
…ition The per-stack re-read lexed the shared token again from its own start byte, after the whitespace the shared lexer had skipped. C lexes each version from its own position with its own lex mode, and lex modes read bytes after whitespace differently from bytes right after the previous token. Read from the token's start, Julia's ':' in 'c ? a : b', F#'s '<' and '+' and Earthfile's immediate string fragments got readings C never makes there. The same-named-rival limit already keeps those fixtures clean; this makes the reading itself match C wherever the limit lets it through. The re-read now starts at the stack's byte offset, lets the mode skip the gap itself (restarting after whole-token skips, without immediate tokens after a restart), and leaves modes with an after-whitespace start state or gaps over included ranges alone. The no-action re-lex keeps its start.
…he C token source On the registered C++ route (grammars.ParseFile, LangEntry.Parse) the C token source read 'A<B<T>>' with one '>>' token, because the comparison version accepts it. C lexes each version in its own mode: the template versions read the first '>' alone, since the template closer outranks '>>'. One shared token cannot be both, so the token source now emits the single '>' when a live state's own lex mode reads one byte there, as the DFA route's election already does. 'A<B<T>> x;' and the class-template aliases now parse like C on both routes.
…split against locked C
C keeps a version's lookahead through its reductions and looks the same symbol up in each new state (ts_parser__advance, parser.c:1720-1745). The per-stack reading lasted one dispatch: stackRelexRestoreTok put the shared token back, and after a single reduce the stack read again in its post-reduction state. A stack that reduced on its own reading could then shift the shared one. A stack that dispatched on its own reading and reduced in place now goes on dispatching with that reading until it shifts. TestOwnTokenIsKeptThroughAReduction is the reviewer's table: Q reduces on its own x and lands where the two readings shift to different states. The root was rule_xa and is now rule_xb, as C's rule gives.
C lexes a version again only when its cached token cannot be reused in the version's state (ts_parser__can_reuse_first_leaf, parser.c:470-503): the version's lex mode differs from the one the token was lexed in, and the entry is not reusable or the state has an external lex state. ownLexModeToken now applies that rule before it scans, through the existing transcription incr.FirstLeaf. The state the shared token was lexed in is the election's winner: nextGLRUnionDFAToken moves the elected scan to the front of its scan cache, and electedScan reads it from there. Every other reader of the cache looks scans up by state. electionHadSameNameRival also returns false before it walks the cache when the token did not win the last election. GLR dispatch asks it once per stack. On one W5 go one-byte edit pair (src/debug/elf/elf.go) that is 93,450 calls, none of which found a rival; each is now rejected in O(1). On the committed fixtures the rule skips 4 of 6 readings in cpp, 2 of 6 in earthfile and 45 of 68 in less. Trees of all 204 fixtures, on the DFA and token-source routes, are unchanged against main, and the generated counter ledger equals main's in all 412 rows.
…reading The own reading covers the shared token's bytes after the same gap, but it did not carry the shared token's skipped-prefix flag and start. A read that skips the gap inside one DFA run sets them itself. A read that restarts after a whole skip token, as Lexer.next does, has no record of the gap, so the new start position does not make the point moot. The reading now takes the flag and start from the shared token it replaces. TestOwnLexModeTokenReadsFromStackPosition checks them on every accepted case; "skip token, then a reading" failed before.
The shared GLR token election prefers a single `>` over `>>`, which nested template closers need. In C++ `(d[c] >> 8)` that kills the stack reading `>>` at once, and the stack that took `>` reduces into a state with no action for it, so the parse stops with no_stacks_alive. C lexes each version with its own lex mode and keeps the `>>` version. At dispatch start, record a stack's own-lex-mode reading when it starts at the shared token's byte but ends later and the stack's state has an action for it. If every stack then dies on the shared token, revive the stacks still at their recorded state and depth with that reading, and move the lexer to its end. The election and the lockstep loop are unchanged otherwise. Refs odvcencio#1559
xidus90
force-pushed
the
fix/glr-close-angle-width-fallback
branch
from
November 12, 2026 14:19
7c1eadd to
d5dffe9
Compare
This branch has not been deployed
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.
Fixes #1636. Refs #1559. Stacked on #1629: this branch is #1629's head
a2030de(rebased onmain0f3af76) plus one commit of its own,d5dffe9. Please review and merge #1629 first. The diff of this PR on its own isgit diff a2030de..d5dffe9.What does this PR do?
On the plain DFA route,
melonds_GPU.cppfrom #1559 now parses to EOF with noERROR, like C. On main it stops withno_stacks_aliveat 75.5%. It also fixes the minimal valid caseint f(){ v = (d[c] >> 8) | ((d[c] & 0x80) << 1); }.Why this approach?
Root cause
This is cause B from the findings on #1559. After
d[c], two stacks are live:>>.>(sym 190).compareAngleTokenPreferenceelects the single>, so the>>stack dies at once. The>stack reduces into state 9092, which has no action for>, so no stack is left. C lexes each version with its own lex mode and keeps the>>version.The angle rule cannot go: nested template closers need it. With it disabled,
TestCppNestedTemplateCloseAngleMatchesC,TestApexGenericLocalDeclarationMatchesCShape,TestTypeScriptGenericLineageRegressionsandTestTypeScriptShiftOperatorNotSplitAsCloseAnglefail. Nor can the election tell the two cases apart: it sees states, not stacks, and whether the>survives depends on the reduce chain.Fix
The fix leaves the election and the lockstep loop alone and adds a fallback for the case where the shared token kills every stack.
ownLexModeToken(from fix(parser): let each GLR stack read the shared lookahead with its own lex mode #1629) already lexes the shared token's bytes with the stack's own lex mode. It now also reports a reading that starts at the same byte and ends later, if the stack's state has an action for it. The parser records that reading with the stack's state and depth, before the stack reduces.reviveWiderStackReadingrevives the stacks that are still at their recorded state and depth with the first such reading. It returns that reading as the lookahead, and the parser moves the DFA lexer to the reading's end.tryRelexCurrentStateDFArepositions the lexer the same way.The change has no language-name checks (D6). It does not run with included ranges.
Cost against #1629. #1629 returns before its probe when the election scanned no same-named rival (
electionHadSameNameRival). This PR moves that check after the probe, where it gates only the same-span swap, because the wider reading must be seen without a rival. With the check left in front,TestCppShiftAfterSubscriptSurvivesCloseAngleElectionfails again (no_stacks_aliveat byte 24). So on multi-stack dispatches without a rival, this PR runs the probe where #1629 does not. #1629'selectedTokenReusableshortcut, thets_parser__can_reuse_first_leafrule, still runs first. I have not measured the cost; CI'seditor_latencywill show it.Correctness
First measured on
main0577027 + #1629 (cd88a46). Re-run after the restack onmain0f3af76 + #1629 (a2030de): the new test fails ona2030de(no_stacks_aliveat byte 24) and passes with this PR,TestReviveWiderStackReadingpasses,melonds_GPU.cppisacceptedwith 0 ERROR,grammarspasses, the root package fails only the timing-based tests that also fail onmain, and the ledger,v1guardandv1layout -base origin/mainpass. Windows amd64, go1.27.0.grammars/TestCppShiftAfterSubscriptSurvivesCloseAngleElection: full S-expressions forint f(){ v = (d[c] >> 8) | ((d[c] & 0x80) << 1); }andx = i[] >> 8;, plus the empty input, with a 2 s timeout.no_stacks_aliveat byte 24 of 50.tree-sitter-language-pack, minus field names. These are not the locked commits.TestReviveWiderStackReadinginglr_test.go. It covers: a live stack blocks revival, a stale stack index, a stack that moved on, two different readings, and the same reading on two stacks.reviveWiderStackReadinghas 100% statement coverage.ownLexModeTokenhas 90.9% through the grammar tests; the uncovered line is the early return when no DFA token source is present.go test -count=1 . ./grammars:grammarspasses.TestMalformedTokenInvariantGoLeafDoesNotRunBaseMergeRetryfailed 1 of 8 runs on main, 3 of 8 on fix(parser): let each GLR stack read the shared lookahead with its own lex mode #1629 and 0 of 8 with this PR. The Awk/CMake/C# incremental tests are the timing-based ones described in fix(parser): stop no-action re-lex steps from undoing each other #1630.go run ./cmd/v1guardandgo run ./cmd/v1layout -base origin/mainpass.melonds_GPU.cpp(upstream, 47,975 B)no_stacks_aliveat 36,225accepted, 0 ERROR(d[c] >> 8) | …snippetno_stacks_aliveat 24 / 50accepted, matches Ci[]>>&;(invalid)no_stacks_alive, erroraccepted, no errori[]>>&;: after the revive, the parse ends inLEGAL-REPLAY-RESYNC, which drops>> &without anERROR. That is the false-clean replay from parser: legacy top-level replay returns false-clean trees outside the C recovery port #1530. With the prepared parser: legacy top-level replay returns false-clean trees outside the C recovery port #1530 change applied on top, the tree is(translation_unit (ERROR (expression (subscript_expression …))) (expression_statement)), as in C. I did not include parser: legacy top-level replay returns false-clean trees outside the C recovery port #1530 here.cgo_harnessparity suites. Please run the cpp, apex and typescript suites in CI.Counters
go run ./cmd/perfcounterledger:counter ledger passed: 412 rows, 206 languages. No row moves past the 2 percent threshold against main (ond5dffe9).Performance
scripts/run_randomized_benchmarks.sh. On the hot path, the change appends to a slice only when a stack's own reading is wider. The revive check runs at iteration start only when that slice is non-empty.Self-review
Commits are signed.