From 7e4d15b0c45858ebd24522676c20fc1cda5306ba Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 20 Aug 2026 15:46:49 -0700 Subject: [PATCH 1/6] Maker: more helpful CLI text on wrong enum tag provided --- lib/compiler/Maker.zig | 46 ++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index b5132c6ad05787c9318d649beb2549b7282de07f..d32e3e15ad4f9bb37bf6fb9ccb358c9b1ea9f7c4 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -292,9 +292,7 @@ pub fn main(init: process.Init.Minimal) !void { try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len)); configure_argv.appendAssumeCapacity(arg); } else if (mem.eql(u8, arg, "--color")) { - const next_arg = nextArgOrFatal(args, &arg_i); - color = stringToEnum(Color, next_arg) orelse - fatalWithHint("expected [auto|on|off] found {q}", .{next_arg}); + color = nextEnumArg(args, &arg_i, Color); try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len)); configure_argv.appendAssumeCapacity(try arena.print("--color={t}", .{color})); @@ -401,25 +399,11 @@ pub fn main(init: process.Init.Minimal) !void { } else if (mem.eql(u8, arg, "--libc")) { graph.libc_file = nextArgOrFatal(args, &arg_i); } else if (mem.eql(u8, arg, "--error-style")) { - const next_arg = nextArg(args, &arg_i) orelse - fatalWithHint("expected style after {q}", .{arg}); - error_style = stringToEnum(ErrorStyle, next_arg) orelse { - fatalWithHint("expected style after {q}, found {q}", .{ arg, next_arg }); - }; + error_style = nextEnumArg(args, &arg_i, ErrorStyle); } else if (mem.eql(u8, arg, "--multiline-errors")) { - const next_arg = nextArg(args, &arg_i) orelse - fatalWithHint("expected style after {q}", .{arg}); - multiline_errors = stringToEnum(MultilineErrors, next_arg) orelse { - fatalWithHint("expected style after {q}, found {q}", .{ arg, next_arg }); - }; + multiline_errors = nextEnumArg(args, &arg_i, MultilineErrors); } else if (mem.eql(u8, arg, "--summary")) { - const next_arg = nextArg(args, &arg_i) orelse - fatalWithHint("expected [all|new|failures|line|none] after {q}", .{arg}); - summary = stringToEnum(Summary, next_arg) orelse { - fatalWithHint("expected [all|new|failures|line|none] after {q}, found {q}", .{ - arg, next_arg, - }); - }; + summary = nextEnumArg(args, &arg_i, Summary); } else if (mem.cutPrefix(u8, arg, "--seed=")) |rest| { graph.random_seed = parseRandomSeed(rest); } else if (mem.eql(u8, arg, "--build-id")) { @@ -4053,3 +4037,25 @@ fn confPathDepToCachePath( .install_include => @panic("TODO"), }; } + +fn fatalEnumHint(comptime E: type, arg: []const u8, param: ?[]const u8) noreturn { + var buf: [100]u8 = undefined; + var w: Io.Writer = .fixed(&buf); + for (@typeInfo(E).@"enum".field_names) |field_name| { + w.writeAll(field_name) catch unreachable; + w.writeByte('|') catch unreachable; + } + const buffered = w.buffered(); + const enum_options_text = buffered[0 .. buffered.len - 1]; + if (param) |p| { + fatalWithHint("expected [{s}] after {q}; found {q}", .{ enum_options_text, arg, p }); + } else { + fatalWithHint("expected [{s}] after {q}", .{ enum_options_text, arg }); + } +} + +fn nextEnumArg(args: []const []const u8, i: *usize, comptime E: type) E { + const arg = args[i.* - 1]; + const next_arg = nextArg(args, i) orelse fatalEnumHint(E, arg, null); + return stringToEnum(E, next_arg) orelse fatalEnumHint(E, arg, next_arg); +} -- 2.54.0 From f37a9103562116a19c21e3f2f392b9217fca63ab Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 20 Aug 2026 15:49:41 -0700 Subject: [PATCH 2/6] std.zig.stringEscape: relax the escaping rules pass through everything except characters that have escapes in zig language and ascii control characters. --- lib/std/zig.zig | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/std/zig.zig b/lib/std/zig.zig index 81baffb36bc70dd6551b4acfd360d2a811db2629..6f060d0550047ec569ccb14ef12330f7db45ffc6 100644 --- a/lib/std/zig.zig +++ b/lib/std/zig.zig @@ -538,18 +538,24 @@ test fmtChar { } /// Print the string as escaped contents of a double quoted string. +/// +/// The following transformations are made: +/// * escaped: '\n', '\r', '\t', '\\', '"' +/// * hex-encoded: ascii control characters +/// +/// Everything else is passed through unmodified. pub fn stringEscape(bytes: []const u8, w: *Writer) Writer.Error!void { for (bytes) |byte| switch (byte) { + '\t' => try w.writeAll("\\t"), '\n' => try w.writeAll("\\n"), '\r' => try w.writeAll("\\r"), - '\t' => try w.writeAll("\\t"), '\\' => try w.writeAll("\\\\"), '"' => try w.writeAll("\\\""), - ' ', '!', '#'...'[', ']'...'~' => try w.writeByte(byte), - else => { + 0...8, 11, 12, 14...0x1f, 0x7f => { try w.writeAll("\\x"); try w.printInt(byte, 16, .lower, .{ .width = 2, .fill = '0' }); }, + else => try w.writeByte(byte), }; } -- 2.54.0 From 7e012b4f7c08e234b7e13cd7875a3a496dd80956 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 20 Aug 2026 16:23:10 -0700 Subject: [PATCH 3/6] std.Io.Writer: introduce {qf} calls format function and then double quote escapes it --- lib/std/Io/Writer.zig | 17 +++++++++++ lib/std/zig.zig | 68 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig index 7a6f4468964c4b5f8c8a1a6ff86fcd6d7a63a6b4..e7c54d9a52549577aafd34c34a27f648e86e490f 100644 --- a/lib/std/Io/Writer.zig +++ b/lib/std/Io/Writer.zig @@ -1231,6 +1231,18 @@ pub fn printValue( }, else => {}, }, + 'q' => switch (fmt[1]) { + 'f' => { + try w.writeByte('"'); + var buffer: [64]u8 = undefined; + var escaping_writer: std.zig.StringEscapeWriter = .init(w, &buffer); + try value.format(&escaping_writer.writer); + try escaping_writer.writer.flush(); + try w.writeByte('"'); + return; + }, + else => {}, + }, else => {}, }, 3 => if (fmt[0] == 'b' and fmt[1] == '6' and fmt[2] == '4') switch (@typeInfo(T)) { @@ -2143,6 +2155,11 @@ test "{q} format string" { try testing.expectFmt("hello \"i\\tlike\\\"cheese\\x00\\x05cheese\" world", "hello {q} world", .{data}); } +test "{qf} format string" { + const data: []const u8 = "😎"; + try testing.expectFmt("hello \"@\\\"😎\\\"\" world", "hello {qf} world", .{std.zig.fmtId(data)}); +} + fn testPrintIntCase(expected: []const u8, value: anytype, base: u8, case: std.fmt.Case, options: std.fmt.Options) !void { var buffer: [100]u8 = undefined; var w: Writer = .fixed(&buffer); diff --git a/lib/std/zig.zig b/lib/std/zig.zig index 6f060d0550047ec569ccb14ef12330f7db45ffc6..e18a6414c6e9ba130640e061307c68b16ff02456 100644 --- a/lib/std/zig.zig +++ b/lib/std/zig.zig @@ -545,20 +545,76 @@ test fmtChar { /// /// Everything else is passed through unmodified. pub fn stringEscape(bytes: []const u8, w: *Writer) Writer.Error!void { + _ = try stringEscapeCounting(bytes, w); +} + +pub fn stringEscapeCounting(bytes: []const u8, w: *Writer) Writer.Error!usize { + var n: usize = 0; for (bytes) |byte| switch (byte) { - '\t' => try w.writeAll("\\t"), - '\n' => try w.writeAll("\\n"), - '\r' => try w.writeAll("\\r"), - '\\' => try w.writeAll("\\\\"), - '"' => try w.writeAll("\\\""), + '\t' => { + try w.writeAll("\\t"); + n += 2; + }, + '\n' => { + try w.writeAll("\\n"); + n += 2; + }, + '\r' => { + try w.writeAll("\\r"); + n += 2; + }, + '\\' => { + try w.writeAll("\\\\"); + n += 2; + }, + '"' => { + try w.writeAll("\\\""); + n += 2; + }, 0...8, 11, 12, 14...0x1f, 0x7f => { try w.writeAll("\\x"); try w.printInt(byte, 16, .lower, .{ .width = 2, .fill = '0' }); + n += 4; + }, + else => { + try w.writeByte(byte); + n += 1; }, - else => try w.writeByte(byte), }; + return n; } +pub const StringEscapeWriter = struct { + out: *Writer, + writer: Writer, + + pub fn init(out: *Writer, buffer: []u8) @This() { + return .{ + .out = out, + .writer = .{ + .vtable = &.{ .drain = @This().drain }, + .buffer = buffer, + }, + }; + } + + fn drain(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { + const sew: *StringEscapeWriter = @alignCast(@fieldParentPtr("writer", w)); + const out = sew.out; + _ = try stringEscapeCounting(w.buffered(), out); + w.end = 0; + var n: usize = 0; + for (data[0 .. data.len - 1]) |bytes| { + n += try stringEscapeCounting(bytes, out); + } + const pattern = data[data.len - 1]; + for (0..splat) |_| { + n += try stringEscapeCounting(pattern, out); + } + return n; + } +}; + /// Print as escaped contents of a single-quoted string. pub fn charEscape(codepoint: u21, w: *Writer) Writer.Error!void { switch (codepoint) { -- 2.54.0 From 4c655f4672572c522fd41296e0fc8737847d1101 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 20 Aug 2026 16:27:49 -0700 Subject: [PATCH 4/6] std.Io.Writer.print: update doc comments --- lib/std/Io/Writer.zig | 45 ++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig index e7c54d9a52549577aafd34c34a27f648e86e490f..2bc12d27c00acd7b605409e02cadf88b09715197 100644 --- a/lib/std/Io/Writer.zig +++ b/lib/std/Io/Writer.zig @@ -584,36 +584,41 @@ pub fn writeAll(w: *Writer, bytes: []const u8) Error!void { /// required, otherwise the digit following ':' is interpreted as **width**. /// /// **specifier** supports: -/// - `x` and `X`: numeric value in hexadecimal notation, or string in hexadecimal bytes -/// - `s`: +/// - "x" and "X": numeric value in hexadecimal notation, or string in hexadecimal bytes +/// - "s": /// - for pointer-to-many and C pointers of u8, print as a C-string using zero-termination /// - for slices of u8, print the entire slice as a string without zero-termination -/// - `t`: +/// - "t": /// - for enums and tagged unions: prints the tag name /// - for error sets: prints the error name -/// - `b64`: string as standard base64 -/// - `e`: floating point value in scientific notation -/// - `d`: numeric value in decimal notation -/// - `b`: integer value in binary notation -/// - `o`: integer value in octal notation -/// - `c`: integer as an ASCII character. Integer type must have 8 bits at max. -/// - `u`: integer as an UTF-8 sequence. Integer type must have 21 bits at max. -/// - `B`: bytes in SI units (decimal) -/// - `Bi`: bytes in IEC units (binary) -/// - `?`: optional value as either the unwrapped value, or `null`; may be +/// - "b64": string as standard base64 +/// - "e": floating point value in scientific notation +/// - "d": numeric value in decimal notation +/// - "b": integer value in binary notation +/// - "o": integer value in octal notation +/// - "c": integer as an ASCII character. Integer type must have 8 bits at max. +/// - "u": integer as an UTF-8 sequence. Integer type must have 21 bits at max. +/// - "B": bytes in SI units (decimal) +/// - "Bi": bytes in IEC units (binary) +/// - "?": optional value as either the unwrapped value, or `null`; may be /// followed by a format specifier for the underlying value. -/// - `!`: error union value as either the unwrapped value, or the formatted +/// - "!": error union value as either the unwrapped value, or the formatted /// error value; may be followed by a format specifier for the underlying /// value. -/// - `*`: the address of the value instead of the value itself. -/// - `any`: a value of any type using its default format. -/// - `f`: delegates to the `format` method of the type, passing `*Writer` and +/// - "*": the address of the value instead of the value itself. +/// - "any": a value of any type using its default format. +/// - "f": delegates to the `format` method of the type, passing `*Writer` and /// expecting `Error!void` returned. -/// -/// A user type may be a struct, vector, union or enum type. +/// - "q": prints as a double-quote escaped string. Inside the double-quoted +/// string, everything is passed through unmodified, except for the following +/// transformations: +/// - escaped: '\n', '\r', '\t', '\\', '"' +/// - hex-encoded: ASCII control characters +/// - "qf": delegates to the `format` method of the type, while double-quote +/// escaping. /// /// Literal curly braces can be escaped in the format string via doubling, e.g. -/// `{{` or `}}`. +/// "{{" or "}}". pub fn print(w: *Writer, comptime fmt: []const u8, args: anytype) Error!void { const ArgsType = @TypeOf(args); const args_type_info = @typeInfo(ArgsType); -- 2.54.0 From 6ddb6f5759456c319e4305e00bbb5a5a881001ae Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 20 Aug 2026 17:13:29 -0700 Subject: [PATCH 5/6] regenerate parser oracle --- lib/std/zig/parser_generated_oracle.zig | 10 +++++----- tools/gen_parser_oracle.zig | 9 +++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/std/zig/parser_generated_oracle.zig b/lib/std/zig/parser_generated_oracle.zig index 1cd16d6ce1495e8587fd16feb019f23501c90f3d..79de64f7298ad6f141babcea868bdef5cae4c474 100644 --- a/lib/std/zig/parser_generated_oracle.zig +++ b/lib/std/zig/parser_generated_oracle.zig @@ -3073,7 +3073,7 @@ const Parser = struct { return blk_0: { const pos_0 = p.i; if (blk_1: { - if (std.mem.startsWith(u8, p.source[p.i..], "\xef\xbb\xbf")) { + if (std.mem.startsWith(u8, p.source[p.i..], "")) { p.i += 3; break :blk_1 true; } @@ -3145,7 +3145,7 @@ const Parser = struct { return blk_0: { const pos_0 = p.i; if (blk_1: { - if (std.mem.startsWith(u8, p.source[p.i..], "\xf4")) { + if (std.mem.startsWith(u8, p.source[p.i..], "ô")) { p.i += 1; break :blk_1 true; } @@ -3201,7 +3201,7 @@ const Parser = struct { return blk_0: { const pos_0 = p.i; if (blk_1: { - if (std.mem.startsWith(u8, p.source[p.i..], "\xf0")) { + if (std.mem.startsWith(u8, p.source[p.i..], "ð")) { p.i += 1; break :blk_1 true; } @@ -3257,7 +3257,7 @@ const Parser = struct { return blk_0: { const pos_0 = p.i; if (blk_1: { - if (std.mem.startsWith(u8, p.source[p.i..], "\xed")) { + if (std.mem.startsWith(u8, p.source[p.i..], "í")) { p.i += 1; break :blk_1 true; } @@ -3313,7 +3313,7 @@ const Parser = struct { return blk_0: { const pos_0 = p.i; if (blk_1: { - if (std.mem.startsWith(u8, p.source[p.i..], "\xe0")) { + if (std.mem.startsWith(u8, p.source[p.i..], "à")) { p.i += 1; break :blk_1 true; } diff --git a/tools/gen_parser_oracle.zig b/tools/gen_parser_oracle.zig index e20b3bfc15f9e376370778c21061d4363d1dd6ba..866c266d82b6284d506aaa2dbf0ae4d11136c987 100644 --- a/tools/gen_parser_oracle.zig +++ b/tools/gen_parser_oracle.zig @@ -267,17 +267,14 @@ const Generator = struct { const bytes = g.p.strings.items[literal.off..][0..literal.len]; try g.w.print( \\blk_{d}: {{ - \\if (std.mem.startsWith(u8, p.source[p.i..], " - , .{suffix}); - try std.zig.stringEscape(bytes, g.w); - try g.w.print( - \\")) {{ + \\if (std.mem.startsWith(u8, p.source[p.i..], {q})) {{ + \\ \\p.i += {d}; \\ break :blk_{d} true; \\}} \\break :blk_{d} false; \\}} - , .{ bytes.len, suffix, suffix }); + , .{ suffix, bytes, bytes.len, suffix, suffix }); }, .class => |ranges| { try g.w.writeAll("(p.i < p.source.len and switch (p.source[p.i]) {"); -- 2.54.0 From 9296ec14e9efe901b397cf8560f75cacf867f170 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 20 Aug 2026 17:33:16 -0700 Subject: [PATCH 6/6] langref: rewrite Source Encoding section --- doc/langref.html.in | 53 +++++++++++++++------------------------------ 1 file changed, 17 insertions(+), 36 deletions(-) diff --git a/doc/langref.html.in b/doc/langref.html.in index 974acb49716d37308f88150d92c2a6ac3268127c..a0ea833d85a0f16320c71a784a4fd35a8a855f1a 100644 --- a/doc/langref.html.in +++ b/doc/langref.html.in @@ -7274,46 +7274,27 @@ fn readU32Be() u32 {} {#header_close#} {#header_close#} + {#header_open|Source Encoding#} -

Zig source code is encoded in UTF-8. An invalid UTF-8 byte sequence results in a compile error.

-

Throughout all zig source code (including in comments), some code points are never allowed:

+

Zig source code is UTF-8 encoded. Invalid UTF-8 byte sequences are not allowed anywhere.

+

Some code points are never allowed, even in {#link|Comments#}:

    -
  • Ascii control characters, except for U+000a (LF), U+000d (CR), and U+0009 (HT): U+0000 - U+0008, U+000b - U+000c, U+000e - U+0001f, U+007f.
  • -
  • Non-Ascii Unicode line endings: U+0085 (NEL), U+2028 (LS), U+2029 (PS).
  • +
  • ASCII control characters, except for U+000a (LF): U+0000...U+0009, U+000b...U+0001f, U+007f.
  • +
  • Non-ASCII Unicode line endings: U+0085 (NEL), U+2028 (LS), U+2029 (PS).
  • +
  • Byte order marks: U+FEFF (BOM).

- LF (byte value 0x0a, code point U+000a, {#syntax#}'\n'{#endsyntax#}) is the line terminator in Zig source code. - This byte value terminates every line of zig source code except the last line of the file. - It is recommended that non-empty source files end with an empty line, which means the last byte would be 0x0a (LF). -

-

- Each LF may be immediately preceded by a single CR (byte value 0x0d, code point U+000d, {#syntax#}'\r'{#endsyntax#}) - to form a Windows style line ending, but this is discouraged. Note that in multiline strings, CRLF sequences will - be encoded as LF when compiled into a zig program. - A CR in any other context is not allowed. -

-

- HT hard tabs (byte value 0x09, code point U+0009, {#syntax#}'\t'{#endsyntax#}) are interchangeable with - SP spaces (byte value 0x20, code point U+0020, {#syntax#}' '{#endsyntax#}) as a token separator, - but use of hard tabs is discouraged. See {#link|Grammar#}. -

-

- For compatibility with other tools, the compiler ignores a UTF-8-encoded byte order mark (U+FEFF) - if it is the first Unicode code point in the source text. A byte order mark is not allowed anywhere else in the source. -

-

- Note that running zig fmt on a source file will implement all recommendations mentioned here. -

-

- Note that a tool reading Zig source code can make assumptions if the source code is assumed to be correct Zig code. - For example, when identifying the ends of lines, a tool can use a naive search such as /\n/, - or an advanced - search such as /\r\n?|[\n\u0085\u2028\u2029]/, and in either case line endings will be correctly identified. - For another example, when identifying the whitespace before the first token on a line, - a tool can either use a naive search such as /[ \t]/, - or an advanced search such as /\s/, - and in either case whitespace will be correctly identified. -

+ LF (byte value 0x0a, code point U+000a, {#syntax#}'\n'{#endsyntax#}) is + the line terminator in Zig source code. This byte value terminates every + line of Zig source code, including last line of the file. +

+

These conservative rules mean that third party tools reading + already-validated Zig source code may make simplifying assumptions, such + as naively separating lines based on {#syntax#}'\n'{#endsyntax#}. + However, tooling such as zig fmt provides convenience + functionality to convert invalid source encodings to valid source + encodings, for instance by stripping byte order marks and carriage + returns.

{#header_close#} {#header_open|Keyword Reference#} -- 2.54.0