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

Open
#357 0 comments 0 reactions 0 assignees View on GitHub

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 (the Foo::~Foo() call assigned to identifier default)

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.