tree-sitter / tree-sitter/tree-sitter-cpp
C++: a 16-character raw-string delimiter (the standard maximum) fails to parse — capacity guard is checked before the '(' terminator
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 453
- Forks
- 185
- PR merge metrics
- No merged PRs in 30d
Description
Summary
A raw-string delimiter of exactly 16 characters — the maximum [lex.string] allows — fails to parse. The whole translation unit after it collapses into ERROR nodes. 15 characters is fine, so the one length that is legal-but-rejected is exactly the spec's limit.
The cause is a check ordering in scan_raw_string_delimiter, and the fix is a one-line reorder. I have built and tested it; details and a differential sweep below.
Repro
// compiles cleanly with `g++ -std=c++17 -fsyntax-only`
const char* kTemplate = R"FFFFFFFFFFFFFFFF(
struct Ignored { int v; };
)FFFFFFFFFFFFFFFF";
int after_the_raw_string(int x) {
return x + 1;
}
FFFFFFFFFFFFFFFF is 16 chars.
Parsed with the C API against this repo at HEAD (src/parser.c + src/scanner.c, linked with the tree-sitter runtime — no editor or downstream tool involved):
delim=14 hasError=false function_definition=1 ERROR=0
delim=15 hasError=false function_definition=1 ERROR=0
delim=16 hasError=true function_definition=0 ERROR=3 <-- legal, rejected
delim=17 hasError=true function_definition=0 ERROR=3
delim=18 hasError=true function_definition=0 ERROR=3
after_the_raw_string disappears entirely at 16.
Cause
src/scanner.c, opening-delimiter loop:
for (;;) {
if (scanner->delimiter_length >= MAX_DELIMITER_LENGTH || lexer->eof(lexer) || lexer->lookahead == '\\' ||
iswspace(lexer->lookahead)) {
return false; // (1) capacity guard
}
if (lexer->lookahead == '(') {
return scanner->delimiter_length > 0; // (2) legal terminator
}
scanner->delimiter[scanner->delimiter_length++] = lexer->lookahead;
advance(lexer);
}
With a 16-char delimiter, the 16 d-chars are consumed and delimiter_length == 16. On the next iteration lookahead is ( — the legal terminator, check (2) — but check (1) runs first, sees the buffer is full, and returns false. The delimiter is never accepted even though nothing is wrong with it: the buffer is full, not overflowing.
15 works only because the loop still has a free slot when it reaches (.
Fix
Test the terminator before the capacity guard. A full buffer followed by ( is a complete, legal delimiter; a full buffer followed by another d-char is the genuine overflow.
for (;;) {
- if (scanner->delimiter_length >= MAX_DELIMITER_LENGTH || lexer->eof(lexer) || lexer->lookahead == '\\' ||
- iswspace(lexer->lookahead)) {
- return false;
- }
if (lexer->lookahead == '(') {
// Rather than create a token for an empty delimiter, we fail and
// let the grammar fall back to a delimiter-less rule.
return scanner->delimiter_length > 0;
}
+ if (scanner->delimiter_length >= MAX_DELIMITER_LENGTH || lexer->eof(lexer) || lexer->lookahead == '\\' ||
+ iswspace(lexer->lookahead)) {
+ return false;
+ }
scanner->delimiter[scanner->delimiter_length++] = lexer->lookahead;
advance(lexer);
}
No bounds change: delimiter[] is still only written when delimiter_length < MAX_DELIMITER_LENGTH, so the array cannot overflow.
Verification
Built stock and patched scanners from the same sources and swept delimiter lengths 0-18, comparing function_definition counts:
| length | stock | patched | |
|---|---|---|---|
| 0-15 | 1 | 1 | identical |
| 16 | 0 | 1 | fixed |
| 17, 18 | 0 | 0 | identical — still rejected |
The patch changes behaviour at exactly one length, the one that is legal today and rejected. 17+ stays rejected, so the spec limit is preserved, and length 0 (the empty-delimiter fallback the comment describes) is unchanged.
What I did not do: I could not run the corpus suite — npx tree-sitter-cli did not finish downloading in my environment — so this is verified by differential parsing rather than by tree-sitter test. Worth running before merging.
Happy to open a PR with the patch and a corpus case if useful.
Why it matters downstream
Files most likely to carry long descriptive delimiters — code generators, template/scaffold files, embedded shader or SQL blobs — are exactly where FILE_TEMPLATE_V1, SHADER_SOURCE_V2 or CMAKE_TEMPLATE_1 read as natural, and all are 16. When it happens the file yields no symbols at all, with no diagnostic.
Distinct from #245 (error-recovery synthesising a raw_string_delimiter, fixed in 3d8d510a) — nothing here is in error recovery; the input is well-formed C++.
Environment: this repo at HEAD, tree-sitter runtime from tree-sitter/tree-sitter HEAD, gcc 13.3.0, Ubuntu 24.04.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/scanner.c at scan_raw_string_delimiter and inspect the opening-delimiter loop and its capacity check. Verify the parser accepts a 16-character delimiter while still rejecting longer delimiters, then run the available parser checks and add or run a corpus regression case if supported; the existing differential sweep provides the expected behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- Under an hour
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 90/100