Functional cast with `unsigned` / `signed` / `long` / `short` (e.g. `unsigned(x)`) parsed as a declaration in conditions, producing ERROR
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 82/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- javascript
- Domain
- compilers
Research direction
Start in grammar.js at the call_expression rule (the issue links the relevant lines), then review how its function field handles primitive_type. Run tree-sitter generate and tree-sitter test, and parse the minimal if (unsigned(id) < 8) example. Done means the example and while (long(n) > 0) parse without errors while the listed declaration examples keep their existing trees.
Written by the indexing model from the issue text.
Description
Description
A functional-notation cast whose type is a sized_type_specifier (unsigned(x), signed(x), long(x), short(x)) is not
parsed as an expression wherever a declaration could also start: at the beginning of a statement or in the condition of
if / while / switch. The parser commits to a declaration (unsigned as the type, (x) as a parenthesized declarator),
and the following operator (<, >=, ==, ...) produces an ERROR node.
The same code with a primitive_type (int(x)) parses fine, because call_expression accepts primitive_type as the
function field, but not sized_type_specifier. In positions where a declaration is impossible (e.g. return unsigned(id) < 8;)
there is no error only because unsigned is lexed as an identifier and the cast becomes an ordinary call.
Real-world example: fmt, include/fmt/core.h (basic_format_args::get):
if (unsigned(id) < unsigned(max_size())) arg = args_[id];
In larger files the error recovery after this ERROR can swallow much more than the condition itself (in our runs on fmt
headers it absorbed the enclosing class body).
Checked with tree-sitter CLI 0.26.3 on:
12bd6f7(v0.23.4-37, 2025-09-27);- current master
c009222(v0.23.4-41, 2026-09-17).
Both produce the same tree.
Minimal example
bool f(int id) {
if (unsigned(id) < 8) return true;
return false;
}
Actual (tree-sitter parse, identical on 12bd6f7 and c009222)
(translation_unit [0, 0] - [4, 0]
(function_definition [0, 0] - [3, 1]
type: (primitive_type [0, 0] - [0, 4])
declarator: (function_declarator [0, 5] - [0, 14]
declarator: (identifier [0, 5] - [0, 6])
parameters: (parameter_list [0, 6] - [0, 14]
(parameter_declaration [0, 7] - [0, 13]
type: (primitive_type [0, 7] - [0, 10])
declarator: (identifier [0, 11] - [0, 13]))))
body: (compound_statement [0, 15] - [3, 1]
(if_statement [1, 2] - [1, 36]
condition: (condition_clause [1, 5] - [1, 23]
(ERROR [1, 6] - [1, 20]
type: (sized_type_specifier [1, 6] - [1, 14])
(parenthesized_declarator [1, 14] - [1, 18]
(identifier [1, 15] - [1, 17])))
value: (number_literal [1, 21] - [1, 22]))
consequence: (return_statement [1, 24] - [1, 36]
(true [1, 31] - [1, 35])))
(return_statement [2, 2] - [2, 15]
(false [2, 9] - [2, 14])))))
ex.cc Parse: 0.77 ms 93 bytes/ms (ERROR [1, 6] - [1, 20])
Expected
The same shape as for if (int(id) < 8), with sized_type_specifier as the function of the call_expression
(output of a local build with the change proposed below):
(translation_unit [0, 0] - [4, 0]
(function_definition [0, 0] - [3, 1]
type: (primitive_type [0, 0] - [0, 4])
declarator: (function_declarator [0, 5] - [0, 14]
declarator: (identifier [0, 5] - [0, 6])
parameters: (parameter_list [0, 6] - [0, 14]
(parameter_declaration [0, 7] - [0, 13]
type: (primitive_type [0, 7] - [0, 10])
declarator: (identifier [0, 11] - [0, 13]))))
body: (compound_statement [0, 15] - [3, 1]
(if_statement [1, 2] - [1, 36]
condition: (condition_clause [1, 5] - [1, 23]
value: (binary_expression [1, 6] - [1, 22]
left: (call_expression [1, 6] - [1, 18]
function: (sized_type_specifier [1, 6] - [1, 14])
arguments: (argument_list [1, 14] - [1, 18]
(identifier [1, 15] - [1, 17])))
right: (number_literal [1, 21] - [1, 22])))
consequence: (return_statement [1, 24] - [1, 36]
(true [1, 31] - [1, 35])))
(return_statement [2, 2] - [2, 15]
(false [2, 9] - [2, 14])))))
Cause
call_expression only allows primitive_type (or a splice type) as the function of a functional cast:
https://github.com/tree-sitter/tree-sitter-cpp/blob/c009222808634c1014f82438d4883753516a2c24/grammar.js#L1080-L1086
call_expression: ($, original) => prec.dynamic(1, choice(original, seq(
field('function', choice($.primitive_type, seq(
optional('typename'),
$.splice_type_specifier,
))),
field('arguments', $.argument_list),
))),
Proposed fix (one line)
call_expression: ($, original) => prec.dynamic(1, choice(original, seq(
- field('function', choice($.primitive_type, seq(
+ field('function', choice($.primitive_type, $.sized_type_specifier, seq(
optional('typename'),
$.splice_type_specifier,
))),
With this change on master c009222:
tree-sitter generate(CLI 0.26.3) succeeds without new conflicts;tree-sitter testpasses (200 of 200, same as without the change);- the example above parses without errors (tree above), and so does
while (long(n) > 0) --n;
(on master it gives the same kind ofERRORwithlongassized_type_specifier); - declarations stay declarations:
unsigned (*fp)(int);,std::function<unsigned(int)> h;,
unsigned long y = (unsigned long)(x);give the same trees as before.
One visible side effect: the statement unsigned (x); becomes an expression_statement with a call_expression instead of a
declaration. This matches how master already parses int (x); (also a call_expression), so the two are now consistent;
if the declaration reading is preferred for both, that is a separate question about prec.dynamic(1, ...) on this rule.
- Dominant language
- JavaScript
- Stars
- 459
- Forks
- 190
- PR merge metrics
- No merged PRs in 30d
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
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.
More from tree-sitter/tree-sitter-cpp
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
tree-sitter/tree-sitter-cpp#371 · 1 comment ·
-
`tree_sitter_cpp_external_scanner_deserialize` doesn't bound `length` against the size of the delimiter bufferPossibly taken @dekobon claimed this 73 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
tree-sitter/tree-sitter-cpp#368 ·
-
bug
Difficulty 3/5 Half a day Newbie friendliness 55/100
tree-sitter/tree-sitter-cpp#376 ·
-
enhancement
Difficulty 3/5 1-2 days Newbie friendliness 45/100
tree-sitter/tree-sitter-cpp#365 · 1 reaction ·
-
bug
Difficulty 3/5 1-2 days Newbie friendliness 45/100
tree-sitter/tree-sitter-cpp#363 ·
All issues in tree-sitter/tree-sitter-cpp
Similar issues
-
app_installations reports an error on every repo or suborg change when the plugin is not configuredOpen
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
github-community-projects/safe-settings#1111 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
needs triage
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
pbakaus/impeccable#1055 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 82/100
guaguastandup/zotero-pdf2zh#368 ·
-
Add: EUROTV NEWSOpenchannels:add check:passed
Difficulty 2/5 1-3 hours Newbie friendliness 70/100