Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
302eb81
Implement for/while/do-while/try-catch, braceless if-bodies, array co…
mallyskies Jul 6, 2026
d0ac3a5
test: fix comprehension_while corpus test to match this fork's dedica…
mallyskies Jul 8, 2026
e72d96b
fix: allow a parenthesized sub-expression as a chain head or tail term
mallyskies Jul 7, 2026
4e01d06
fix: allow a ternary or parenthesized expression as a return value, and
mallyskies Jul 7, 2026
47b59f6
fix: allow multiple statements in a switch case without a block
mallyskies Jul 7, 2026
b0164d1
fix: single-field/no-comma typedef struct, and anonymous struct as a
mallyskies Jul 7, 2026
b265ecd
fix: preprocessor-conditional type annotations (#if/#else/#end around…
mallyskies Jul 7, 2026
5305bdd
fix: allow a different default/initializer per branch in a conditiona…
mallyskies Jul 7, 2026
4ff3653
fix: dangling else/else-if split from its if by a #if/#end boundary
mallyskies Jul 7, 2026
771ebd8
Update grammar-literals.js
mallyskies Jul 8, 2026
3577586
Update grammar-literals.js
mallyskies Jul 8, 2026
11bb841
Retrigger CI (previous run was cancelled, not a real test failure)
mallyskies Jul 8, 2026
0671782
Trim scenario narrative from comments, keep behavior/invariants
mallyskies Jul 8, 2026
227258b
Merge remote-tracking branch 'origin/fix/for-while-trycatch-comprehen…
mallyskies Jul 8, 2026
41ba7d2
Merge remote-tracking branch 'origin/fix/parenthesized-chain-terms' i…
mallyskies Jul 8, 2026
72eb247
Merge remote-tracking branch 'origin/fix/ternary-parenthesized-return…
mallyskies Jul 8, 2026
cc71a85
Merge remote-tracking branch 'origin/fix/switch-case-multiple-stateme…
mallyskies Jul 8, 2026
0d433a3
Merge remote-tracking branch 'origin/fix/typedef-struct-anonymous-fun…
mallyskies Jul 8, 2026
3644ff2
Merge remote-tracking branch 'origin/fix/preprocessor-conditional-typ…
mallyskies Jul 8, 2026
406eea9
Merge remote-tracking branch 'origin/fix/conditional-type-per-branch-…
mallyskies Jul 8, 2026
c97309d
Restore comment for the return-value ternary/parenthesized-expression…
mallyskies Jul 8, 2026
122771a
Merge remote-tracking branch 'origin/fix/ternary-parenthesized-return…
mallyskies Jul 8, 2026
3272535
Merge remote-tracking branch 'origin/fix/switch-case-multiple-stateme…
mallyskies Jul 8, 2026
0b26cce
Merge remote-tracking branch 'origin/fix/typedef-struct-anonymous-fun…
mallyskies Jul 8, 2026
e0d9dfd
Merge remote-tracking branch 'origin/fix/preprocessor-conditional-typ…
mallyskies Jul 8, 2026
ac14d5a
Merge remote-tracking branch 'origin/fix/conditional-type-per-branch-…
mallyskies Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 54 additions & 4 deletions grammar-declarations.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,55 @@ module.exports = {
field('body', $.block),
),

// Haxe's "class-like" anonymous structure syntax
// (`typedef Point = { var x:Int; function bar():Void; }`) needs a body
// that can hold var/function member declarations, so this used to just
// reuse $.block directly. That collided with $.structure_type's own
// comma-separated-pairs syntax (`typedef X = { x:Int, y:Int }`) for a
// single-field, no-trailing-comma struct (`typedef X = { f:T }`): a bare
// 'identifier : expression' is ALSO a valid one-statement $.block (via
// $.pair's low-precedence bare-literal alternative, see its comment in
// grammar-literals.js), so $.block and $.structure_type both matched
// that exact token sequence. This is the same class of GLR table-
// construction merge as the documented block-vs-object empty-body
// ambiguity above ($.block's own comment) -- tree-sitter's conflict
// analysis calls [$.typedef_declaration, $.structure_type] an
// "unnecessary conflict" (no static ambiguity to resolve) yet the
// single-item-no-comma case still hard-errors at runtime, and neither
// declaring the conflict explicitly, nor bumping either rule's static
// OR dynamic precedence, changes that -- it is not a real tie to break,
// the same not-fixable-via-prec class as the earlier case. Two or more
// fields, or a trailing comma, dodge it because a ',' forces an early
// commit into the comma-list production, ruling out the single-bare-
// statement reading before the ambiguous state is ever reached.
//
// The actual fix: this body never legitimately needs $.block's full
// generality (arbitrary statements) -- real Haxe only ever puts
// var/function member declarations in it, both of which start with a
// keyword ('var'/'final'/'function', or metadata's '@'), never a bare
// identifier, so a dedicated body rule restricted to those two
// declaration kinds can never overlap with $.structure_type's bare
// 'identifier : type' shape in the first place. Aliased to $.block so
// existing consumers/tests see the same node type as before.
_structure_class_body: ($) =>
seq('{', repeat(choice($.variable_declaration, $.function_declaration)), $._closing_brace),

typedef_declaration: ($) =>
seq(
repeat($.metadata),
repeat($._modifier),
'typedef',
field('name', $._lhs_expression),
optional($.type_params),
seq('=', choice($.block, $.structure_type, $._lhs_expression, $.type)),
seq(
'=',
choice(
$.structure_type,
alias($._structure_class_body, $.block),
$._lhs_expression,
$.type,
),
),
$._lookback_semicolon,
),

Expand Down Expand Up @@ -116,7 +157,7 @@ module.exports = {
field('name', choice($._lhs_expression, 'new')),
optional($.type_params),
$._function_arg_list,
optional(seq(':', field('return_type', $.type))),
optional(seq(':', field('return_type', choice($.type, $._conditional_type)))),
optional(field('body', $.block)),
$._lookback_semicolon,
),
Expand All @@ -135,7 +176,9 @@ module.exports = {
optional('?'),
field('name', $._lhs_expression),
optional('?'),
optional(seq(':', alias(choice($._lhs_expression, $.type, $.structure_type), $.type))),
optional(
seq(':', alias(choice($._lhs_expression, $.type, $.structure_type, $._conditional_type), $.type)),
),
optional(seq($._assignmentOperator, $._literal)),
),
),
Expand All @@ -147,7 +190,14 @@ module.exports = {
choice('var', 'final'),
field('name', $._lhs_expression),
optional($.access_identifiers),
optional(seq(':', optional(repeat('(')), field('type', $.type), optional(repeat(')')))),
optional(
seq(
':',
optional(repeat('(')),
field('type', choice($.type, $._conditional_type)),
optional(repeat(')')),
),
),
optional(seq(($._assignmentOperator, $.operator), $.expression)),
$._lookback_semicolon,
),
Expand Down
37 changes: 33 additions & 4 deletions grammar-literals.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,23 @@ module.exports = {

// Match any [42, 0xFF43]
integer: ($) => choice(/[\d_]+/, /0x[a-fA-F\d_]+/),
// Match any [0.32, 3., 2.1e5]
float: ($) => choice(/[\d_]+[\.]+[\d_]*/, /[\d_]+[\.]+[\d_]*e[\d_]*/),
// Match any [0.32, 2.1e5]. Requires at least one digit after the '.'
// (`[\d_]+` not `[\d_]*`) -- deliberately drops support for a *trailing*-
// dot float with no digits after it (`3.`, which the original comment
// here listed as supported) to fix a much more damaging, silent bug:
// `0...10` (an integer immediately followed by the range operator -- the
// single most common shape for a `for` loop's iterable, e.g.
// `for (i in 0...10)`) was being lexed as ONE token, "0.", stopping right
// after the first '.' and silently swallowing the rest of the range
// operator with no ERROR node (a float is a perfectly valid, complete
// token on its own). Tree-sitter's regex engine has no lookahead, so
// there's no way to keep `3.` valid while still telling `0.` apart from
// `0...`; picked the fix that helps far more real code than it costs
// (the `for (i in 0...N)` shape is extremely common;
// occurrence of a genuinely bare trailing-dot float is comparatively
// rare and easy to write as `3.0` instead if it ever turns up
// broken).
float: ($) => choice(/[\d_]+\.[\d_]+/, /[\d_]+\.[\d_]+e[\d_]*/),
// Match either [true, false]
bool: ($) => choice('true', 'false'),
// Match any ["XXX", 'XXX']
Expand All @@ -24,10 +39,16 @@ module.exports = {
null: ($) => 'null',

// https://haxe.org/manual/expression-array-declaration.html
// Array comprehension (https://haxe.org/manual/lf-array-comprehension.html,
// `[for (i in 0...10) i]` / `[while (...) ...]`) is its own alternative,
// not folded into the plain-elements one -- it starts with a literal
// 'for'/'while' token, so there's no overlap with a normal
// comma-separated array literal (including the single-element case,
// `[x]`) to disambiguate.
array: ($) =>
choice(
seq('[', commaSep(prec.left($.expression)), ']'),
seq('[', $.expression, $.identifier, ']'), //array comprehension
seq('[', choice($.comprehension_for, $.comprehension_while), ']'),
),

// https://haxe.org/manual/expression-map-declaration.html
Expand All @@ -36,7 +57,15 @@ module.exports = {
// https://haxe.org/manual/expression-object-declaration.html
object: ($) => prec(1, seq('{', commaSep($.pair), $._closing_brace)),

structure_type: ($) => prec(1, seq('{', commaSep(alias($.structure_type_pair, $.pair)), $._closing_brace)),
// prec.dynamic, NOT a bump to the static prec(): it must only tie-break
// a genuinely completed GLR ambiguity in favor of the structure reading
// -- e.g. `(name: {})`, ambiguous against a parenthesized bare-pair
// expression whose value is an empty $.object, since $.structure_type
// is a bare $.type alternative (grammar.js). A static bump changes the
// shift/reduce table globally and breaks multi-arrow $.function_type
// chains (`String->{}->Void` stops after the `{}` segment).
structure_type: ($) =>
prec.dynamic(1, prec(1, seq('{', commaSep(alias($.structure_type_pair, $.pair)), $._closing_brace))),
structure_type_pair: ($) => prec.left(seq(choice($.identifier), ':', $.type)),

// Sub part of map and object literals.
Expand Down
Loading
Loading