Skip to content

bug: out-of-class = default; reparses as expression_statement after regen with tree-sitter-cli >= 0.25.7 #357

@coseto6125

Description

@coseto6125

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;`.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions