Repository navigation
Description
On main f015f68, Parser.Parse (the plain DFA-lexer route) stops with no_stacks_alive on five real C++ files. In every case the published root has one child, an ERROR that wraps the part of the file the parser reached. That part is 1.2% to 75.6% of the file. Locked C covers 100% of all five. melonds_GPU.cpp is the sharpest case: locked C and the registered token source both parse it with no error, and the plain route stops at 75.6%.
The registered token source (grammars.ParseFile, LangEntry.Parse) reaches the end of all five, but on four of them its tree is also mostly one ERROR. No existing issue covers any of this.
Lane: Legacy fixes (Q item) for a bounded change to the legacy loop or DFA lexer. Core (recover) if the owner decides to turn the C recovery port on for cpp.
Measurements
Main f015f68. Go 1.25.14, linux/amd64 (20 host CPUs, container limit 4 CPUs and 6 GiB), Docker image gotreesitter/cgo-harness:go1.25-local. Locked C: tree-sitter runtime 0.27.1, grammar tree-sitter-cpp at c0092228. cgo_harness at the same commit.
Plain route. gotreesitter.NewParser(lang).Parse(src). Line is 1-based. Column is a 0-based byte offset. "Root end" is the end byte of the returned root. The stop token is the lookahead that had no action. The retry ladder runs 5 or 9 passes; the table shows the pass that got farthest, which is the one published.
| File | Bytes | Stop | Root end (share) | Stop token (symbol, text, bytes) | Line:col | Iterations | Root children |
|---|---|---|---|---|---|---|---|
basic_string.cpp |
160,213 | no_stacks_alive |
1,855 (1.2%) | identifier elif [1857,1861) |
61:1 | 83 | ERROR [0,1855) |
melonds_DSi.cpp |
106,872 | no_stacks_alive |
25,322 (23.7%) | identifier n [25325,25326) |
931:50 | 5,619 | ERROR [0,25322) |
melonds_GPU.cpp |
47,904 | no_stacks_alive |
36,225 (75.6%) | ( [36228,36229) |
1373:41 | 6,296 | ERROR [0,36225) |
stl_tree.cpp |
73,792 | no_stacks_alive |
43,702 (59.2%) | identifier _M_t [43708,43712) |
1636:5 | 6,844 | ERROR [0,43702) |
tsan_interceptors_posix.cpp |
105,604 | no_stacks_alive |
1,721 (1.6%) | identifier define [1723,1729) |
45:1 | 165 | ERROR [0,1721) |
Truncated is true on all five. Three runs in one process gave the same digest each time.
Same files on the registry route and in locked C.
| File | Go registry route | Locked C |
|---|---|---|
basic_string.cpp |
accepted, covers; 4 root children, the first is ERROR [0,160148); 618 ERROR nodes |
covers; root is ERROR; 239 ERROR + 130 MISSING |
melonds_DSi.cpp |
accepted, covers; 10 root children, the first is ERROR [0,91852); 49 ERROR nodes |
covers; root is translation_unit; 1 ERROR + 36 MISSING |
melonds_GPU.cpp |
accepted, covers; 7 root children; 0 ERROR; 12,830 nodes |
covers; 0 ERROR; 12,830 nodes |
stl_tree.cpp |
accepted, covers; 3 root children, the first is ERROR [0,73770); 347 ERROR nodes |
covers; root is ERROR; 133 ERROR + 12 MISSING |
tsan_interceptors_posix.cpp |
accepted, covers; 2 root children, the first is ERROR [0,105588); 71 ERROR nodes |
covers; root is translation_unit; 24 ERROR |
- Locked C covers all five completely and reports errors on four. It reports none on
melonds_GPU.cpp. - "Covers" on the registry route does not mean a useful tree. In four files one
ERRORchild spans 86% to 99.98% of the file. Locked C's tree formelonds_DSi.cpphas one small error. I did not compare any tree node by node. - Locked C parses these files in 8.1 to 29.9 ms (median of 20 parses, each with a new Parser). The plain route takes 1.8 to 332 ms to reach the stop (three parses each).
With the C recovery port forced on (GOT_C_RECOVERY=cpp, plain route), all five end accepted and cover the file. The ERROR counts are 290, 120, 15, 129 and 28 in the table's order. melonds_GPU.cpp then has 15 ERROR nodes where locked C has none. parser_recover_c.go:624 gives that as the reason cpp stays opted out: forced recovery still differs on edited expressions.
With the packed version order off (Language.CompactPackedGSSVersionOrderCertified = false, which removes the C-order action-cell transaction from the path), the five plain-route stops do not change. The root ends are the same: 1,855, 25,322, 36,225, 43,702 and 1,721. So the transaction is not involved here, unlike in the hang draft (finding B).
The shipping Q variant qa-wave-scratch, applied to main through -overlay, changes nothing here: the five stops and root ends are the same.
Where the published plain pass first differs from the registry token stream. The index counts tokens in the pass. The first pass stops at token 82, 1,089, 265, 882 and 164. In basic_string.cpp and tsan_interceptors_posix.cpp every pass stops at that same token. In the other three the published pass is a later retry pass that gets much farther (5,618, 6,294 and 6,840 tokens).
| File | Token index | Plain | Registry | Source |
|---|---|---|---|---|
basic_string.cpp |
70 of 82 | identifier [1668,1675) | keyword default |
line 53, _GLIBCXX_VISIBILITY(default) |
melonds_DSi.cpp |
1,088 of 5,618 | > [5718,5719) |
>> [5718,5720) |
line 260, (mbk[0] >> 8) |
melonds_GPU.cpp |
6,289 of 6,294 | > [36220,36221) |
>> [36220,36222) |
line 1373, (DispStat[cpu] >> 8) | ... |
stl_tree.cpp |
67 of 6,840 | identifier [2743,2750) | keyword default |
line 75, _GLIBCXX_VISIBILITY(default) |
tsan_interceptors_posix.cpp |
156 of 164 | identifier [1657,1659) | #if [1656,1659) |
line 43, #if SANITIZER_FREEBSD || SANITIZER_APPLE |
The first pass of melonds_GPU.cpp is an exception. It sees exactly the registry tokens and still dies at token 265, the ) [3308,3309) of NonStupidBitField<128*1024/VRAMDirtyGranularity>() on line 102. The retry passes get past that line. So for this file part of the difference is in what the parser does with the same tokens, not only in what the lexer returns.
Where the published pass stops, plain route against registry route, token by token:
| File | Plain route | Registry route |
|---|---|---|
tsan_interceptors_posix.cpp, line 45 |
identifier define [1723,1729); no #define token |
#define [1722,1729), preproc_arg, preproc_include_token2 |
basic_string.cpp, line 60 to 61 |
identifier define, identifier L [1854,1855) (the digits 201907 are skipped), identifier elif |
#define [1812,1820), preproc_arg 201907L, #elif [1856,1861) |
melonds_DSi.cpp, line 931 ("%" PRIx64 "\n") |
identifier PRIx64, then identifier n [25325,25326); the " at 25323 and the \ at 25324 are skipped |
" [25323,25324), escape_sequence [25324,25326), " [25326,25327) |
melonds_GPU.cpp, line 1373 ((a >> 8) | ((b & 0x80) << 1)) |
> > as two tokens, ) [36224,36225), then ( [36228,36229); the | at 36226 is skipped |
>> [36220,36222), | [36226,36227) |
stl_tree.cpp, line 1636 (: _M_t(__t)) |
) [43701,43702), then identifier _M_t [43708,43712); the : at 43706 is skipped |
: [43706,43707) |
In all five files the final stop sits where the plain route returns a different token, or none, for text that the registry source lexes as an ordinary token. The parse then has no action, and with the C recovery port off for cpp it has no recovery either. Full token windows for each file are in results/tokens_around_stops.txt. This is my reading of the streams; I did not trace the lexer.
Small inputs. Delta debugging with the predicate "the plain route stops with no_stacks_alive while the registry route accepts and covers the input" shrinks each file to 4 to 9 bytes of text that is not C++:
| From | Bytes | Smallest input found |
|---|---|---|
melonds_GPU.cpp |
4 | [c]| |
melonds_GPU.cpp, registry tree must also be error-free |
7 | i[]>>&; |
tsan_interceptors_posix.cpp |
9 | (v)#f D#i |
basic_string.cpp |
6 | X#g 1L |
stl_tree.cpp |
6 | }d o#f |
melonds_DSi.cpp |
6 | ,""P\n (a backslash and an n) |
They show the plain route stopping on a bare #, | or >>, or on string text next to an identifier. They are not valid C++, so they do not show the failure on valid code.
I also tried 18 hand-made snippets of valid code taken from the stop lines (each inside a function). 17 parse on both routes. One stops the plain route: void f(){ log("a: %" PRIx64 "\n", x); } (38 bytes). It ends no_stacks_alive on the plain route and accepted on the registry route. It is the melonds_DSi.cpp pattern (a macro name between two string literals). The stops in melonds_GPU.cpp, stl_tree.cpp, basic_string.cpp and tsan_interceptors_posix.cpp did not reproduce in isolation, so they need the earlier part of each file.
A possible link to #1558 (cpp hang). The first token differences in basic_string.cpp and stl_tree.cpp are a word the registry source returns as the keyword default and the plain route returns as an identifier. #1558 is a loop that flips a word between its keyword and identifier readings. Both may come from keyword handling in the DFA token source (promoteKeyword). I did not test that.
Reproduction
The inputs, harness and probe sources, and raw outputs are in issue-cpp-no-stacks-alive-f015f6802.tar.gz (SHA-256 in issue-cpp-no-stacks-alive-f015f6802.sha256 next to it). The five full source files are not attached, because they are third-party code under their own licenses; their names, sizes and SHA-256 values are in inputs.sha256. The 38-byte snippet above reproduces the melonds_DSi.cpp stop on its own.
- Use a source copy of main f015f68 as the module
github.com/odvcencio/gotreesitter. - Get the five files (not attached; see above) and put them in
c/inputs/. Check them againstinputs.sha256. - Build the program in
repro/c_five_files/main.go:go build -tags grammar_subset,grammar_subset_cpp -o c_five_files ./c_five_files. - Run
./c_five_files c/inputs/*.cpp. Expected: for each file, theroute=plainline showsstop=no_stacks_alive covers=falsewith the root end and stop token from the first table, and theroute=registryline showsstop=accepted covers=true. - For locked C:
cprobe/zz_oak_probe_test.gois a Go test added to the read-onlycgo_harnessmodule withgo test -c -tags treesitter_c_parity -overlay ov/cprobe.json. Run the test binary fromcgo_harnesswithOAK_LIST=c/c_list.tsv OAK_OUT=out.jsonl. - For the forced-recovery row:
GOT_C_RECOVERY=cpp ./c_five_files c/inputs/*.cppshowsstop=acceptedon the plain route. - For the token streams:
oak probe -lang cpp -file c/inputs/<file>.cpp -route plain -logprints onetoken sym= start= end=line per token. - For the small inputs:
oak ptest -list c/variants2.txt -timeout-ms 2000prints stop reason and time for each snippet on both routes. The 38-byte snippet is the one withno_stacks_alive.
Lane
Legacy fixes (Q item) if the fix is a bounded change to how the legacy loop and DFA lexer handle a token that has no action. The design asks for no language-name test (D6). Counters must stay flat or improve (D10). The cpp ledger row uses a 2 KB sample, so it will not show a change.
Core (recover) if the choice is to enable the C recovery port for cpp. The recorded reason for the opt-out is at parser_recover_c.go:624.
Whether the plain route for cpp is in scope is the owner's decision. docs/api-guide.md says NewParser(lang).Parse always uses the DFA lexer and that the registered source "is not always the better lexer", so both routes are public.
Related issues
I searched issues and pull requests for: no_stacks_alive, basic_string.cpp, stl_tree.cpp, melonds, tsan_interceptors_posix, "cpp truncated", "cpp stop reason", "cpp recovery", "stops early cpp". Nothing covers these files. Closest:
- # field reports from downstream user (Windows GUI toolkit, pure-Go editor widget, multiple syntaxes) on latest release versions #454 (downstream field reports, open). Its C++ root-coverage item concerns a 137 KB generated fixture that was fixed in v0.54.0. These files are real code.
- parser: legacy top-level replay returns false-clean trees outside the C recovery port #1530 and javascript: production reports no error on two inputs where locked C reports one #1519 (false-clean trees from top-level replay). A different cause: those trees have no error where C reports one. These trees stop early.
- v1 Q2: budget retries by first-pass work #1306 (Q2, retry budget). It mentions one
no_stacks_aliveHTML fixture. It does not cover cpp.
What I did not verify
- Why the DFA lexer produces these tokens in these states. I compared token streams and did not trace the lexer.
- Other languages on the plain route.
- The compact route.
- Whether the files stop the same way on release v0.56.0.
- Node-by-node equality between the registry trees and locked C's.
- The licenses of the inputs before upload to the public
evidencerelease.basic_string.cppandstl_tree.cppare libstdc++ headers (GPL-3 with the GCC runtime exception).melonds_*.cppare melonDS (GPL-3).tsan_interceptors_posix.cppis LLVM compiler-rt (Apache-2.0 with LLVM exception). I took this from the file headers and did not record upstream commits. The owner should decide whether to attach them. - Every Go result here comes from an unmodified source copy of main f015f68.
Activity
xidus90 commentedon Nov 12, 2026
Re-check on
main934a39e (Windows amd64, go1.27.0): still reproduces. I did not have the five full files, so I checked the snippet and the minimized inputs.void f(){ log("a: %" PRIx64 "\n", x); }(39 bytes): the plain route stops withno_stacks_aliveand the root ends at 27 of 39 bytes. The registry route isacceptedand covers all 39.[c]|,i[]>>&;,(v)#f D#i,X#g 1L,}d o#f, the,""P+ backslash-n input) still stop withno_stacks_aliveon the plain route. Five of them end short of the input.Parser.Parsedoes not return on a 20-byte input (the action-cell transaction finds no version, then two relex steps undo each other) #1558 loop guard) does not change them either.Effort: the cause sits in the DFA token source (
#define/#if,>>and keyword promotion), and the fix probably reaches into recovery. I estimate well over an hour, so I am not opening a PR. Note thatGOT_C_RECOVERY=cppchanges the picture, as the report says; whether to enable it is an owner decision.xidus90 commentedon Nov 12, 2026
Root-cause findings (plain route,
main0577027, Windows amd64, go1.27.0)I traced the minimized inputs and the current upstream melonDS
src/GPU.cpp(47,975 bytes; line 1373 is the same as in the report). There are at least three separate causes. The C references below come from py-tree-sitter withtree-sitter-language-pack, not from the locked commits.A. The DFA lexer silently drops bytes the current lex mode cannot match
Explains
[c]|,X#g 1L,}d o#f,(v)#f D#iand,""P\n. In these,|,#,1and\are skipped, and the next token starts after them. C runsts_parser__lexagain inERROR_STATEinstead. That C behaviour is already ported (Lexer.nextWithFrontier,errorModeRetry), but it is reachable only when C recovery is on: seesetLexerErrorRunLexStateEnabled(parser_dfa_token_source.go:259) andnextTokenForLexState(:1211).Experiment, with both gates opened for every grammar:
melonds_GPU.cppandmelonds_DSi.cppreach EOF withaccepted.}d o#fand,""P\nthen publish a childless(translation_unit)with no error, while C reports anERROR. That is the class-3 shape from incr: a cpp reuse parse can differ from a fresh parse of the same bytes, on inputs of 2 to 4 bytes (D8) #1557. Applying the prepared parser: legacy top-level replay returns false-clean trees outside the C recovery port #1530 branch does not change this.I am not proposing A until that false-clean root is understood.
B. The
>/>>width election kills every stackcompareAngleTokenPreferencealways prefers a single>over a wider close-angle run. Ini[]>>&;, after], stack 0 (state 3926) wants>>and stack 1 (state 8556) wants>(sym 190), each with score 1.>wins, so stack 0 dies. Stack 1 reduces to state 9092, which has no action for>either, so no stack survives. C lexes each version separately and keeps the>>version.The rule cannot simply go: with it disabled,
TestCppNestedTemplateCloseAngleMatchesC,TestApexGenericLocalDeclarationMatchesCShape,TestTypeScriptGenericLineageRegressionsandTestTypeScriptShiftOperatorNotSplitAsCloseAnglefail.C. Template closer read as an operator (already covered by #1629)
The first pass of
melonds_GPU.cppdies at line 102,NonStupidBitField<128*1024/VRAMDirtyGranularity>(): the closing>is elected as the binary operator (sym 36). This is the shared-lookahead election that #1629 fixes.Combined
melonds_GPU.cppmelonds_DSi.cppno_stacks_aliveat 36,225 (75.5%)no_stacks_aliveat 27,515accepted, 30 ERRORno_stacks_aliveaccepted, 0 ERROR (C: 0)no_stacks_alive(needs A)I did not compare that tree with C node for node.
Next
I will open a PR for B, stacked on #1629. A stack with no action for the elected narrow
>whose own lex mode reads a wider close-angle run with an action gets parked instead of being killed. If every other stack dies on that token, the parked stack continues alone with its own token, which in effect is what C's per-version lexing does. A stays open until the false-clean root is understood.xidus90 commentedon Nov 12, 2026
Cause B: #1632 (stacked on #1629). With both,
melonds_GPU.cppparses to EOF with noERRORon the plain route, as in C. Cause A (silent byte skip) is still open, sobasic_string.cpp,melonds_DSi.cpp,stl_tree.cppandtsan_interceptors_posix.cppstill stop early.fix(parser): revive a stack whose own lex mode read a wider lookahead
xidus90 commentedon Nov 12, 2026
Cause A (silent byte skip): what turning on the C lexer error mode does
Experiment (
main0577027): open both gates so the plain DFA route always uses the portedts_parser__lexfailure handling, i.e. anERROR_STATEretry and then an error-run token:setLexerErrorRunLexStateEnabledatparser_dfa_token_source.go:259nextTokenForLexStateat:1211Results, together with #1635:
no_stacks_alive, stops earlyaccepted, covers,HasErrormelonds_GPU.cppmelonds_DSi.cpp#1635 is needed first. Without it,
}d o#fand,""P\npublish a clean, childless root (trailing-EOF recovery drops its ERROR extras).Why I am not opening a PR for A. As a change for every grammar, it moves pinned gates:
perfcounterledger:doxygen candidate declinechanges reason, fromaccepted-leaf-tiling-gaptocompact route error: parser-core fresh-full runner did not accept EOF. Both decline; no work counter moves. This would need R3.TestDoxygenDispatchBlockerRoutes: 4 pinned raw digests change.TestCompactRouteAcceptedRootLeafCoverageGapDeclines: html<!--c-->>and friends are routed instead of declined.TestRetryStopReasonSwallowedErrorFallbackRejected: its fallback is now adopted.grammars/TestHaskellUnterminatedStringUsesInternalGapFallback:(haskell (ERROR (ERROR)))instead of(haskell (ERROR)).TestSetLexerErrorRunLexStateUsesCRecoveryGatepins the current gating on purpose.Owner decision needed:
Until then, the remaining four files (
basic_string.cpp,melonds_DSi.cpp,stl_tree.cpp,tsan_interceptors_posix.cpp) still stop early on the plain route.fix(result): report error extras left beside the result root
fix(parser): revive a stack whose own lex mode read a wider lookahead