commit b229e001ff650b117f3103502ff4bb4765d226da (tree)
parent e471e4564a27564ff4eb6db4a3d53070252a7341
Author: Motiejus Jakštys <motiejus.jakstys@chronosphere.io>
Date: Tue, 10 Feb 2026 12:58:44 +0000
parser: add struct/array init literal tests
Port tests from upstream parser_test.zig:
- "anon struct literal 0/1/2/3 element" (with and without comma)
- "struct literal 0/1 element"
Implement in parser.c:
- parseFieldInit: parse .field = expr field initializers
- parseInitList: full struct/array init list parsing
- parseCurlySuffixExpr: use parseInitList for X{...} syntax
- parsePrimaryTypeExpr: handle .{...} anonymous init and .ident
enum literals
- Empty X{} produces struct_init_one (matching upstream behavior)
Fix zigData in parser_test.zig:
- array_init_one uses node_and_node (not node_and_opt_node)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat:
| M | parser.c | | | 226 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
| M | parser_test.zig | | | 96 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
2 files changed, 318 insertions(+), 4 deletions(-)
diff --git a/parser.c b/parser.c
@@ -28,6 +28,7 @@ static AstNodeIndex expectSemicolon(Parser*);
static AstTokenIndex expectToken(Parser*, TokenizerTag);
static AstNodeIndex parseFnProto(Parser*);
static Members parseContainerMembers(Parser*);
+static AstNodeIndex parseInitList(Parser*, AstNodeIndex, AstTokenIndex);
typedef struct {
enum { FIELD_STATE_NONE, FIELD_STATE_SEEN, FIELD_STATE_END } tag;
@@ -512,6 +513,29 @@ static AstNodeIndex parsePrimaryTypeExpr(Parser* p) {
case TOKEN_KEYWORD_FOR:
case TOKEN_KEYWORD_WHILE:
case TOKEN_PERIOD:
+ switch (p->token_tags[p->tok_i + 1]) {
+ case TOKEN_IDENTIFIER: {
+ const AstTokenIndex dot = nextToken(p);
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = AST_NODE_ENUM_LITERAL,
+ .main_token = nextToken(p),
+ .data = { .lhs = dot, .rhs = 0 },
+ });
+ }
+ case TOKEN_L_BRACE: {
+ // Anonymous struct/array init: .{ ... }
+ const AstTokenIndex lbrace = p->tok_i + 1;
+ p->tok_i = lbrace + 1;
+ return parseInitList(p, null_node, lbrace);
+ }
+ default:
+ fprintf(stderr,
+ "parsePrimaryTypeExpr: unsupported period suffix %s\n",
+ tokenizerGetTagString(p->token_tags[p->tok_i + 1]));
+ exit(1);
+ }
+ return 0; // tcc
case TOKEN_KEYWORD_ERROR:
fprintf(stderr, "parsePrimaryTypeExpr does not support %s\n",
tokenizerGetTagString(tok));
@@ -987,6 +1011,204 @@ static AstTokenIndex parseBreakLabel(Parser* p) {
return expectToken(p, TOKEN_IDENTIFIER);
}
+// parseFieldInit tries to parse .field_name = expr; returns 0 if not a
+// field init
+static AstNodeIndex parseFieldInit(Parser* p) {
+ if (p->token_tags[p->tok_i] == TOKEN_PERIOD
+ && p->token_tags[p->tok_i + 1] == TOKEN_IDENTIFIER
+ && p->token_tags[p->tok_i + 2] == TOKEN_EQUAL) {
+ p->tok_i += 3;
+ return expectExpr(p);
+ }
+ return null_node;
+}
+
+// parseInitList parses the contents of { ... } for struct/array init.
+// lhs is the type expression (0 for anonymous .{...}).
+// lbrace is the lbrace token index.
+static AstNodeIndex parseInitList(
+ Parser* p, AstNodeIndex lhs, AstTokenIndex lbrace) {
+ CleanupScratch scratch_top __attribute__((__cleanup__(cleanupScratch)))
+ = initCleanupScratch(p);
+
+ const AstNodeIndex field_init = parseFieldInit(p);
+ if (field_init != 0) {
+ // Struct init
+ SLICE_APPEND(AstNodeIndex, &p->scratch, field_init);
+ while (true) {
+ if (p->token_tags[p->tok_i] == TOKEN_COMMA)
+ p->tok_i++;
+ else if (p->token_tags[p->tok_i] == TOKEN_R_BRACE) {
+ p->tok_i++;
+ break;
+ } else {
+ fprintf(
+ stderr, "parseInitList: expected , or } in struct init\n");
+ exit(1);
+ }
+ if (eatToken(p, TOKEN_R_BRACE) != null_token)
+ break;
+ const AstNodeIndex next = parseFieldInit(p);
+ assert(next != 0);
+ SLICE_APPEND(AstNodeIndex, &p->scratch, next);
+ }
+ const bool comma = p->token_tags[p->tok_i - 2] == TOKEN_COMMA;
+ const uint32_t inits_len = p->scratch.len - scratch_top.old_len;
+ if (lhs == 0) {
+ // Anonymous struct init: .{...}
+ switch (inits_len) {
+ case 0:
+ case 1:
+ case 2:
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = comma
+ ? AST_NODE_STRUCT_INIT_DOT_TWO_COMMA
+ : AST_NODE_STRUCT_INIT_DOT_TWO,
+ .main_token = lbrace,
+ .data = {
+ .lhs = inits_len >= 1
+ ? p->scratch.arr[scratch_top.old_len]
+ : 0,
+ .rhs = inits_len >= 2
+ ? p->scratch.arr[scratch_top.old_len + 1]
+ : 0,
+ },
+ });
+ default:;
+ const AstSubRange span = listToSpan(
+ p, &p->scratch.arr[scratch_top.old_len], inits_len);
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = comma ? AST_NODE_STRUCT_INIT_DOT_COMMA
+ : AST_NODE_STRUCT_INIT_DOT,
+ .main_token = lbrace,
+ .data = { .lhs = span.start, .rhs = span.end },
+ });
+ }
+ }
+ // Named struct init: X{...}
+ switch (inits_len) {
+ case 0:
+ case 1:
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = comma ? AST_NODE_STRUCT_INIT_ONE_COMMA
+ : AST_NODE_STRUCT_INIT_ONE,
+ .main_token = lbrace,
+ .data = {
+ .lhs = lhs,
+ .rhs = inits_len >= 1
+ ? p->scratch.arr[scratch_top.old_len]
+ : 0,
+ },
+ });
+ default:;
+ const AstSubRange span = listToSpan(
+ p, &p->scratch.arr[scratch_top.old_len], inits_len);
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = comma ? AST_NODE_STRUCT_INIT_COMMA
+ : AST_NODE_STRUCT_INIT,
+ .main_token = lbrace,
+ .data = {
+ .lhs = lhs,
+ .rhs = addExtra(p,
+ (AstNodeIndex[]) { span.start, span.end }, 2),
+ },
+ });
+ }
+ }
+
+ // Array init or empty init
+ while (true) {
+ if (eatToken(p, TOKEN_R_BRACE) != null_token)
+ break;
+ const AstNodeIndex elem = expectExpr(p);
+ SLICE_APPEND(AstNodeIndex, &p->scratch, elem);
+ if (p->token_tags[p->tok_i] == TOKEN_COMMA)
+ p->tok_i++;
+ else if (p->token_tags[p->tok_i] == TOKEN_R_BRACE) {
+ p->tok_i++;
+ break;
+ } else {
+ fprintf(stderr, "parseInitList: expected , or } in array init\n");
+ exit(1);
+ }
+ }
+
+ const bool comma = p->token_tags[p->tok_i - 2] == TOKEN_COMMA;
+ const uint32_t elems_len = p->scratch.len - scratch_top.old_len;
+ if (lhs == 0) {
+ // Anonymous array init: .{a, b, ...}
+ switch (elems_len) {
+ case 0:
+ case 1:
+ case 2:
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = comma ? AST_NODE_ARRAY_INIT_DOT_TWO_COMMA
+ : AST_NODE_ARRAY_INIT_DOT_TWO,
+ .main_token = lbrace,
+ .data = {
+ .lhs = elems_len >= 1
+ ? p->scratch.arr[scratch_top.old_len]
+ : 0,
+ .rhs = elems_len >= 2
+ ? p->scratch.arr[scratch_top.old_len + 1]
+ : 0,
+ },
+ });
+ default:;
+ const AstSubRange span = listToSpan(
+ p, &p->scratch.arr[scratch_top.old_len], elems_len);
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = comma ? AST_NODE_ARRAY_INIT_DOT_COMMA
+ : AST_NODE_ARRAY_INIT_DOT,
+ .main_token = lbrace,
+ .data = { .lhs = span.start, .rhs = span.end },
+ });
+ }
+ }
+ // Named init: X{a, b, ...}
+ switch (elems_len) {
+ case 0:
+ // Empty init X{} — treat as struct init
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = AST_NODE_STRUCT_INIT_ONE,
+ .main_token = lbrace,
+ .data = { .lhs = lhs, .rhs = 0 },
+ });
+ case 1:
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = comma ? AST_NODE_ARRAY_INIT_ONE_COMMA
+ : AST_NODE_ARRAY_INIT_ONE,
+ .main_token = lbrace,
+ .data = {
+ .lhs = lhs,
+ .rhs = p->scratch.arr[scratch_top.old_len],
+ },
+ });
+ default:;
+ const AstSubRange span
+ = listToSpan(p, &p->scratch.arr[scratch_top.old_len], elems_len);
+ return addNode(&p->nodes,
+ (AstNodeItem) {
+ .tag = comma ? AST_NODE_ARRAY_INIT_COMMA
+ : AST_NODE_ARRAY_INIT,
+ .main_token = lbrace,
+ .data = {
+ .lhs = lhs,
+ .rhs = addExtra(p,
+ (AstNodeIndex[]) { span.start, span.end }, 2),
+ },
+ });
+ }
+}
+
static AstNodeIndex parseCurlySuffixExpr(Parser* p) {
const AstNodeIndex lhs = parseTypeExpr(p);
if (lhs == 0)
@@ -996,9 +1218,7 @@ static AstNodeIndex parseCurlySuffixExpr(Parser* p) {
if (lbrace == null_token)
return lhs;
- fprintf(stderr, "parseCurlySuffixExpr is not implemented\n");
- exit(1);
- return 0; // tcc
+ return parseInitList(p, lhs, lbrace);
}
typedef struct {
diff --git a/parser_test.zig b/parser_test.zig
@@ -310,9 +310,13 @@ fn zigData(tag: Ast.Node.Tag, lhs: u32, rhs: u32) Ast.Node.Data {
.struct_init_one_comma,
.container_field_init,
.aligned_var_decl,
+ => .{ .node_and_opt_node = .{ toIndex(lhs), toOptIndex(rhs) } },
+
+ // .node_and_node (array_init_one uses node_and_node, not
+ // node_and_opt_node)
.array_init_one,
.array_init_one_comma,
- => .{ .node_and_opt_node = .{ toIndex(lhs), toOptIndex(rhs) } },
+ => .{ .node_and_node = .{ toIndex(lhs), toIndex(rhs) } },
// .opt_node_and_node
.ptr_type_aligned,
@@ -932,3 +936,93 @@ test "zig fmt: array types last token" {
\\
);
}
+
+test "zig fmt: anon struct literal 0 element" {
+ try testCanonical(
+ \\test {
+ \\ const x = .{};
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: anon struct literal 1 element" {
+ try testCanonical(
+ \\test {
+ \\ const x = .{ .a = b };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: anon struct literal 1 element comma" {
+ try testCanonical(
+ \\test {
+ \\ const x = .{
+ \\ .a = b,
+ \\ };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: anon struct literal 2 element" {
+ try testCanonical(
+ \\test {
+ \\ const x = .{ .a = b, .c = d };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: anon struct literal 2 element comma" {
+ try testCanonical(
+ \\test {
+ \\ const x = .{
+ \\ .a = b,
+ \\ .c = d,
+ \\ };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: anon struct literal 3 element" {
+ try testCanonical(
+ \\test {
+ \\ const x = .{ .a = b, .c = d, .e = f };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: anon struct literal 3 element comma" {
+ try testCanonical(
+ \\test {
+ \\ const x = .{
+ \\ .a = b,
+ \\ .c = d,
+ \\ .e = f,
+ \\ };
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: struct literal 0 element" {
+ try testCanonical(
+ \\test {
+ \\ const x = X{};
+ \\}
+ \\
+ );
+}
+
+test "zig fmt: struct literal 1 element" {
+ try testCanonical(
+ \\test {
+ \\ const x = X{ .a = b };
+ \\}
+ \\
+ );
+}