tree-sitter / tree-sitter/tree-sitter-cpp
bug: out-of-class `= default;` reparses as expression_statement after regen with tree-sitter-cli >= 0.25.7
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 453
- Forks
- 185
- PR merge metrics
- No merged PRs in 30d
Description
Out-of-class = default; special-member definitions silently lose the function_definition shape on master (commit 8b5b49e). = delete; is unaffected.
Repro
Foo::~Foo() = default; // BUG — see AST below
Foo::Foo() = default; // BUG — same shape
Foo::~Foo() = delete; // OK — still function_definition
Foo::Foo() = delete; // OK — still function_definition
Bad parse for Foo::~Foo() = default;:
translation_unit
expression_statement
assignment_expression
call_expression (left)
qualified_identifier (function)
namespace_identifier (scope) "Foo"
destructor_name (name)
identifier "Foo"
argument_list (arguments)
"=" (operator)
identifier (right) "default" ;; ← `default` lexed as identifier
Expected (and what 0.23.4 / pre-regen master produce):
translation_unit
function_definition (aliased from constructor_or_destructor_definition)
function_declarator (declarator)
qualified_identifier (declarator)
namespace_identifier (scope) "Foo"
destructor_name (name)
identifier "Foo"
parameter_list (parameters)
default_method_clause
Bisect
grammar.js is identical between the last good and first bad commit. Only the regenerated artefacts differ.
| commit | tree-sitter-cli | parses = default; as |
|---|---|---|
| cacfb40 (2025-07-06) | 0.25.6 | function_definition ✓ |
| 12bd6f7 (2025-09-19) | 0.25.9 | expression_statement ✗ |
The cli bump in 1832dd7 (0.25.6 → 0.25.9) is the only intervening change that touches generator behaviour.
Likely root cause
tree-sitter v0.25.7 shipped PR #4586 (`fix(generate): use topological sort for subtype map`) which changed the visit order used when building the LR conflict-resolution table. At the ambiguity point for Foo::~Foo() = default;:
- candidate A:
constructor_or_destructor_definition+default_method_clause - candidate B:
expression_statement > assignment_expression > call_expression(theFoo::~Foo()call assigned to identifierdefault)
Old visit order picked A. Topologically sorted order picks B.
= delete; survives because delete is a keyword token in tree-sitter-cpp and can't slot into the right: of assignment_expression. default lexes as (identifier) in expression position (e.g. for switch labels), so the alternative reduction stays viable.
PR #4586 itself is a legitimate cli fix — the grammar is what needs a precedence hint.
Suggested fix
prec.dynamic on the default_method_clause branch should be enough to force candidate A back to winning:
constructor_or_destructor_definition: $ => seq(
repeat($._constructor_specifiers),
field('declarator', $.function_declarator),
choice(
seq(
optional($.field_initializer_list),
field('body', $.compound_statement),
),
alias($.constructor_try_statement, $.try_statement),
- $.default_method_clause,
+ prec.dynamic(1, $.default_method_clause),
$.delete_method_clause,
$.pure_virtual_clause,
),
),
Untested — happy to verify against real corpora (nlohmann/json, Fuzzer, LLVM) if a candidate patch lands.
Workaround for downstream consumers stuck on master
If you're indexing C++ with tree-sitter-cpp ABI 15 and need to recover the lost destructor/constructor symbols, this query pattern catches the regressed shape:
(expression_statement
(assignment_expression
left: (call_expression
function: (qualified_identifier
name: [
(destructor_name) @name.method
(identifier) @name.method
]))
right: (identifier) @_default
(#eq? @_default "default"))) @method
False-positive risk is essentially zero — the only inputs that match are exactly the regressed AST shape, and real C++ never has a non-special-member `expr() = default;`.
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 grammar.js at constructor_or_destructor_definition and inspect the default_method_clause branch and its conflict with expression_statement. Regenerate the parser with the affected tree-sitter-cli version, then verify that out-of-class constructor and destructor definitions using = default; produce function_definition nodes while = delete; remains unchanged. Validate against the mentioned C++ corpora if available.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, javascript
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100