parser: port pointer modifier tests

Port tests:
- "pointer-to-one with modifiers"
- "pointer-to-many with modifiers"
- "sentinel pointer with modifiers"
- "c pointer with modifiers"
- "slice with modifiers"
- "sentinel slice with modifiers"
- "allowzero pointer"

Implement in parser.c:
- parsePtrModifiersAndType: shared pointer modifier parsing with
  align(expr:expr:expr) bit-range, addrspace, sentinel support
- ptr_type, ptr_type_bit_range nodes with proper OptionalIndex
  encoding via global OPT() macro
- Refactor * and [*] pointer type parsing to use shared code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-02-10 17:40:48 +00:00
parent 508a94dd33
commit a4d9e12498
2 changed files with 227 additions and 63 deletions

231
parser.c
View File

@@ -11,6 +11,9 @@
const AstNodeIndex null_node = 0;
const AstTokenIndex null_token = ~(AstTokenIndex)(0);
// OPT encodes a node index as OptionalIndex: 0 → ~0 (none)
#define OPT(x) ((x) == 0 ? ~(AstNodeIndex)0 : (x))
typedef struct {
uint32_t len;
AstNodeIndex lhs;
@@ -790,6 +793,95 @@ static AstNodeIndex parseErrorUnionExpr(Parser* p) {
});
}
// parsePtrModifiersAndType parses pointer modifiers (allowzero, align,
// addrspace, const, volatile, sentinel) and the child type for a pointer
// started at main_token.
static AstNodeIndex parsePtrModifiersAndType(
Parser* p, AstTokenIndex main_token) {
AstNodeIndex sentinel = 0;
AstNodeIndex align_expr = 0;
AstNodeIndex bit_range_start = 0;
AstNodeIndex bit_range_end = 0;
AstNodeIndex addrspace_expr = 0;
// sentinel: *:0
if (eatToken(p, TOKEN_COLON) != null_token)
sentinel = expectExpr(p);
// allowzero, const, volatile (before align)
while (p->token_tags[p->tok_i] == TOKEN_KEYWORD_CONST
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_VOLATILE
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_ALLOWZERO)
p->tok_i++;
// align(expr) or align(expr:expr:expr)
if (eatToken(p, TOKEN_KEYWORD_ALIGN) != null_token) {
expectToken(p, TOKEN_L_PAREN);
align_expr = expectExpr(p);
if (eatToken(p, TOKEN_COLON) != null_token) {
bit_range_start = expectExpr(p);
expectToken(p, TOKEN_COLON);
bit_range_end = expectExpr(p);
}
expectToken(p, TOKEN_R_PAREN);
}
// addrspace
addrspace_expr = parseAddrSpace(p);
// const, volatile, allowzero (after align/addrspace)
while (p->token_tags[p->tok_i] == TOKEN_KEYWORD_CONST
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_VOLATILE
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_ALLOWZERO)
p->tok_i++;
const AstNodeIndex child_type = parseTypeExpr(p);
if (bit_range_start != 0) {
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE_BIT_RANGE,
.main_token = main_token,
.data = {
.lhs = addExtra(p,
(AstNodeIndex[]) { OPT(sentinel), align_expr,
OPT(addrspace_expr), bit_range_start,
bit_range_end },
5),
.rhs = child_type,
},
});
}
if (addrspace_expr != 0) {
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE,
.main_token = main_token,
.data = {
.lhs = addExtra(p,
(AstNodeIndex[]) { OPT(sentinel), OPT(align_expr),
addrspace_expr },
3),
.rhs = child_type,
},
});
}
if (sentinel != 0) {
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE_SENTINEL,
.main_token = main_token,
.data = { .lhs = sentinel, .rhs = child_type },
});
}
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE_ALIGNED,
.main_token = main_token,
.data = { .lhs = align_expr, .rhs = child_type },
});
}
static AstNodeIndex parseTypeExpr(Parser* p) {
const TokenizerTag tok = p->token_tags[p->tok_i];
switch (tok) {
@@ -806,39 +898,7 @@ static AstNodeIndex parseTypeExpr(Parser* p) {
exit(1);
case TOKEN_ASTERISK: {
const AstTokenIndex asterisk = nextToken(p);
const AstNodeIndex align_expr = parseByteAlign(p);
const AstNodeIndex sentinel
= eatToken(p, TOKEN_COLON) != null_token ? parseExpr(p) : 0;
// const/volatile/allowzero are pointer modifiers consumed here.
// They are not stored in the AST node; the renderer re-derives
// them from token positions.
while (p->token_tags[p->tok_i] == TOKEN_KEYWORD_CONST
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_VOLATILE
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_ALLOWZERO)
p->tok_i++;
const AstNodeIndex child_type = parseTypeExpr(p);
if (sentinel != 0) {
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE_SENTINEL,
.main_token = asterisk,
.data = { .lhs = sentinel, .rhs = child_type },
});
}
if (align_expr != 0) {
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE_ALIGNED,
.main_token = asterisk,
.data = { .lhs = align_expr, .rhs = child_type },
});
}
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE_ALIGNED,
.main_token = asterisk,
.data = { .lhs = 0, .rhs = child_type },
});
return parsePtrModifiersAndType(p, asterisk);
}
case TOKEN_ASTERISK_ASTERISK: {
// ** is two nested pointer types sharing the same token
@@ -879,32 +939,54 @@ static AstNodeIndex parseTypeExpr(Parser* p) {
sentinel = expectExpr(p);
}
expectToken(p, TOKEN_R_BRACKET);
// const/volatile/allowzero pointer modifiers
// Reuse shared pointer modifier + type parsing
// If we captured a sentinel from [*:s], temporarily store it
// and let parsePtrModifiersAndType handle the rest.
// But parsePtrModifiersAndType expects sentinel after main
// token via `:`. Since we already consumed it, we need to
// handle this inline.
// allowzero, const, volatile (before align)
while (p->token_tags[p->tok_i] == TOKEN_KEYWORD_CONST
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_VOLATILE
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_ALLOWZERO)
p->tok_i++;
const AstNodeIndex align_expr = parseByteAlign(p);
const AstNodeIndex addrspace_expr = parseAddrSpace(p);
// const/volatile/allowzero again (can appear before or after
// align)
while (p->token_tags[p->tok_i] == TOKEN_KEYWORD_CONST
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_VOLATILE
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_ALLOWZERO)
p->tok_i++;
const AstNodeIndex elem_type = parseTypeExpr(p);
if (sentinel != 0) {
if (addrspace_expr != 0) {
fprintf(stderr,
"parseTypeExpr: [*:s] with addrspace not "
"implemented\n");
exit(1);
AstNodeIndex align_expr = 0;
AstNodeIndex bit_range_start = 0;
AstNodeIndex bit_range_end = 0;
if (eatToken(p, TOKEN_KEYWORD_ALIGN) != null_token) {
expectToken(p, TOKEN_L_PAREN);
align_expr = expectExpr(p);
if (eatToken(p, TOKEN_COLON) != null_token) {
bit_range_start = expectExpr(p);
expectToken(p, TOKEN_COLON);
bit_range_end = expectExpr(p);
}
expectToken(p, TOKEN_R_PAREN);
}
const AstNodeIndex addrspace_expr = parseAddrSpace(p);
while (p->token_tags[p->tok_i] == TOKEN_KEYWORD_CONST
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_VOLATILE
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_ALLOWZERO)
p->tok_i++;
const AstNodeIndex elem_type = parseTypeExpr(p);
if (bit_range_start != 0) {
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE_SENTINEL,
.tag = AST_NODE_PTR_TYPE_BIT_RANGE,
.main_token = lbracket,
.data = { .lhs = sentinel, .rhs = elem_type },
.data = {
.lhs = addExtra(p,
(AstNodeIndex[]) { OPT(sentinel),
align_expr, OPT(addrspace_expr),
bit_range_start, bit_range_end },
5),
.rhs = elem_type,
},
});
}
if (addrspace_expr != 0) {
@@ -914,13 +996,21 @@ static AstNodeIndex parseTypeExpr(Parser* p) {
.main_token = lbracket,
.data = {
.lhs = addExtra(p,
(AstNodeIndex[]) {
0, align_expr, addrspace_expr },
(AstNodeIndex[]) { OPT(sentinel),
OPT(align_expr), addrspace_expr },
3),
.rhs = elem_type,
},
});
}
if (sentinel != 0) {
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE_SENTINEL,
.main_token = lbracket,
.data = { .lhs = sentinel, .rhs = elem_type },
});
}
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE_ALIGNED,
@@ -933,16 +1023,34 @@ static AstNodeIndex parseTypeExpr(Parser* p) {
= eatToken(p, TOKEN_COLON) != null_token ? expectExpr(p) : 0;
expectToken(p, TOKEN_R_BRACKET);
if (len_expr == 0) {
// Slice type: []T or [:s]T
// const/volatile/allowzero are pointer modifiers consumed here.
// They are not stored in the AST node; the renderer re-derives
// them from token positions.
// Slice type: []T or [:s]T — reuse shared modifier parsing
// allowzero, const, volatile
while (p->token_tags[p->tok_i] == TOKEN_KEYWORD_CONST
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_VOLATILE
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_ALLOWZERO)
p->tok_i++;
const AstNodeIndex align_expr = parseByteAlign(p);
const AstNodeIndex addrspace_expr = parseAddrSpace(p);
while (p->token_tags[p->tok_i] == TOKEN_KEYWORD_CONST
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_VOLATILE
|| p->token_tags[p->tok_i] == TOKEN_KEYWORD_ALLOWZERO)
p->tok_i++;
const AstNodeIndex elem_type = parseTypeExpr(p);
if (sentinel != 0) {
if (addrspace_expr != 0) {
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE,
.main_token = lbracket,
.data = {
.lhs = addExtra(p,
(AstNodeIndex[]) { OPT(sentinel),
OPT(align_expr), addrspace_expr },
3),
.rhs = elem_type,
},
});
}
if (sentinel != 0 && align_expr == 0) {
return addNode(&p->nodes,
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE_SENTINEL,
@@ -954,7 +1062,7 @@ static AstNodeIndex parseTypeExpr(Parser* p) {
(AstNodeItem) {
.tag = AST_NODE_PTR_TYPE_ALIGNED,
.main_token = lbracket,
.data = { .lhs = 0, .rhs = elem_type },
.data = { .lhs = align_expr, .rhs = elem_type },
});
}
// Array type: [N]T or [N:s]T
@@ -1102,9 +1210,7 @@ static AstNodeIndex parseFnProto(Parser* p) {
}
}
// Complex fn proto with align/section/callconv/addrspace
// Extra data fields are OptionalIndex: 0 → ~0 (none)
#define OPT(x) ((x) == 0 ? ~(AstNodeIndex)0 : (x))
// Complex fn proto with align/section/callconv/addrspace
switch (params.tag) {
case SMALL_SPAN_ZERO_OR_ONE:
return setNode(p, fn_proto_index,
@@ -1138,7 +1244,6 @@ static AstNodeIndex parseFnProto(Parser* p) {
},
});
}
#undef OPT
return 0; // tcc
}

View File

@@ -1864,6 +1864,65 @@ test "zig fmt: C pointers" {
);
}
test "zig fmt: pointer-to-one with modifiers" {
try testCanonical(
\\const x: *u32 = undefined;
\\const y: *allowzero align(8) addrspace(.generic) const volatile u32 = undefined;
\\const z: *allowzero align(8:4:2) addrspace(.generic) const volatile u32 = undefined;
\\
);
}
test "zig fmt: pointer-to-many with modifiers" {
try testCanonical(
\\const x: [*]u32 = undefined;
\\const y: [*]allowzero align(8) addrspace(.generic) const volatile u32 = undefined;
\\const z: [*]allowzero align(8:4:2) addrspace(.generic) const volatile u32 = undefined;
\\
);
}
test "zig fmt: sentinel pointer with modifiers" {
try testCanonical(
\\const x: [*:42]u32 = undefined;
\\const y: [*:42]allowzero align(8) addrspace(.generic) const volatile u32 = undefined;
\\const y: [*:42]allowzero align(8:4:2) addrspace(.generic) const volatile u32 = undefined;
\\
);
}
test "zig fmt: c pointer with modifiers" {
try testCanonical(
\\const x: [*c]u32 = undefined;
\\const y: [*c]allowzero align(8) addrspace(.generic) const volatile u32 = undefined;
\\const z: [*c]allowzero align(8:4:2) addrspace(.generic) const volatile u32 = undefined;
\\
);
}
test "zig fmt: slice with modifiers" {
try testCanonical(
\\const x: []u32 = undefined;
\\const y: []allowzero align(8) addrspace(.generic) const volatile u32 = undefined;
\\
);
}
test "zig fmt: sentinel slice with modifiers" {
try testCanonical(
\\const x: [:42]u32 = undefined;
\\const y: [:42]allowzero align(8) addrspace(.generic) const volatile u32 = undefined;
\\
);
}
test "zig fmt: allowzero pointer" {
try testCanonical(
\\const T = [*]allowzero const u8;
\\
);
}
test "zig fmt: sentinel-terminated array type" {
try testCanonical(
\\pub fn cStrToPrefixedFileW(s: [*:0]const u8) ![PATH_MAX_WIDE:0]u16 {