Repository navigation
Conversation
f62410a to
e49472a
Compare
Regression: earthfile real sample goes from clean to
|
| main | this PR | C (py-tree-sitter, tree-sitter-language-pack) |
|
|---|---|---|---|
HasError() |
false | true | false |
| tokens | 492 | 478 | |
new_nodes |
4,399 | 6,844 |
With this PR, one ERROR covers bytes 1518–1893 (the smoke-test: target from line 56 to the end).
Where it goes wrong
The shared token streams first differ at byte 660. On main, the parser gets block_token1 (indent) and then the keyword COPY. With this PR, the indent is gone and COPY arrives as _string_base_alpha. That follows from earlier OWN-LEX substitutions. The first is at byte 59, WORKDIR (sym 45 → _immediate_string_base_alpha 133) on a stack in state 1619, which continues the previous line's string.
Over the whole file, OWN-LEX fires hundreds of times. Earthfile tells many tokens apart by lex state alone: _string_base_alpha/_immediate_string_base_alpha (130→133), . (60→49), - (61→50), [ (73→139), and others. Swapping them per stack while the rest of the loop stays in lockstep changes which versions survive, and the parse ends on an error path that C does not take.
What I tried (none is enough on its own)
- Only take a reading with the same symbol name as the shared token, which is what the C++
>/>case needs. This removesWORKDIR→string, but.→.and-→-remain, and the sample still has an error. - Never take an immediate token after skipped whitespace. This has no effect:
Language.ImmediateTokensis false for all of these earthfile symbols. - Probe from where the shared lexer started, before the skipped whitespace, and require the same span. This is C-faithful, because C lexes each version from the previous token's end. It removes the whitespace cases (
./after a space), but most substitutions have no skipped prefix, and the sample still has an error.
All three kept TestCppNestedTemplateCloseAngleMatchesC, the Apex test and the two TypeScript angle tests green.
Options
- Narrow the rule to what the PR targets: apply the own-lex-mode reading only when the shared election was decided by the same-name tie-break (
compareSpecificTokenPreference) between two candidates the stacks disagree on. This is closer to the "alternative considered" in the description. - Keep the per-stack reading but gate it on proof that the grammar's lex states agree on everything except the disputed symbol. That is harder, and I have not designed it.
This PR should not merge until one of these is settled. #1632, which is stacked on this PR, does not add a ledger change of its own, but it inherits this one.
Regression test to add with the fix (fails on this PR, passes on main):
func TestOwnLexModeReadingKeepsEarthfileSampleClean(t *testing.T) {
src, err := os.ReadFile(filepath.Join("..", "internal", "benchfixtures", "testdata", "real", "earthfile"))
if err != nil {
t.Fatalf("read earthfile sample: %v", err)
}
parser := ts.NewParser(DetectLanguageByName("earthfile").Language())
parser.SetTimeoutMicros(10_000_000)
tree, err := parser.Parse(src)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
if root := tree.RootNode(); root.HasError() || root.EndByte() != uint32(len(src)) {
t.Fatalf("has_error=%v end=%d/%d", root.HasError(), root.EndByte(), len(src))
}
}|
Fixed in cd88a46: the own-lex-mode reading now applies only when the union election scanned a same-named rival of the shared token (C++ |
|
Thank you for this work, and for finding the earthfile regression yourself before I got to it. The diagnosis is right: C lexes once per stack version, and one shared lookahead for all stacks is where this engine goes wrong. I checked the PR by running it. The runs below used your first revision ( What I ran (main
No input is right on main and wrong with the fix. The four that stay different are std::vector<std::shared_ptr<Foo>> v;
std::map<std::string, std::vector<Foo>> m;
std::vector<std::vector<Foo>> g();
auto p = std::make_shared<std::vector<Foo>>();For the first one the engine still gives a For reference: One thing to change: keep the stack's own token until it is shifted. C keeps the lookahead through reductions: after a reduction it looks the same symbol up in the new state ( Test (root package)func keptTokenLanguage() *Language {
return &Language{
Name: "kept_token", SymbolCount: 7, TokenCount: 4, StateCount: 9, ProductionIDCount: 1,
SymbolNames: []string{"end", "a", "x", "x", "n", "rule_xa", "rule_xb"},
SymbolMetadata: []SymbolMetadata{
{Name: "end"}, {Name: "a", Visible: true}, {Name: "x", Visible: true}, {Name: "x"},
{Name: "n", Visible: true, Named: true}, {Name: "rule_xa", Visible: true, Named: true}, {Name: "rule_xb", Visible: true, Named: true},
},
LexStates: []LexState{
{Default: -1, EOF: -1, Transitions: []LexTransition{{Lo: 'a', Hi: 'a', NextState: 2}, {Lo: 'x', Hi: 'x', NextState: 3}}},
{Default: -1, EOF: -1, Transitions: []LexTransition{{Lo: 'x', Hi: 'x', NextState: 4}}},
{AcceptToken: 1, Default: -1, EOF: -1},
{AcceptToken: 2, Default: -1, EOF: -1},
{AcceptToken: 3, Default: -1, EOF: -1},
},
LexModes: []LexMode{{LexState: 0}, {LexState: 1}, {LexState: 0}, {LexState: 0}, {LexState: 0}, {LexState: 0}, {LexState: 0}, {LexState: 0}, {LexState: 0}},
ParseActions: []ParseActionEntry{
{},
{Actions: []ParseAction{{Type: ParseActionShift, State: 1}, {Type: ParseActionShift, State: 2}}}, // 1: `a` forks into Q and P
{Actions: []ParseAction{{Type: ParseActionShift, State: 8}}}, // 2: P on the shared reading
{Actions: []ParseAction{{Type: ParseActionReduce, Symbol: 4, ChildCount: 1}}}, // 3: Q reduces on its own reading
{Actions: []ParseAction{{Type: ParseActionShift, State: 4}}}, // 4: T on the shared reading
{Actions: []ParseAction{{Type: ParseActionShift, State: 5}}}, // 5: T on Q's reading
{Actions: []ParseAction{{Type: ParseActionReduce, Symbol: 5, ChildCount: 2}}}, // 6
{Actions: []ParseAction{{Type: ParseActionReduce, Symbol: 6, ChildCount: 2}}}, // 7
{Actions: []ParseAction{{Type: ParseActionShift, State: 3}}}, // 8: goto n -> T
{Actions: []ParseAction{{Type: ParseActionShift, State: 6}}}, // 9: goto rule_xa
{Actions: []ParseAction{{Type: ParseActionShift, State: 7}}}, // 10: goto rule_xb
{Actions: []ParseAction{{Type: ParseActionAccept}}}, // 11
},
ParseTable: [][]uint16{
{0, 1, 0, 0, 8, 9, 10},
{0, 0, 0, 3, 0, 0, 0}, // Q (lex mode 1)
{0, 0, 2, 0, 0, 0, 0}, // P
{0, 0, 4, 5, 0, 0, 0}, // T
{6, 0, 0, 0, 0, 0, 0},
{7, 0, 0, 0, 0, 0, 0},
{11, 0, 0, 0, 0, 0, 0},
{11, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0},
},
}
}
func TestOwnTokenIsKeptThroughAReduction(t *testing.T) {
lang := keptTokenLanguage()
p := NewParser(lang)
source := []byte("ax")
ts := newDFATokenSourceDirect(NewLexer(lang.LexStates, source), lang, p.lookupActionIndex, nil, nil, nil)
tree := p.parseInternal(source, ts, nil, nil, arenaClassFull, nil, 0, 0, 0, false)
if tree == nil {
t.Fatal("nil tree")
}
defer tree.Release()
if got := tree.RootNode().Type(lang); got != "rule_xb" {
t.Fatalf("root = %s, want rule_xb: Q reduced on its own reading and must shift it too", got)
}
}A pointer that may help with the cost. C decides in Smaller. The replacement token does not carry the shared token's skipped-prefix flag. I counted no stack killed because of that on the samples of 204 languages, and your new start position may make the point moot. A maintainer has to approve the workflow runs of each push, because this is your first contribution here. I approved |
…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.
b5d3544 to
a2030de
Compare
|
Thank you for the careful review and for the test. I rebased onto What ran this round. Docker parity, the invariant gate and the Docker benchmarks were not re-run on this head. Everything below ran on the host (Windows, Go 1.27, anchored 1. Keep the stack's own token until it is shifted (
2.
3. Cost (
4. Skipped-prefix flag (
Other host checks on this head
From my side, the head is ready for points 1, 3 and 4 and for your full run. Point 2 waits on your call about the bit. |
Fixes #1628.
What does this PR do?
In C++
A<B<T>>/A<B<T> >, the GLR stacks that holdB<T>as a template type now read the closing>as the template closer instead of the binary operator. Aliases, declarations and class bodies then parse like the C runtime, on two routes:Parser.Parse);grammars.ParseFile,LangEntry.Parse, the cgo harness).Commits, rebased onto
main0f3af76 (#1631). The earlier hashes are in parentheses.d5654112(e49472a5) fix(parser): let each GLR stack read the shared lookahead with its own lex modec149ff13(9a38de8a) test(parser): cover the per-stack lookahead re-read331e9239(cd88a469) fix(parser): limit the own-lex-mode reading to same-named rivalsaedcefc0(9c130f9a) fix(parser): start the per-stack lookahead re-read at the stack's position92ca9413(d239be39) fix(grammars): split a close-angle run for C++ template versions in the C token sourcedc9ad6c1(b5d35445) test(cgo_harness): pin the per-stack re-read and the C++ close-angle split against locked Cdfccea66fix(parser): keep a stack's own reading through its reductions301e62dfperf(parser): read no own token where C would reuse the shared onea2030debfix(parser): carry the shared token's skipped-prefix record onto the readingCommits 7 to 9 answer the review of the first revision. They are described under "Changes after review" below.
Why this approach?
Root cause
tree-sitter-cpp has two tokens spelled
>:token(prec(1, '>'))(sym 190).After
A<B<T>is closed by its first>, three GLR stacks are live:B<T>as a template type insideA<…>. Their lex mode reads the next>as the closer (190).B < Tcomparison fork. Its lex mode reads>as the operator (36).C lexes each version with its own state's lex mode, from that version's position, before reducing (
ts_parser__advance,ts_parser__lex). So the template versions get 190 and closeA<…>.gotreesitter lexes one shared token per iteration.
nextGLRUnionDFATokenscores 190 at 2 and 36 at 1. It still elects 36, becausecompareSpecificTokenPreference, a tie-break between same-named symbols, is checked beforescore. The template stacks then reduceB<T>and land in a state that has an action for both>symbols. They take the operator, soA < B<T> > …becomes a comparison.The existing per-stack re-lex (
relexTokenForStackLexState) cannot help:The registered C++ token source has a second, separate problem with the unspaced
A<B<T>>. It emits>>as one token, because the comparison version accepts>>. C's template versions read the first>alone: the template closer has lexical precedence 1, so their lex mode stops after one byte. One shared>>token consumes the byte that the template versions need to closeA<…>.Fix
Per-stack re-read (parser,
e49472a5,cd88a469,9c130f9a). When more than one stack is live, each stack re-reads the shared lookahead once with its own head state's lex mode, before it dispatches. This is the state C lexes in. A stack takes the reading only when all of the following hold:electionHadSameNameRival), and the reading has that name too (cd88a469). This limits the re-read to elections where the stacks disagreed about how to spell the same bytes, such as the two>. Grammars that tell many tokens apart by lex state alone are left alone, for example earthfile's plain and immediate string words.The read starts where C starts this version (
9c130f9a): at the stack's byte offset, before any whitespace the shared lexer skipped, so the lex mode has to skip that whitespace itself. Lex modes read bytes after whitespace differently from bytes that directly follow the previous token. After a whole-token skip, the read restarts in the same lex state, asLexer.nextdoes, and an immediate token after such a restart is not a reading. The probe leaves alone two cases it does not model: a lex mode with its own after-whitespace start state, and a gap across included ranges.The reading is scoped to its stack. The shared token is restored before the next stack, through the existing
stackRelexActive/stackRelexRestoreTokmechanism. The DFA probe ofrelexTokenForStackLexStatemoved intoscanTokenInStackLexState, and both paths share it. The no-action path keeps its start position (tok.StartByte) and its behaviour, including the zero-width external rescue.Close-angle split (C token source,
grammars/runtime/c_lexer.go,d239be39). When a close-angle run such as>>matches, the token source asks each live GLR state's own lex mode, using the grammar's DFA. If one of them reads a one-byte token there, it emits the single>. This is the choice the DFA route's election already makes (compareAngleTokenPreference). The choice between 36 and 190 then follows the token source's existing preference and the no-action re-lex.No language-name checks in engine files (D6). The probe reuses the parser-owned
relexProbeLexer.How the two limits on the re-read relate
The first revision (
e49472a5) read every token again from its own start byte, for every stack. In real fixtures, stacks then took readings right after a space that C never makes:":"(51) →":"(35)? 0x94cb2bb20a28ce96 : 0x1f60a087_string_base_alpha→_immediate_string_base_alpha,"+"(73) →"+"(139)RUN sbt compile,FROM +depsinfix_op_token2→"<","+"(138) →infix_op_token1i < span.Length,i <- i + 1At every such site, reading from the stack's position gives back the shared symbol, or no token. So the read position is the root cause.
The same-named-rival limit (
cd88a469) independently stops all of them: no election scanned a same-named rival at those spans. Withcd88a469alone, the julia, earthfile and fsharp fixtures and the C++ cases already behave like the full branch, and the start-position commit changes no fixture or counter on top of it.9c130f9astays because it makes the reading itself match C wherever the rival limit lets it through. Without it, a stack in such an election would still be read from after the whitespace.The C++ readings come out as 190 under both limits.
Alternative considered: put
scoreahead ofspecificPreferenceinnextGLRUnionDFAToken. This would also fix the DFA repro, but it changes the shared election for every grammar and every stack, and it still gives the minority stack the wrong symbol.Changes after review
dfccea66).ts_parser__advance,parser.c:1720-1745).stackRelexRestoreTokput the shared token back, and the stack read again in its post-reduction state.goto retryAction) until it shifts.TestOwnTokenIsKeptThroughAReductionis the reviewer'skeptTokenLanguagetable, unchanged. It gaverule_xabefore this commit and givesrule_xbwith it.301e62df).ownLexModeTokenappliests_parser__can_reuse_first_leaf(parser.c:470-503) before it scans. It uses the existing transcriptionincr.FirstLeaf. The state the shared token was lexed in is the election's winner.nextGLRUnionDFATokenmoves the elected scan to the front of its scan cache, andelectedScanreads it from there. Every other reader of that cache looks scans up by state.dfaTokenSourceandglrStackkeep their sizes.electionHadSameNameRivalreturns false in O(1) when the token did not win the last election.TestOwnLexModeTokenSkipsAReusableSharedTokencovers four cases: a token lexed in another mode, one lexed in the stack's own mode, a reusable entry, and a reusable entry in a state with an external lex state.TestElectionHadSameNameRivalgains "token did not win the last election". Both new rejections failed before the commit.a2030deb).TestOwnLexModeTokenReadsFromStackPositionnow checks the flag and its start on every accepted case. "skip token, then a reading" failed before the commit.Correctness
Host checks of this revision (Docker not re-run)
The parity suites, the corpus and fixture comparisons against the pinned C parser, the invariant gate and the benchmarks were not re-run in Docker for this revision. The tables further down are from the earlier head
b5d35445. On this head, all checks ran on the host: Windows, Go 1.27, anchored-run.As the oracle I used
internal/ccore, the port of the C core, driven throughbindtest. A scratch test that is not committed registers cpp's scanner for the comparison. The oracle reproduces the earlier Docker numbers:TestOwnTokenIsKeptThroughAReduction,TestOwnLexMode*,TestRelexTokenForStackLexStateNoAction,TestElectionHadSameNameRival,TestGLRUnion*, size budgets)go test ./grammars -run 'Cpp|OwnLexMode'c0092228, 198 blocks incl.test/corpus/c, vs ccore, DFA / token sourceinternal/benchfixtures/testdata/real), DFA and token source, vs maingo run ./cmd/perfcounterledgergo vet .,v1guard -base origin/main,v1layout -base origin/mainThe 18 inputs:
class A { H<I<J>> method() { typename K::template L<M<N>>::type …; typename K::template L<M<N>>::template O<P>::template Q<R>::type …; } };;std::lines from the review.The first revision (
d5654112, rebased) gives 12 / 6 on this set. No input that it got right is lost. The reviewer counted 14 for that revision on their set, so the two sets or routes differ.About the corpus count: my block extraction finds 198 blocks, where the reviewer counted 202. It also normalizes CRLF, so
crlf.txttests nothing in this comparison. On the DFA route, main differs from the oracle in 8 blocks and this head in 3, which matches the reviewer's 194 → 199.The julia, earthfile, fsharp and pascal fixtures have unchanged trees against main on this head. They were not compared with C again.
Tests
grammars/TestCppNestedTemplateCloseAngleMatchesCchecksusing X = A<B<T> >;,A<B<T>> x;and a class template withtypename A<B<T>>::typeandA<B<T> >::typealiases.LangEntry.Parse).d239be39, the token-source route failed for 2 of 3 inputs.grammars/TestOwnLexModeReadingKeepsEarthfileSampleClean(cd88a469): the earthfile fixture parses without error.grammars/TestOwnLexModeReadAfterWhitespaceMatchesCchecks two things:h = (UInt === UInt64) ? 0x94cb2bb20a28ce96 : 0x1f60a087, as an exact tree;It fails on
9a38de8aand passes fromcd88a469on.Unit tests (
parser_relex_state_test.go):TestOwnLexModeTokenGuardsgainscd88a469's cases for a rival under another name and for no rival at the span.TestOwnLexModeTokenReadsFromStackPositioncovers seven cases:parser_dfa_token_source_test.gocoverselectionHadSameNameRival.cgo_harness/TestOwnLexModeReadMatchesLockedCandTestOwnLexModeFixturesKeepLockedCRootcompare against the pinned C parser:Results in Docker:
9a38de8a's engine and token source (go -overlay): 8 subtests fail. Those are julia on both routes, three of the four C++ inputs on the token-source route, and all three fixtures.Docker validation of the earlier head
b5d35445(cgo_harness, pinned C)Not re-run for this revision.
Environment:
--memory 3500mand--gomemlimit 3GiB.gotreesitter/cgo-harness:go1.25-local, go1.25.14 linux/amd64.c0092228.origin/main934a39e (add(lexexact): add exact-lexer records for 195 grammars #1624).GTS_PARITY_COMPARE_HAS_ERROR=1, and baseline and branch ran the same probe program.TestParity{FreshParse,IncrementalParse,HasNoErrors,Highlight,GLRCanarySet}: cpp, c, c_sharp, typescript, tsx, javascript, rust, javaTestParityGLRCapPressureTopLanguages: c, cpp, java (exhaustive)TestParityCorpusFreshParse(exhaustive; docs only for c, java)Why cpp drops from 17 to 14 in the grammargen real corpus: the test admits only samples whose reference parse (gotreesitter with the shipped blob) is clean, and it stops after 25. This branch repairs the reference parse of three
reflection.txtblocks, which C parses without errors. Those blocks become eligible and expose generated-grammar gaps that also exist on baseline, and they push out three exact matches. This is a sample-set artefact, not a regression.Invariant gate on the earlier head
b5d35445(scripts/run_v1_invariant_sessions.sh)Not re-run for this revision: the script requires Docker. On this head, the host counter ledger and the fixture-tree comparison above stand in for it.
206 languages × 2 routes, 72 edit steps each:
passed=trueandsource_unchanged=true;counters.json) are byte-identical to baseline (sha2567b46fd40…, 0 differing fields). The first revision had 40 differing fields in fsharp, julia, earthfile and pascal.Broad host tests on the earlier head
b5d35445(Windows, Go 1.27)go test ./grammars/...passes exceptgrammars/lean TestPackagedBlobMatchesNativeGrammar, a blob SHA-256 mismatch that does not involve the parser. I did not check it against baseline.go test .has 8 failures. All of them are among the 14 that fail on v0.57.0 in the same checkout: incremental leaf-reuse and fast-rescue tests.Performance
editor_latency (go), this head, host replica
CI's
editor_latency (go)failed on9a38de8with +6.6 to +7.7 % per edit. The gate itself needs Docker, cgo and the C oracle, so I could not run it on the host. Instead I used a scratch replica of its Go backend (GoA/GoB):golang/go@d00c67fsrc/debug/elf/elf.go, sha256ff2f811d…;cgo_harness/editor_latency;These are host numbers, not CI's.
Deterministic counts:
ownLexModeToken93,450 times.ownLexModeTokennorelectionHadSameNameRivalhas a sample.Timing, benchstat:
B/op and allocs/op are unchanged. Host noise is about 5 to 10 %, so these runs cannot resolve a change of a few percent, and I do not call the cost fixed. CI's
editor_latency (go)on this head decides. Oncd88a46that job was still running when I wrote this.Earlier head
b5d35445(Docker, not re-run)Measured with
scripts/run_randomized_benchmarks.shagainst baseline 934a39e:-benchtime=750ms,-benchmem;golang.org/x/perf@v0.0.0-20261009192801-be2c69fb417e.No significant change.
These benchmarks run the DFA route only and do not reach the C token source. So I also measured allocations per fresh parse through
LangEntry.Parse("cpp"): 50 repetitions of each line,testing.AllocsPerRun, Windows host.int f(int a) { return a >> 2; }void g() { x = a >> b; y = c<d<e>>(1); }std::vector<std::vector<int>> v;>>.>>,liveLexModeReadsOneBytebuilds oneLexerper live state.std::vectorline: the correct template parse forks more than the old error parse did. The token-source route used to misparse it.Remaining gaps
The
std::-qualified inputs: still open, being worked onThe root cause is found. The fix needs an R3 decision.
Six of the 18 inputs still differ from C on this head:
std::vector<std::shared_ptr<Foo>> v;std::map<std::string, std::vector<Foo>> m;std::vector<std::vector<Foo>> g();auto p = std::make_shared<std::vector<Foo>>();B<C::D<E, F>>::G field;K::L<M<N>> v = K::L<M<N>>{};This is not a lexing problem. I compared the GLR trace with ccore's C-format parse log for
std::vector<std::shared_ptr<Foo>> v;:std::vector<std::, C has two versions in state 8983 at the same byte, the comparison reading and the template reading.ts_stack_can_mergecompares only status, state, position, error cost and external scanner state.shared_ptrC reduces_scope_resolutionover both links, which leaves two versions, 3596 and 7921. The template reading survives and later closesstd::shared_ptr<Foo>.gssMergeScoresRefuse). cpp's one-version-per-merge-key policy then keeps only one history.The existing per-blob bit
LegacyMergeAdmitDynamicPrecedenceadmits C's merge. elixir already sets it. With the bit on cpp's exact profile, on top of this head:The bit also raises the counter ledger's
cppdefaultnew_nodesfrom 1171 to 1379 (+17.8 %). That is an R3 exception, which I cannot approve. So the bit and its tests sit on a separate local commit (3dfee8bd, not pushed), waiting for the maintainer's decision. The six newTestOwnLexModeReadMatchesLockedCrows on that commit have not been compiled or run: there is no cgo on the host. On main, the bit alone fixes one more of the 18 inputs, on the DFA route only. Both changes are needed.Also
Checklist
b5d35445, not re-run for this head. On this head the oracle was ccore on the host.b5d35445, not re-run for this head (Docker only)