authorgravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-06-15 17:09:59+02:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-07-08 11:48:58+02:00
loge21ced91e30d8bc943ebe0a1f1f6d3bc6821740e
tree4655fd293313e5cab24cdefe7b95fc498f92a29a
parentac6930544f812582df10181210e80675ce6a42ee
signaturelock-open Commit is signed but in an unrecognized format.

std: move for loop capture count errors to AstGen

Currently Parse.zig gives a parse error when the number of for loop inputs does not match the number of for loop captures. This property is however both overly complex to specify in the formal grammar and not necessary to make further parsing possible. This eliminates yet another discrepancy between the formal grammar and Parse.zig implementation that has been discovered through fuzzing with AFL++.

4 files changed, 13 insertions(+), 21 deletions(-)

lib/std/zig/Ast.zig-8
......@@ -559,12 +559,6 @@ pub fn renderError(tree: Ast, parse_error: Error, w: *Writer) Writer.Error!void
559559 .var_const_decl => {
560560 return w.writeAll("use 'var' or 'const' to declare variable");
561561 },
562 .extra_for_capture => {
563 return w.writeAll("extra capture in for loop");
564 },
565 .for_input_not_captured => {
566 return w.writeAll("for input is not captured");
567 },
568562
569563 .invalid_byte => {
570564 const tok_slice = tree.source[tree.tokens.items(.start)[parse_error.token]..];
......@@ -2889,8 +2883,6 @@ pub const Error = struct {
28892883 expected_var_const,
28902884 wrong_equal_var_decl,
28912885 var_const_decl,
2892 extra_for_capture,
2893 for_input_not_captured,
28942886
28952887 zig_style_container,
28962888 previous_field,
lib/std/zig/AstGen.zig+7
......@@ -6708,6 +6708,9 @@ fn forExpr(
67086708 for (for_full.ast.inputs, indexables, lens) |input, *indexable_ref, *len_refs| {
67096709 const capture_is_ref = tree.tokenTag(capture_token) == .asterisk;
67106710 const ident_tok = capture_token + @intFromBool(capture_is_ref);
6711 if (tree.tokenTag(ident_tok) != .identifier) {
6712 return astgen.failNode(input, "for input is not captured", .{});
6713 }
67116714 const is_discard = mem.eql(u8, tree.tokenSlice(ident_tok), "_");
67126715
67136716 if (is_discard and capture_is_ref) {
......@@ -6750,6 +6753,10 @@ fn forExpr(
67506753 len_refs.* = .{ indexable, .none };
67516754 }
67526755 }
6756 // There may or may not be a trailing comma after the final capture
6757 if (tree.tokenTag(capture_token) != .pipe and tree.tokenTag(capture_token - 1) != .pipe) {
6758 return astgen.failTok(capture_token, "extra capture in for loop", .{});
6759 }
67536760 }
67546761
67556762 if (!any_len_checks) {
lib/std/zig/Parse.zig+1-13
......@@ -2106,16 +2106,9 @@ fn forPrefix(p: *Parse) Error!usize {
21062106 return inputs;
21072107 };
21082108
2109 var warned_excess = false;
2110 var captures: u32 = 0;
21112109 while (true) {
21122110 _ = p.eatToken(.asterisk);
2113 const identifier = try p.expectToken(.identifier);
2114 captures += 1;
2115 if (captures > inputs and !warned_excess) {
2116 try p.warnMsg(.{ .tag = .extra_for_capture, .token = identifier });
2117 warned_excess = true;
2118 }
2111 _ = try p.expectToken(.identifier);
21192112 switch (p.tokenTag(p.tok_i)) {
21202113 .comma => p.tok_i += 1,
21212114 .pipe => {
......@@ -2128,11 +2121,6 @@ fn forPrefix(p: *Parse) Error!usize {
21282121 if (p.eatToken(.pipe)) |_| break;
21292122 }
21302123
2131 if (captures < inputs) {
2132 const index = p.scratch.items.len - captures;
2133 const input = p.nodeMainToken(p.scratch.items[index]);
2134 try p.warnMsg(.{ .tag = .for_input_not_captured, .token = input });
2135 }
21362124 return inputs;
21372125}
21382126
lib/std/zig/parser_fuzz.zig+5
......@@ -52,6 +52,11 @@ test "newline required before doc comment not at start of file" {
5252 try checkAgainstOracle("///\ntest {}");
5353}
5454
55// Found using AFL++
56test "extra capture in for loop" {
57 try checkAgainstOracle("for(0)|t,r|0");
58}
59
5560fn checkAgainstOracle(source: [:0]const u8) !void {
5661 var fba_buf: [1 << 18]u8 = undefined;
5762 var fba: std.heap.FixedBufferAllocator = .init(&fba_buf);