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 @@...@@ -1,4 +1,4 @@
1Root <- ContainerMembers skip eof1Root <- initial_doc_comment? ContainerMembers skip eof
22
3# *** Top level ***3# *** Top level ***
4ContainerMembers <- container_doc_comment? ContainerDeclaration* (ContainerField COMMA)* (ContainerField / ContainerDeclaration*)4ContainerMembers <- container_doc_comment? ContainerDeclaration* (ContainerField COMMA)* (ContainerField / ContainerDeclaration*)
...@@ -446,14 +446,20 @@ string_char...@@ -446,14 +446,20 @@ string_char
446 <- '\\"'446 <- '\\"'
447 / !["] non_control_utf8447 / !["] 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
449container_doc_comment <- (skip '//!' non_control_utf8* newline)+454container_doc_comment <- (skip '//!' non_control_utf8* newline)+
450doc_comment <- (skip '///' non_control_utf8* newline)+455doc_comment <- skip_require_newline (skip '///' non_control_utf8* newline)+
451line_comment456line_comment
452 <- '//' ![!/] non_control_utf8* newline457 <- '//' ![!/] non_control_utf8* newline
453 / '////' non_control_utf8* newline458 / '////' non_control_utf8* newline
454line_string <- '\\\\' non_control_utf8* newline459line_string <- '\\\\' non_control_utf8* newline
455newline <- "\n" / "\r\n" / eof460newline <- "\n" / "\r\n" / eof
456skip <- ([ \n\t\r] / line_comment)*461skip <- ([ \n\t\r] / line_comment)*
462skip_require_newline <- [ \t\r]* ([\n] / line_comment) skip
457pre_op_white <- ([ \n\t\r] / line_comment)+463pre_op_white <- ([ \n\t\r] / line_comment)+
458post_op_white <- [ \n\t\r] skip464post_op_white <- [ \n\t\r] skip
459465
lib/std/zig/parser_fuzz.zig+12
...@@ -25,6 +25,7 @@ test "operator whitespace" {...@@ -25,6 +25,7 @@ test "operator whitespace" {
25 \\}25 \\}
26 );26 );
27}27}
28
28// Found using AFL++29// Found using AFL++
29test "doc comment or division operator" {30test "doc comment or division operator" {
30 try checkAgainstOracle("0=0///\n0");31 try checkAgainstOracle("0=0///\n0");
...@@ -36,6 +37,17 @@ test "double ampersand" {...@@ -36,6 +37,17 @@ test "double ampersand" {
36 try checkAgainstOracle("test{&&0;}"); // ok37 try checkAgainstOracle("test{&&0;}"); // ok
37}38}
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
39fn checkAgainstOracle(source: [:0]const u8) !void {51fn checkAgainstOracle(source: [:0]const u8) !void {
40 var fba_buf: [1 << 18]u8 = undefined;52 var fba_buf: [1 << 18]u8 = undefined;
41 var fba: std.heap.FixedBufferAllocator = .init(&fba_buf);53 var fba: std.heap.FixedBufferAllocator = .init(&fba_buf);
lib/std/zig/parser_generated_oracle.zig+90-2
...@@ -16,7 +16,7 @@ const Parser = struct {...@@ -16,7 +16,7 @@ const Parser = struct {
16 pub fn parseRoot(p: *Parser) bool {16 pub fn parseRoot(p: *Parser) bool {
17 return blk_0: {17 return blk_0: {
18 const pos_0 = p.i;18 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;
20 p.i = pos_0;20 p.i = pos_0;
21 break :blk_0 false;21 break :blk_0 false;
22 };22 };
...@@ -2330,6 +2330,60 @@ const Parser = struct {...@@ -2330,6 +2330,60 @@ const Parser = struct {
2330 break :blk_0 false;2330 break :blk_0 false;
2331 };2331 };
2332 }2332 }
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 }
2333 pub fn parsecontainer_doc_comment(p: *Parser) bool {2387 pub fn parsecontainer_doc_comment(p: *Parser) bool {
2334 return blk_0: {2388 return blk_0: {
2335 const pos_0 = p.i;2389 const pos_0 = p.i;
...@@ -2361,7 +2415,7 @@ const Parser = struct {...@@ -2361,7 +2415,7 @@ const Parser = struct {
2361 pub fn parsedoc_comment(p: *Parser) bool {2415 pub fn parsedoc_comment(p: *Parser) bool {
2362 return blk_0: {2416 return blk_0: {
2363 const pos_0 = p.i;2417 const pos_0 = p.i;
2364 if (blk_1: {2418 if (p.parseskip_require_newline() and blk_1: {
2365 var match_1 = false;2419 var match_1 = false;
2366 while (blk_3: {2420 while (blk_3: {
2367 const pos_3 = p.i;2421 const pos_3 = p.i;
...@@ -2496,6 +2550,40 @@ const Parser = struct {...@@ -2496,6 +2550,40 @@ const Parser = struct {
2496 break :blk_0 false;2550 break :blk_0 false;
2497 };2551 };
2498 }2552 }
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 }
2499 pub fn parsepre_op_white(p: *Parser) bool {2587 pub fn parsepre_op_white(p: *Parser) bool {
2500 return blk_0: {2588 return blk_0: {
2501 const pos_0 = p.i;2589 const pos_0 = p.i;