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...@@ -559,12 +559,6 @@ pub fn renderError(tree: Ast, parse_error: Error, w: *Writer) Writer.Error!void
559 .var_const_decl => {559 .var_const_decl => {
560 return w.writeAll("use 'var' or 'const' to declare variable");560 return w.writeAll("use 'var' or 'const' to declare variable");
561 },561 },
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
569 .invalid_byte => {563 .invalid_byte => {
570 const tok_slice = tree.source[tree.tokens.items(.start)[parse_error.token]..];564 const tok_slice = tree.source[tree.tokens.items(.start)[parse_error.token]..];
...@@ -2889,8 +2883,6 @@ pub const Error = struct {...@@ -2889,8 +2883,6 @@ pub const Error = struct {
2889 expected_var_const,2883 expected_var_const,
2890 wrong_equal_var_decl,2884 wrong_equal_var_decl,
2891 var_const_decl,2885 var_const_decl,
2892 extra_for_capture,
2893 for_input_not_captured,
28942886
2895 zig_style_container,2887 zig_style_container,
2896 previous_field,2888 previous_field,
lib/std/zig/AstGen.zig+7
...@@ -6708,6 +6708,9 @@ fn forExpr(...@@ -6708,6 +6708,9 @@ fn forExpr(
6708 for (for_full.ast.inputs, indexables, lens) |input, *indexable_ref, *len_refs| {6708 for (for_full.ast.inputs, indexables, lens) |input, *indexable_ref, *len_refs| {
6709 const capture_is_ref = tree.tokenTag(capture_token) == .asterisk;6709 const capture_is_ref = tree.tokenTag(capture_token) == .asterisk;
6710 const ident_tok = capture_token + @intFromBool(capture_is_ref);6710 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 }
6711 const is_discard = mem.eql(u8, tree.tokenSlice(ident_tok), "_");6714 const is_discard = mem.eql(u8, tree.tokenSlice(ident_tok), "_");
67126715
6713 if (is_discard and capture_is_ref) {6716 if (is_discard and capture_is_ref) {
...@@ -6750,6 +6753,10 @@ fn forExpr(...@@ -6750,6 +6753,10 @@ fn forExpr(
6750 len_refs.* = .{ indexable, .none };6753 len_refs.* = .{ indexable, .none };
6751 }6754 }
6752 }6755 }
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 }
6753 }6760 }
67546761
6755 if (!any_len_checks) {6762 if (!any_len_checks) {
lib/std/zig/Parse.zig+1-13
...@@ -2106,16 +2106,9 @@ fn forPrefix(p: *Parse) Error!usize {...@@ -2106,16 +2106,9 @@ fn forPrefix(p: *Parse) Error!usize {
2106 return inputs;2106 return inputs;
2107 };2107 };
21082108
2109 var warned_excess = false;
2110 var captures: u32 = 0;
2111 while (true) {2109 while (true) {
2112 _ = p.eatToken(.asterisk);2110 _ = p.eatToken(.asterisk);
2113 const identifier = try p.expectToken(.identifier);2111 _ = 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 }
2119 switch (p.tokenTag(p.tok_i)) {2112 switch (p.tokenTag(p.tok_i)) {
2120 .comma => p.tok_i += 1,2113 .comma => p.tok_i += 1,
2121 .pipe => {2114 .pipe => {
...@@ -2128,11 +2121,6 @@ fn forPrefix(p: *Parse) Error!usize {...@@ -2128,11 +2121,6 @@ fn forPrefix(p: *Parse) Error!usize {
2128 if (p.eatToken(.pipe)) |_| break;2121 if (p.eatToken(.pipe)) |_| break;
2129 }2122 }
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 }
2136 return inputs;2124 return inputs;
2137}2125}
21382126
lib/std/zig/parser_fuzz.zig+5
...@@ -52,6 +52,11 @@ test "newline required before doc comment not at start of file" {...@@ -52,6 +52,11 @@ test "newline required before doc comment not at start of file" {
52 try checkAgainstOracle("///\ntest {}");52 try checkAgainstOracle("///\ntest {}");
53}53}
5454
55// Found using AFL++
56test "extra capture in for loop" {
57 try checkAgainstOracle("for(0)|t,r|0");
58}
59
55fn checkAgainstOracle(source: [:0]const u8) !void {60fn checkAgainstOracle(source: [:0]const u8) !void {
56 var fba_buf: [1 << 18]u8 = undefined;61 var fba_buf: [1 << 18]u8 = undefined;
57 var fba: std.heap.FixedBufferAllocator = .init(&fba_buf);62 var fba: std.heap.FixedBufferAllocator = .init(&fba_buf);