authorgravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-07-07 12:10:33+02:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-07-08 11:49:02+02:00
logd45de6d4bd0052fa0187b0ea4e6c274fded7bdc0
tree324244b3924c22dfe1871f7ff1d1171925dd83bb
parentf2328d520860049f542539e4e28c401970870f84
signaturelock-open Commit is signed but in an unrecognized format.

grammar: fix inconsistency with parser

Consider the new test case (found by AFL++): test{for(0)|t|0,const w=0;} Currently the grammar backtracks after failing to parse this with the ForStatement rule and instead finds that the VarAssignStatement rule is a match. This behavior is not LL(k) and inconsistent with the parser. Add the required negative lookahead to eliminate this case and similar cases.

4 files changed, 57 insertions(+), 25 deletions(-)

doc/langref/grammar.peg+8-2
......@@ -77,7 +77,7 @@ BlockStatement
7777 <- Statement
7878 / KEYWORD_defer BlockExprStatement
7979 / KEYWORD_errdefer BlockExprStatement
80 / !KEYWORD_nosuspend (KEYWORD_comptime !BlockExprPrefix)? VarAssignStatement
80 / !StatementPrefix KEYWORD_comptime? VarAssignStatement
8181
8282Statement
8383 <- IfStatement
......@@ -85,7 +85,13 @@ Statement
8585 / KEYWORD_nosuspend BlockExprStatement
8686 / KEYWORD_comptime BlockExpr
8787 / KEYWORD_suspend BlockExprStatement
88 / (KEYWORD_comptime !BlockExprPrefix)? AssignExpr SEMICOLON
88 / !StatementPrefix KEYWORD_comptime? AssignExpr SEMICOLON
89
90StatementPrefix
91 <- KEYWORD_if
92 / BlockLabel? (LBRACE / KEYWORD_inline? (KEYWORD_for / KEYWORD_while) / KEYWORD_switch)
93 / KEYWORD_nosuspend
94 / KEYWORD_comptime BlockExprPrefix
8995
9096IfStatement
9197 <- IfPrefix BlockExpr (KEYWORD_else Payload? Statement / !KEYWORD_else)
lib/std/zig/Parse.zig+8-2
......@@ -956,7 +956,7 @@ fn expectContainerField(p: *Parse) !Node.Index {
956956/// <- Statement
957957/// / KEYWORD_defer BlockExprStatement
958958/// / KEYWORD_errdefer BlockExprStatement
959/// / (KEYWORD_comptime !BlockExprPrefix)? VarAssignStatement
959/// / !StatementPrefix KEYWORD_comptime? VarAssignStatement
960960///
961961/// Statement
962962/// <- IfStatement
......@@ -964,7 +964,13 @@ fn expectContainerField(p: *Parse) !Node.Index {
964964/// / KEYWORD_nosuspend BlockExprStatement
965965/// / KEYWORD_comptime BlockExpr
966966/// / KEYWORD_suspend BlockExprStatement
967/// / (KEYWORD_comptime !BlockExprPrefix)? AssignExpr SEMICOLON
967/// / !StatementPrefix KEYWORD_comptime? AssignExpr SEMICOLON
968///
969/// StatementPrefix
970/// <- KEYWORD_if
971/// / BlockLabel? (LBRACE / KEYWORD_inline? (KEYWORD_for / KEYWORD_while) / KEYWORD_switch)
972/// / KEYWORD_nosuspend
973/// / KEYWORD_comptime BlockExprPrefix
968974fn expectStatement(p: *Parse, is_block_level: bool) Error!Node.Index {
969975 if (p.eatToken(.keyword_comptime)) |comptime_token| {
970976 const opt_block_expr = try p.parseBlockExpr();
lib/std/zig/parser_fuzz.zig+5
......@@ -153,6 +153,11 @@ test "resume block chained compare ops" {
153153 try checkAgainstOracle("test{resume{0 > 0;} > 0 > 0;}");
154154}
155155
156// Found using AFL++
157test "for multiassign" {
158 try checkAgainstOracle("test{for(0)|t|0,const w=0;}");
159}
160
156161fn checkAgainstOracle(source: [:0]const u8) !void {
157162 var fba_buf: [1 << 18]u8 = undefined;
158163 var fba: std.heap.FixedBufferAllocator = .init(&fba_buf);
lib/std/zig/parser_generated_oracle.zig+36-21
......@@ -270,20 +270,10 @@ const Parser = struct {
270270 p.i = pos_0;
271271 if (blk_1: {
272272 const pos_1 = p.i;
273 const match_1 = try p.parseKEYWORD_nosuspend();
273 const match_1 = try p.parseStatementPrefix();
274274 p.i = pos_1;
275275 break :blk_1 !match_1;
276 } and (blk_3: {
277 const pos_3 = p.i;
278 if (try p.parseKEYWORD_comptime() and blk_4: {
279 const pos_4 = p.i;
280 const match_4 = try p.parseBlockExprPrefix();
281 p.i = pos_4;
282 break :blk_4 !match_4;
283 }) break :blk_3 true;
284 p.i = pos_3;
285 break :blk_3 false;
286 } or true) and try p.parseVarAssignStatement()) break :blk_0 true;
276 } and (try p.parseKEYWORD_comptime() or true) and try p.parseVarAssignStatement()) break :blk_0 true;
287277 p.i = pos_0;
288278 break :blk_0 false;
289279 };
......@@ -301,17 +291,42 @@ const Parser = struct {
301291 p.i = pos_0;
302292 if (try p.parseKEYWORD_suspend() and try p.parseBlockExprStatement()) break :blk_0 true;
303293 p.i = pos_0;
304 if ((blk_3: {
305 const pos_3 = p.i;
306 if (try p.parseKEYWORD_comptime() and blk_4: {
294 if (blk_1: {
295 const pos_1 = p.i;
296 const match_1 = try p.parseStatementPrefix();
297 p.i = pos_1;
298 break :blk_1 !match_1;
299 } and (try p.parseKEYWORD_comptime() or true) and try p.parseAssignExpr() and try p.parseSEMICOLON()) break :blk_0 true;
300 p.i = pos_0;
301 break :blk_0 false;
302 };
303 }
304 pub fn parseStatementPrefix(p: *Parser) Error!bool {
305 return blk_0: {
306 const pos_0 = p.i;
307 if (try p.parseKEYWORD_if()) break :blk_0 true;
308 p.i = pos_0;
309 if ((try p.parseBlockLabel() or true) and blk_2: {
310 const pos_2 = p.i;
311 if (try p.parseLBRACE()) break :blk_2 true;
312 p.i = pos_2;
313 if ((try p.parseKEYWORD_inline() or true) and blk_4: {
307314 const pos_4 = p.i;
308 const match_4 = try p.parseBlockExprPrefix();
315 if (try p.parseKEYWORD_for()) break :blk_4 true;
309316 p.i = pos_4;
310 break :blk_4 !match_4;
311 }) break :blk_3 true;
312 p.i = pos_3;
313 break :blk_3 false;
314 } or true) and try p.parseAssignExpr() and try p.parseSEMICOLON()) break :blk_0 true;
317 if (try p.parseKEYWORD_while()) break :blk_4 true;
318 p.i = pos_4;
319 break :blk_4 false;
320 }) break :blk_2 true;
321 p.i = pos_2;
322 if (try p.parseKEYWORD_switch()) break :blk_2 true;
323 p.i = pos_2;
324 break :blk_2 false;
325 }) break :blk_0 true;
326 p.i = pos_0;
327 if (try p.parseKEYWORD_nosuspend()) break :blk_0 true;
328 p.i = pos_0;
329 if (try p.parseKEYWORD_comptime() and try p.parseBlockExprPrefix()) break :blk_0 true;
315330 p.i = pos_0;
316331 break :blk_0 false;
317332 };