authorgravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-06-15 15:49:31+02:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-07-08 11:48:58+02:00
logac6930544f812582df10181210e80675ce6a42ee
tree5e5121343fa7587a921ef4060f4e7e03f09ec303
parent27765bb87f1591aac40c80dcd9759fc15d49eebf
signaturelock-open Commit is signed but in an unrecognized format.

grammar: fix operator whitespace after multiline string literal

Currently the grammar fails to enforce matching whitespace or lack of whitespace around operators that are immediately preceded by a multiline string literal and terminating newline.

4 files changed, 49 insertions(+), 17 deletions(-)

doc/langref/grammar.peg+5-1
......@@ -457,7 +457,11 @@ line_comment
457457 <- '//' ![!/] non_control_utf8* newline
458458 / '////' non_control_utf8* newline
459459line_string <- '\\\\' non_control_utf8* newline
460newline <- "\n" / "\r\n" / eof
460
461# This uses a positive lookahead rather than consuming input to make e.g.
462# the newline terminating a multiline string literal or doc comment visible
463# to the pre_op_white non-terminal.
464newline <- &("\n" / "\r\n" / eof)
461465skip <- ([ \n\t\r] / line_comment)*
462466skip_require_newline <- [ \t\r]* ([\n] / line_comment) skip
463467pre_op_white <- ([ \n\t\r] / line_comment)+
lib/std/zig/parser_fuzz.zig+4
......@@ -24,6 +24,10 @@ test "operator whitespace" {
2424 \\ _!= 0;
2525 \\}
2626 );
27 try checkAgainstOracle(
28 \\test{{\\
29 \\*0;}}
30 );
2731}
2832
2933// Found using AFL++
lib/std/zig/parser_generated_oracle.zig+25-15
......@@ -2502,23 +2502,33 @@ const Parser = struct {
25022502 return blk_0: {
25032503 const pos_0 = p.i;
25042504 if (blk_1: {
2505 if (std.mem.startsWith(u8, p.source[p.i..], "\n")) {
2506 p.i += 1;
2507 break :blk_1 true;
2508 }
2509 break :blk_1 false;
2510 }) break :blk_0 true;
2511 p.i = pos_0;
2512 if (blk_1: {
2513 if (std.mem.startsWith(u8, p.source[p.i..], "\r\n")) {
2514 p.i += 2;
2515 break :blk_1 true;
2516 }
2517 break :blk_1 false;
2505 const pos_1 = p.i;
2506 const match_1 = blk_3: {
2507 const pos_3 = p.i;
2508 if (blk_4: {
2509 if (std.mem.startsWith(u8, p.source[p.i..], "\n")) {
2510 p.i += 1;
2511 break :blk_4 true;
2512 }
2513 break :blk_4 false;
2514 }) break :blk_3 true;
2515 p.i = pos_3;
2516 if (blk_4: {
2517 if (std.mem.startsWith(u8, p.source[p.i..], "\r\n")) {
2518 p.i += 2;
2519 break :blk_4 true;
2520 }
2521 break :blk_4 false;
2522 }) break :blk_3 true;
2523 p.i = pos_3;
2524 if (p.parseeof()) break :blk_3 true;
2525 p.i = pos_3;
2526 break :blk_3 false;
2527 };
2528 p.i = pos_1;
2529 break :blk_1 match_1;
25182530 }) break :blk_0 true;
25192531 p.i = pos_0;
2520 if (p.parseeof()) break :blk_0 true;
2521 p.i = pos_0;
25222532 break :blk_0 false;
25232533 };
25242534 }
tools/gen_parser_oracle.zig+15-1
......@@ -141,7 +141,21 @@ const Generator = struct {
141141 switch (node.get(g.p)) {
142142 .id => |id| try g.w.print("p.parse{s}()", .{id}),
143143 .expr => try g.genExpr(node),
144 .@"&" => @panic("'&' not supported, unused in Zig's grammar.peg"),
144 .@"&" => |child| {
145 // XXX forbid unbounded lookahead
146 try g.w.print(
147 \\blk_{d}: {{
148 \\const pos_{d} = p.i;
149 \\const match_{d} =
150 , .{ suffix, suffix, suffix });
151 try g.genNode(child);
152 try g.w.print(
153 \\;
154 \\p.i = pos_{d};
155 \\ break :blk_{d} match_{d};
156 \\}}
157 , .{ suffix, suffix, suffix });
158 },
145159 .@"!" => |child| {
146160 // XXX forbid unbounded lookahead
147161 try g.w.print(