authorgravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-06-15 13:58:22+02:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-07-08 11:48:58+02:00
log27765bb87f1591aac40c80dcd9759fc15d49eebf
treee9fd5095f87918b7e0eb77f88841dd2cd78c2fa4
parente539f7bbebc8016ea05b9203c8be371db8b2e90d
signaturelock-open Commit is signed but in an unrecognized format.

grammar: require newline before doc comment

This matches the behavior of Parse.zig Original motivation: 95b95ea33e4b595d2fc6fbea850694b79f27fe55

3 files changed, 110 insertions(+), 4 deletions(-)

doc/langref/grammar.peg+8-2
......@@ -1,4 +1,4 @@
1Root <- ContainerMembers skip eof
1Root <- initial_doc_comment? ContainerMembers skip eof
22
33# *** Top level ***
44ContainerMembers <- container_doc_comment? ContainerDeclaration* (ContainerField COMMA)* (ContainerField / ContainerDeclaration*)
......@@ -446,14 +446,20 @@ string_char
446446 <- '\\"'
447447 / !["] non_control_utf8
448448
449
450# We forbid same-line doc comments to disambiguate the mapping to e.g. struct
451# fields for documentation generation tooling. However, this requires a special
452# case in the grammar to handle a doc comment right at the beginning of a file.
453initial_doc_comment <- (skip '///' non_control_utf8* newline)+ skip !'//!' !KEYWORD_test !KEYWORD_comptime !eof
449454container_doc_comment <- (skip '//!' non_control_utf8* newline)+
450doc_comment <- (skip '///' non_control_utf8* newline)+
455doc_comment <- skip_require_newline (skip '///' non_control_utf8* newline)+
451456line_comment
452457 <- '//' ![!/] non_control_utf8* newline
453458 / '////' non_control_utf8* newline
454459line_string <- '\\\\' non_control_utf8* newline
455460newline <- "\n" / "\r\n" / eof
456461skip <- ([ \n\t\r] / line_comment)*
462skip_require_newline <- [ \t\r]* ([\n] / line_comment) skip
457463pre_op_white <- ([ \n\t\r] / line_comment)+
458464post_op_white <- [ \n\t\r] skip
459465
lib/std/zig/parser_fuzz.zig+12
......@@ -25,6 +25,7 @@ test "operator whitespace" {
2525 \\}
2626 );
2727}
28
2829// Found using AFL++
2930test "doc comment or division operator" {
3031 try checkAgainstOracle("0=0///\n0");
......@@ -36,6 +37,17 @@ test "double ampersand" {
3637 try checkAgainstOracle("test{&&0;}"); // ok
3738}
3839
40// Found using AFL++
41test "newline required before doc comment not at start of file" {
42 try checkAgainstOracle("0,///\n0"); // error
43 try checkAgainstOracle("///\n0"); // ok
44 try checkAgainstOracle(" ///\n0"); // ok
45 try checkAgainstOracle("\n///\n0"); // ok
46 try checkAgainstOracle("///"); // error
47 try checkAgainstOracle("///\n//!");
48 try checkAgainstOracle("///\ntest {}");
49}
50
3951fn checkAgainstOracle(source: [:0]const u8) !void {
4052 var fba_buf: [1 << 18]u8 = undefined;
4153 var fba: std.heap.FixedBufferAllocator = .init(&fba_buf);
lib/std/zig/parser_generated_oracle.zig+90-2
......@@ -16,7 +16,7 @@ const Parser = struct {
1616 pub fn parseRoot(p: *Parser) bool {
1717 return blk_0: {
1818 const pos_0 = p.i;
19 if (p.parseContainerMembers() and p.parseskip() and p.parseeof()) break :blk_0 true;
19 if ((p.parseinitial_doc_comment() or true) and p.parseContainerMembers() and p.parseskip() and p.parseeof()) break :blk_0 true;
2020 p.i = pos_0;
2121 break :blk_0 false;
2222 };
......@@ -2330,6 +2330,60 @@ const Parser = struct {
23302330 break :blk_0 false;
23312331 };
23322332 }
2333 pub fn parseinitial_doc_comment(p: *Parser) bool {
2334 return blk_0: {
2335 const pos_0 = p.i;
2336 if (blk_1: {
2337 var match_1 = false;
2338 while (blk_3: {
2339 const pos_3 = p.i;
2340 if (p.parseskip() and blk_4: {
2341 if (std.mem.startsWith(u8, p.source[p.i..], "///")) {
2342 p.i += 3;
2343 break :blk_4 true;
2344 }
2345 break :blk_4 false;
2346 } and blk_4: {
2347 while (p.parsenon_control_utf8()) {}
2348 break :blk_4 true;
2349 } and p.parsenewline()) break :blk_3 true;
2350 p.i = pos_3;
2351 break :blk_3 false;
2352 }) {
2353 match_1 = true;
2354 }
2355 break :blk_1 match_1;
2356 } and p.parseskip() and blk_1: {
2357 const pos_1 = p.i;
2358 const match_1 = blk_2: {
2359 if (std.mem.startsWith(u8, p.source[p.i..], "//!")) {
2360 p.i += 3;
2361 break :blk_2 true;
2362 }
2363 break :blk_2 false;
2364 };
2365 p.i = pos_1;
2366 break :blk_1 !match_1;
2367 } and blk_1: {
2368 const pos_1 = p.i;
2369 const match_1 = p.parseKEYWORD_test();
2370 p.i = pos_1;
2371 break :blk_1 !match_1;
2372 } and blk_1: {
2373 const pos_1 = p.i;
2374 const match_1 = p.parseKEYWORD_comptime();
2375 p.i = pos_1;
2376 break :blk_1 !match_1;
2377 } and blk_1: {
2378 const pos_1 = p.i;
2379 const match_1 = p.parseeof();
2380 p.i = pos_1;
2381 break :blk_1 !match_1;
2382 }) break :blk_0 true;
2383 p.i = pos_0;
2384 break :blk_0 false;
2385 };
2386 }
23332387 pub fn parsecontainer_doc_comment(p: *Parser) bool {
23342388 return blk_0: {
23352389 const pos_0 = p.i;
......@@ -2361,7 +2415,7 @@ const Parser = struct {
23612415 pub fn parsedoc_comment(p: *Parser) bool {
23622416 return blk_0: {
23632417 const pos_0 = p.i;
2364 if (blk_1: {
2418 if (p.parseskip_require_newline() and blk_1: {
23652419 var match_1 = false;
23662420 while (blk_3: {
23672421 const pos_3 = p.i;
......@@ -2496,6 +2550,40 @@ const Parser = struct {
24962550 break :blk_0 false;
24972551 };
24982552 }
2553 pub fn parseskip_require_newline(p: *Parser) bool {
2554 return blk_0: {
2555 const pos_0 = p.i;
2556 if (blk_1: {
2557 while ((p.i < p.source.len and switch (p.source[p.i]) {
2558 ' '...' ',
2559 '\t'...'\t',
2560 '\r'...'\r',
2561 => blk_2: {
2562 p.i += 1;
2563 break :blk_2 true;
2564 },
2565 else => false,
2566 })) {}
2567 break :blk_1 true;
2568 } and blk_2: {
2569 const pos_2 = p.i;
2570 if ((p.i < p.source.len and switch (p.source[p.i]) {
2571 '\n'...'\n',
2572 => blk_3: {
2573 p.i += 1;
2574 break :blk_3 true;
2575 },
2576 else => false,
2577 })) break :blk_2 true;
2578 p.i = pos_2;
2579 if (p.parseline_comment()) break :blk_2 true;
2580 p.i = pos_2;
2581 break :blk_2 false;
2582 } and p.parseskip()) break :blk_0 true;
2583 p.i = pos_0;
2584 break :blk_0 false;
2585 };
2586 }
24992587 pub fn parsepre_op_white(p: *Parser) bool {
25002588 return blk_0: {
25012589 const pos_0 = p.i;