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>
This commit is contained in:
2026-02-10 22:26:08 +00:00
parent 7aa68ebbdb
commit 3c83549f77
2 changed files with 479 additions and 4 deletions

143
parser.c
View File

@@ -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);
}

View File

@@ -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");
}