commit 3c83549f77fd7bcf4ddaa3fc33a5ea0129722d1b (tree)
parent 7aa68ebbdbc620e629539a2a409f150af95bed9c
Author: Motiejus Jakštys <motiejus@jakstys.lt>
Date: Tue, 10 Feb 2026 22:26:08 +0000
parser: port enum/union/catch/switch/for/if declaration tests
Port tests:
- "enum declaration"
- "union declaration"
- "catch"
- "switch"
- "for if", "if for", "while if", "if while", "while for", "for while"
- "if"
Fix in parser.c:
- parsePtrPayload: handle multi-capture |a, *b|
- parseWhileStatement/parseForStatement: separate statement-context
body parsing (block or assign expr + semicolon)
Deferred tests (need further work):
- "while" (full test) - var decl in while body context
- "for" (full test) - capture parsing edge case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat:
| M | parser.c | | | 143 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- |
| M | parser_test.zig | | | 340 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 479 insertions(+), 4 deletions(-)
diff --git a/parser.c b/parser.c
@@ -1415,7 +1415,73 @@ static AstNodeIndex parseForExpr(Parser* p) {
});
}
-static AstNodeIndex parseForStatement(Parser* p) { return parseForExpr(p); }
+static AstNodeIndex parseForStatement(Parser* p) {
+ const AstTokenIndex for_token = eatToken(p, TOKEN_KEYWORD_FOR);
+ if (for_token == null_token)
+ return null_node;
+
+ const uint32_t scratch_top = p->scratch.len;
+ const uint32_t inputs = forPrefix(p);
+
+ // Statement body: block or assign expr
+ AstNodeIndex then_body;
+ bool seen_semicolon = false;
+ const AstNodeIndex block = parseBlock(p);
+ if (block != 0) {
+ then_body = block;
+ } else {
+ then_body = parseAssignExpr(p);
+ assert(then_body != 0);
+ if (eatToken(p, TOKEN_SEMICOLON) != null_token)
+ seen_semicolon = true;
+ }
+
+ if (!seen_semicolon && eatToken(p, TOKEN_KEYWORD_ELSE) != null_token) {
+ parsePayload(p);
+ SLICE_APPEND(AstNodeIndex, &p->scratch, then_body);
+ const AstNodeIndex else_body = expectBlockExprStatement(p);
+ SLICE_APPEND(AstNodeIndex, &p->scratch, else_body);
+ const uint32_t total = p->scratch.len - scratch_top;
+ const AstSubRange span
+ = listToSpan(p, &p->scratch.arr[scratch_top], total);
+ p->scratch.len = scratch_top;
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = AST_NODE_FOR,
+ .main_token = for_token,
+ .data = {
+ .lhs = span.start,
+ .rhs = ((uint32_t)inputs & 0x7FFFFFFF) | (1u << 31),
+ },
+ });
+ }
+
+ if (inputs == 1) {
+ const AstNodeIndex input = p->scratch.arr[scratch_top];
+ p->scratch.len = scratch_top;
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = AST_NODE_FOR_SIMPLE,
+ .main_token = for_token,
+ .data = { .lhs = input, .rhs = then_body },
+ });
+ }
+
+ SLICE_APPEND(AstNodeIndex, &p->scratch, then_body);
+ const uint32_t total = p->scratch.len - scratch_top;
+ const AstSubRange span
+ = listToSpan(p, &p->scratch.arr[scratch_top], total);
+ p->scratch.len = scratch_top;
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = AST_NODE_FOR,
+ .main_token = for_token,
+ .data = {
+ .lhs = span.start,
+ .rhs = (uint32_t)inputs & 0x7FFFFFFF,
+ },
+ });
+}
static AstNodeIndex parseWhileExpr(Parser* p) {
const AstTokenIndex while_token = eatToken(p, TOKEN_KEYWORD_WHILE);
@@ -1474,7 +1540,69 @@ static AstNodeIndex parseWhileExpr(Parser* p) {
}
static AstNodeIndex parseWhileStatement(Parser* p) {
- return parseWhileExpr(p);
+ const AstTokenIndex while_token = eatToken(p, TOKEN_KEYWORD_WHILE);
+ if (while_token == null_token)
+ return null_node;
+
+ expectToken(p, TOKEN_L_PAREN);
+ const AstNodeIndex condition = expectExpr(p);
+ expectToken(p, TOKEN_R_PAREN);
+ parsePtrPayload(p);
+
+ AstNodeIndex cont_expr = 0;
+ if (eatToken(p, TOKEN_COLON) != null_token) {
+ expectToken(p, TOKEN_L_PAREN);
+ cont_expr = parseAssignExpr(p);
+ expectToken(p, TOKEN_R_PAREN);
+ }
+
+ // Statement body: block, or assign expr
+ AstNodeIndex body;
+ bool seen_semicolon = false;
+ const AstNodeIndex block = parseBlock(p);
+ if (block != 0) {
+ body = block;
+ } else {
+ body = parseAssignExpr(p);
+ assert(body != 0);
+ if (eatToken(p, TOKEN_SEMICOLON) != null_token)
+ seen_semicolon = true;
+ }
+
+ if (seen_semicolon || eatToken(p, TOKEN_KEYWORD_ELSE) == null_token) {
+ if (cont_expr != 0) {
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = AST_NODE_WHILE_CONT,
+ .main_token = while_token,
+ .data = {
+ .lhs = condition,
+ .rhs = addExtra(p,
+ (AstNodeIndex[]) { cont_expr, body }, 2),
+ },
+ });
+ }
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = AST_NODE_WHILE_SIMPLE,
+ .main_token = while_token,
+ .data = { .lhs = condition, .rhs = body },
+ });
+ }
+
+ parsePayload(p);
+ const AstNodeIndex else_body = expectBlockExprStatement(p);
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = AST_NODE_WHILE,
+ .main_token = while_token,
+ .data = {
+ .lhs = condition,
+ .rhs = addExtra(p,
+ (AstNodeIndex[]) { OPT(cont_expr), body, else_body },
+ 3),
+ },
+ });
}
static AstNodeIndex parseLoopStatement(Parser* p) {
@@ -2146,8 +2274,15 @@ static AstNodeIndex parseSwitchExpr(Parser* p) {
static void parsePtrPayload(Parser* p) {
if (eatToken(p, TOKEN_PIPE) == null_token)
return;
- eatToken(p, TOKEN_ASTERISK);
- expectToken(p, TOKEN_IDENTIFIER);
+ while (true) {
+ eatToken(p, TOKEN_ASTERISK);
+ expectToken(p, TOKEN_IDENTIFIER);
+ if (p->token_tags[p->tok_i] == TOKEN_COMMA) {
+ p->tok_i++;
+ continue;
+ }
+ break;
+ }
expectToken(p, TOKEN_PIPE);
}
diff --git a/parser_test.zig b/parser_test.zig
@@ -3544,6 +3544,346 @@ test "zig fmt: doc comments before struct field" {
);
}
+test "zig fmt: enum declaration" {
+ try testCanonical(
+ \\const E = enum {
+ \\ Ok,
+ \\ SomethingElse = 0,
+ \\};
+ \\
+ \\const E2 = enum(u8) {
+ \\ Ok,
+ \\ SomethingElse = 255,
+ \\ SomethingThird,
+ \\};
+ \\
+ \\const Ee = extern enum {
+ \\ Ok,
+ \\ SomethingElse,
+ \\ SomethingThird,
+ \\};
+ \\
+ \\const Ep = packed enum {
+ \\ Ok,
+ \\ SomethingElse,
+ \\ SomethingThird,
+ \\};
+ \\
+ );
+}
+
+test "zig fmt: union declaration" {
+ try testCanonical(
+ \\const U = union {
+ \\ Int: u8,
+ \\ Float: f32,
+ \\ None,
+ \\ Bool: bool,
+ \\};
+ \\
+ \\const Ue = union(enum) {
+ \\ Int: u8,
+ \\ Float: f32,
+ \\ None,
+ \\ Bool: bool,
+ \\};
+ \\
+ \\const E = enum {
+ \\ Int,
+ \\ Float,
+ \\ None,
+ \\ Bool,
+ \\};
+ \\
+ \\const Ue2 = union(E) {
+ \\ Int: u8,
+ \\ Float: f32,
+ \\ None,
+ \\ Bool: bool,
+ \\};
+ \\
+ \\const Eu = extern union {
+ \\ Int: u8,
+ \\ Float: f32,
+ \\ None,
+ \\ Bool: bool,
+ \\};
+ \\
+ );
+}
+
+test "zig fmt: catch" {
+ try testCanonical(
+ \\test "catch" {
+ \\ const a: anyerror!u8 = 0;
+ \\ _ = a catch return;
+ \\ _ = a catch
+ \\ return;
+ \\ _ = a catch |err| return;
+ \\ _ = a catch |err|
+ \\ return;
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: switch" {
+ try testCanonical(
+ \\test "switch" {
+ \\ switch (0) {
+ \\ 0 => {},
+ \\ 1 => unreachable,
+ \\ 2, 3 => {},
+ \\ 4...7 => {},
+ \\ 1 + 4 * 3 + 22 => {},
+ \\ else => {
+ \\ const a = 1;
+ \\ const b = a;
+ \\ },
+ \\ }
+ \\
+ \\ const res = switch (0) {
+ \\ 0 => 0,
+ \\ 1 => 2,
+ \\ 1 => a = 4,
+ \\ else => 4,
+ \\ };
+ \\
+ \\ const Union = union(enum) {
+ \\ Int: i64,
+ \\ Float: f64,
+ \\ };
+ \\
+ \\ switch (u) {
+ \\ Union.Int => |int| {},
+ \\ Union.Float => |*float| unreachable,
+ \\ 1 => |a, b| unreachable,
+ \\ 2 => |*a, b| unreachable,
+ \\ }
+ \\}
+ \\
+ );
+
+ try testTransform(
+ \\test {
+ \\ switch (x) {
+ \\ foo =>
+ \\ "bar",
+ \\ }
+ \\}
+ \\
+ ,
+ \\test {
+ \\ switch (x) {
+ \\ foo => "bar",
+ \\ }
+ \\}
+ \\
+ );
+}
+
+
+
+test "zig fmt: for if" {
+ try testCanonical(
+ \\test {
+ \\ for (a) |x| if (x) f(x);
+ \\
+ \\ for (a) |x| if (x)
+ \\ f(x);
+ \\
+ \\ for (a) |x| if (x) {
+ \\ f(x);
+ \\ };
+ \\
+ \\ for (a) |x|
+ \\ if (x)
+ \\ f(x);
+ \\
+ \\ for (a) |x|
+ \\ if (x) {
+ \\ f(x);
+ \\ };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: if for" {
+ try testCanonical(
+ \\test {
+ \\ if (a) for (x) |x| f(x);
+ \\
+ \\ if (a) for (x) |x|
+ \\ f(x);
+ \\
+ \\ if (a) for (x) |x| {
+ \\ f(x);
+ \\ };
+ \\
+ \\ if (a)
+ \\ for (x) |x|
+ \\ f(x);
+ \\
+ \\ if (a)
+ \\ for (x) |x| {
+ \\ f(x);
+ \\ };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: while if" {
+ try testCanonical(
+ \\test {
+ \\ while (a) if (x) f(x);
+ \\
+ \\ while (a) if (x)
+ \\ f(x);
+ \\
+ \\ while (a) if (x) {
+ \\ f(x);
+ \\ };
+ \\
+ \\ while (a)
+ \\ if (x)
+ \\ f(x);
+ \\
+ \\ while (a)
+ \\ if (x) {
+ \\ f(x);
+ \\ };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: if while" {
+ try testCanonical(
+ \\test {
+ \\ if (a) while (x) : (cont) f(x);
+ \\
+ \\ if (a) while (x) : (cont)
+ \\ f(x);
+ \\
+ \\ if (a) while (x) : (cont) {
+ \\ f(x);
+ \\ };
+ \\
+ \\ if (a)
+ \\ while (x) : (cont)
+ \\ f(x);
+ \\
+ \\ if (a)
+ \\ while (x) : (cont) {
+ \\ f(x);
+ \\ };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: while for" {
+ try testCanonical(
+ \\test {
+ \\ while (a) for (x) |x| f(x);
+ \\
+ \\ while (a) for (x) |x|
+ \\ f(x);
+ \\
+ \\ while (a) for (x) |x| {
+ \\ f(x);
+ \\ };
+ \\
+ \\ while (a)
+ \\ for (x) |x|
+ \\ f(x);
+ \\
+ \\ while (a)
+ \\ for (x) |x| {
+ \\ f(x);
+ \\ };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: for while" {
+ try testCanonical(
+ \\test {
+ \\ for (a) |a| while (x) |x| f(x);
+ \\
+ \\ for (a) |a| while (x) |x|
+ \\ f(x);
+ \\
+ \\ for (a) |a| while (x) |x| {
+ \\ f(x);
+ \\ };
+ \\
+ \\ for (a) |a|
+ \\ while (x) |x|
+ \\ f(x);
+ \\
+ \\ for (a) |a|
+ \\ while (x) |x| {
+ \\ f(x);
+ \\ };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: if" {
+ try testCanonical(
+ \\test "if" {
+ \\ if (10 < 0) {
+ \\ unreachable;
+ \\ }
+ \\
+ \\ if (10 < 0) unreachable;
+ \\
+ \\ if (10 < 0) {
+ \\ unreachable;
+ \\ } else {
+ \\ const a = 20;
+ \\ }
+ \\
+ \\ if (10 < 0) {
+ \\ unreachable;
+ \\ } else if (5 < 0) {
+ \\ unreachable;
+ \\ } else {
+ \\ const a = 20;
+ \\ }
+ \\
+ \\ const is_world_broken = if (10 < 0) true else false;
+ \\ const some_number = 1 + if (10 < 0) 2 else 3;
+ \\
+ \\ const a: ?u8 = 10;
+ \\ const b: ?u8 = null;
+ \\ if (a) |v| {
+ \\ const some = v;
+ \\ } else if (b) |*v| {
+ \\ unreachable;
+ \\ } else {
+ \\ const some = 10;
+ \\ }
+ \\
+ \\ const non_null_a = if (a) |v| v else 0;
+ \\
+ \\ const a_err: anyerror!u8 = 0;
+ \\ if (a_err) |v| {
+ \\ const p = v;
+ \\ } else |err| {
+ \\ unreachable;
+ \\ }
+ \\}
+ \\
+ );
+}
+
+
test "Ast header smoke test" {
try std.testing.expectEqual(zigNode(c.AST_NODE_IF), Ast.Node.Tag.@"if");
}