diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2020-11-25 15:43:19 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-11-25 15:43:19 -0800 |
| commit | a06afa457928fb7998db3ec9419d38fdbb909706 (patch) | |
| tree | 21a6a5f23dd580417a2401de559fd0208bb207eb /lib | |
| parent | b7b3c1dfaea134a36276786878bf7fe5db63b92b (diff) | |
| parent | 3da6b1218a126c8c6fa043731c9a3c872b42249d (diff) | |
| download | zig-a06afa457928fb7998db3ec9419d38fdbb909706.tar.gz zig-a06afa457928fb7998db3ec9419d38fdbb909706.zip | |
Merge pull request #6411 from LemonBoy/fff
More std.fmt goodness
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/std/fmt.zig | 468 |
1 files changed, 267 insertions, 201 deletions
diff --git a/lib/std/fmt.zig b/lib/std/fmt.zig index 074db49cc5..8ca7db81e5 100644 --- a/lib/std/fmt.zig +++ b/lib/std/fmt.zig @@ -8,6 +8,7 @@ const math = std.math; const assert = std.debug.assert; const mem = std.mem; const unicode = std.unicode; +const meta = std.meta; const builtin = @import("builtin"); const errol = @import("fmt/errol.zig"); const lossyCast = std.math.lossyCast; @@ -27,18 +28,6 @@ pub const FormatOptions = struct { fill: u8 = ' ', }; -fn peekIsAlign(comptime fmt: []const u8) bool { - // Should only be called during a state transition to the format segment. - comptime assert(fmt[0] == ':'); - - inline for (([_]u8{ 1, 2 })[0..]) |i| { - if (fmt.len > i and (fmt[i] == '<' or fmt[i] == '^' or fmt[i] == '>')) { - return true; - } - } - return false; -} - /// Renders fmt string with args, calling output with slices of bytes. /// If `output` returns an error, the error is returned from `format` and /// `output` is not called again. @@ -96,232 +85,285 @@ pub fn format( args: anytype, ) !void { const ArgSetType = u32; - if (@typeInfo(@TypeOf(args)) != .Struct) { - @compileError("Expected tuple or struct argument, found " ++ @typeName(@TypeOf(args))); + + const ArgsType = @TypeOf(args); + // XXX: meta.trait.is(.Struct)(ArgsType) doesn't seem to work... + if (@typeInfo(ArgsType) != .Struct) { + @compileError("Expected tuple or struct argument, found " ++ @typeName(ArgsType)); } - if (args.len > @typeInfo(ArgSetType).Int.bits) { + + const fields_info = meta.fields(ArgsType); + if (fields_info.len > @typeInfo(ArgSetType).Int.bits) { @compileError("32 arguments max are supported per format call"); } - const State = enum { - Start, - Positional, - CloseBrace, - Specifier, - FormatFillAndAlign, - FormatWidth, - FormatPrecision, - }; - - comptime var start_index = 0; - comptime var state = State.Start; - comptime var maybe_pos_arg: ?comptime_int = null; - comptime var specifier_start = 0; - comptime var specifier_end = 0; - comptime var options = FormatOptions{}; comptime var arg_state: struct { next_arg: usize = 0, - used_args: ArgSetType = 0, - args_len: usize = args.len, + used_args: usize = 0, + args_len: usize = fields_info.len, fn hasUnusedArgs(comptime self: *@This()) bool { - return (@popCount(ArgSetType, self.used_args) != self.args_len); + return @popCount(ArgSetType, self.used_args) != self.args_len; } - fn nextArg(comptime self: *@This(), comptime pos_arg: ?comptime_int) comptime_int { - const next_idx = pos_arg orelse blk: { + fn nextArg(comptime self: *@This(), comptime arg_index: ?usize) comptime_int { + const next_index = arg_index orelse init: { const arg = self.next_arg; self.next_arg += 1; - break :blk arg; + break :init arg; }; - if (next_idx >= self.args_len) { + if (next_index >= self.args_len) { @compileError("Too few arguments"); } // Mark this argument as used - self.used_args |= 1 << next_idx; + self.used_args |= 1 << next_index; - return next_idx; + return next_index; } } = .{}; - inline for (fmt) |c, i| { - switch (state) { - .Start => switch (c) { - '{' => { - if (start_index < i) { - try writer.writeAll(fmt[start_index..i]); - } + comptime var parser: struct { + buf: []const u8 = undefined, + pos: comptime_int = 0, + + // Returns a decimal number or null if the current character is not a + // digit + fn number(comptime self: *@This()) ?usize { + var r: ?usize = null; + + while (self.pos < self.buf.len) : (self.pos += 1) { + switch (self.buf[self.pos]) { + '0'...'9' => { + if (r == null) r = 0; + r.? *= 10; + r.? += self.buf[self.pos] - '0'; + }, + else => break, + } + } - start_index = i; - specifier_start = i + 1; - specifier_end = i + 1; - maybe_pos_arg = null; - state = .Positional; - options = FormatOptions{}; - }, - '}' => { - if (start_index < i) { - try writer.writeAll(fmt[start_index..i]); - } - state = .CloseBrace; - }, - else => {}, - }, - .Positional => switch (c) { - '{' => { - state = .Start; - start_index = i; - }, - ':' => { - state = if (comptime peekIsAlign(fmt[i..])) State.FormatFillAndAlign else State.FormatWidth; - specifier_end = i; - }, - '0'...'9' => { - if (maybe_pos_arg == null) { - maybe_pos_arg = 0; - } + return r; + } - maybe_pos_arg.? *= 10; - maybe_pos_arg.? += c - '0'; - specifier_start = i + 1; + // Returns a substring of the input starting from the current position + // and ending where `ch` is found or until the end if not found + fn until(comptime self: *@This(), comptime ch: u8) []const u8 { + const start = self.pos; - if (maybe_pos_arg.? >= args.len) { - @compileError("Positional value refers to non-existent argument"); - } - }, - '}' => { - const arg_to_print = comptime arg_state.nextArg(maybe_pos_arg); - - try formatType( - args[arg_to_print], - fmt[0..0], - options, - writer, - default_max_depth, - ); - - state = .Start; - start_index = i + 1; - }, - else => { - state = .Specifier; - specifier_start = i; - }, - }, - .CloseBrace => switch (c) { - '}' => { - state = .Start; - start_index = i; - }, - else => @compileError("Single '}' encountered in format string"), - }, - .Specifier => switch (c) { - ':' => { - specifier_end = i; - state = if (comptime peekIsAlign(fmt[i..])) State.FormatFillAndAlign else State.FormatWidth; - }, - '}' => { - const arg_to_print = comptime arg_state.nextArg(maybe_pos_arg); - - try formatType( - args[arg_to_print], - fmt[specifier_start..i], - options, - writer, - default_max_depth, - ); - state = .Start; - start_index = i + 1; - }, + if (start >= self.buf.len) + return &[_]u8{}; + + while (self.pos < self.buf.len) : (self.pos += 1) { + if (self.buf[self.pos] == ch) break; + } + return self.buf[start..self.pos]; + } + + // Returns one character, if available + fn char(comptime self: *@This()) ?u8 { + if (self.pos < self.buf.len) { + const ch = self.buf[self.pos]; + self.pos += 1; + return ch; + } + return null; + } + + fn maybe(comptime self: *@This(), comptime val: u8) bool { + if (self.pos < self.buf.len and self.buf[self.pos] == val) { + self.pos += 1; + return true; + } + return false; + } + + // Returns the n-th next character or null if that's past the end + fn peek(comptime self: *@This(), comptime n: usize) ?u8 { + return if (self.pos + n < self.buf.len) self.buf[self.pos + n] else null; + } + } = .{}; + + var options: FormatOptions = .{}; + + @setEvalBranchQuota(2000000); + + comptime var i = 0; + inline while (i < fmt.len) { + comptime const start_index = i; + + inline while (i < fmt.len) : (i += 1) { + switch (fmt[i]) { + '{', '}' => break, else => {}, - }, - // Only entered if the format string contains a fill/align segment. - .FormatFillAndAlign => switch (c) { + } + } + + comptime var end_index = i; + comptime var unescape_brace = false; + + // Handle {{ and }}, those are un-escaped as single braces + if (i + 1 < fmt.len and fmt[i + 1] == fmt[i]) { + unescape_brace = true; + // Make the first brace part of the literal... + end_index += 1; + // ...and skip both + i += 2; + } + + // Write out the literal + if (start_index != end_index) { + try writer.writeAll(fmt[start_index..end_index]); + } + + // We've already skipped the other brace, restart the loop + if (unescape_brace) continue; + + if (i >= fmt.len) break; + + if (fmt[i] == '}') { + @compileError("Missing opening {"); + } + + // Get past the { + comptime assert(fmt[i] == '{'); + i += 1; + + comptime const fmt_begin = i; + // Find the closing brace + inline while (i < fmt.len and fmt[i] != '}') : (i += 1) {} + comptime const fmt_end = i; + + if (i >= fmt.len) { + @compileError("Missing closing }"); + } + + // Get past the } + comptime assert(fmt[i] == '}'); + i += 1; + + options = .{}; + + // Parse the format fragment between braces + parser.buf = fmt[fmt_begin..fmt_end]; + parser.pos = 0; + + // Parse the positional argument number + comptime const opt_pos_arg = init: { + if (comptime parser.maybe('[')) { + comptime const arg_name = parser.until(']'); + + if (!comptime parser.maybe(']')) { + @compileError("Expected closing ]"); + } + + break :init comptime meta.fieldIndex(ArgsType, arg_name) orelse + @compileError("No argument with name '" ++ arg_name ++ "'"); + } else { + break :init comptime parser.number(); + } + }; + + // Parse the format specifier + comptime const specifier_arg = comptime parser.until(':'); + + // Skip the colon, if present + if (comptime parser.char()) |ch| { + if (ch != ':') { + @compileError("Expected : or }, found '" ++ [1]u8{ch} ++ "'"); + } + } + + // Parse the fill character + // The fill parameter requires the alignment parameter to be specified + // too + if (comptime parser.peek(1)) |ch| { + if (comptime mem.indexOfScalar(u8, "<^>", ch) != null) { + options.fill = comptime parser.char().?; + } + } + + // Parse the alignment parameter + if (comptime parser.peek(0)) |ch| { + switch (ch) { '<' => { - options.alignment = Alignment.Left; - state = .FormatWidth; + options.alignment = .Left; + _ = comptime parser.char(); }, '^' => { - options.alignment = Alignment.Center; - state = .FormatWidth; + options.alignment = .Center; + _ = comptime parser.char(); }, '>' => { - options.alignment = Alignment.Right; - state = .FormatWidth; + options.alignment = .Right; + _ = comptime parser.char(); }, - else => { - options.fill = c; - }, - }, - .FormatWidth => switch (c) { - '0'...'9' => { - if (options.width == null) { - options.width = 0; - } + else => {}, + } + } - options.width.? *= 10; - options.width.? += c - '0'; - }, - '.' => { - state = .FormatPrecision; - }, - '}' => { - const arg_to_print = comptime arg_state.nextArg(maybe_pos_arg); - - try formatType( - args[arg_to_print], - fmt[specifier_start..specifier_end], - options, - writer, - default_max_depth, - ); - state = .Start; - start_index = i + 1; - }, - else => { - @compileError("Unexpected character in width value: " ++ [_]u8{c}); - }, - }, - .FormatPrecision => switch (c) { - '0'...'9' => { - if (options.precision == null) { - options.precision = 0; - } + // Parse the width parameter + options.width = init: { + if (comptime parser.maybe('[')) { + comptime const arg_name = parser.until(']'); - options.precision.? *= 10; - options.precision.? += c - '0'; - }, - '}' => { - const arg_to_print = comptime arg_state.nextArg(maybe_pos_arg); - - try formatType( - args[arg_to_print], - fmt[specifier_start..specifier_end], - options, - writer, - default_max_depth, - ); - state = .Start; - start_index = i + 1; - }, - else => { - @compileError("Unexpected character in precision value: " ++ [_]u8{c}); - }, - }, - } - } - comptime { - if (comptime arg_state.hasUnusedArgs()) { - @compileError("Unused arguments"); + if (!comptime parser.maybe(']')) { + @compileError("Expected closing ]"); + } + + comptime const index = meta.fieldIndex(ArgsType, arg_name) orelse + @compileError("No argument with name '" ++ arg_name ++ "'"); + const arg_index = comptime arg_state.nextArg(index); + + break :init @field(args, fields_info[arg_index].name); + } else { + break :init comptime parser.number(); + } + }; + + // Skip the dot, if present + if (comptime parser.char()) |ch| { + if (ch != '.') { + @compileError("Expected . or }, found '" ++ [1]u8{ch} ++ "'"); + } } - if (state != State.Start) { - @compileError("Incomplete format string: " ++ fmt); + + // Parse the precision parameter + options.precision = init: { + if (comptime parser.maybe('[')) { + comptime const arg_name = parser.until(']'); + + if (!comptime parser.maybe(']')) { + @compileError("Expected closing ]"); + } + + comptime const arg_i = meta.fieldIndex(ArgsType, arg_name) orelse + @compileError("No argument with name '" ++ arg_name ++ "'"); + const arg_to_use = comptime arg_state.nextArg(arg_i); + + break :init @field(args, fields_info[arg_to_use].name); + } else { + break :init comptime parser.number(); + } + }; + + if (comptime parser.char()) |ch| { + @compileError("Extraneous trailing character '" ++ [1]u8{ch} ++ "'"); } + + const arg_to_print = comptime arg_state.nextArg(opt_pos_arg); + try formatType( + @field(args, fields_info[arg_to_print].name), + specifier_arg, + options, + writer, + default_max_depth, + ); } - if (start_index < fmt.len) { - try writer.writeAll(fmt[start_index..]); + + if (comptime arg_state.hasUnusedArgs()) { + @compileError("Unused arguments"); } } @@ -1371,6 +1413,11 @@ test "parse unsigned comptime" { } } +test "escaped braces" { + try testFmt("escaped: {{foo}}\n", "escaped: {{{{foo}}}}\n", .{}); + try testFmt("escaped: {foo}\n", "escaped: {{foo}}\n", .{}); +} + test "optional" { { const value: ?i32 = 1234; @@ -2004,3 +2051,22 @@ test "null" { const inst = null; try testFmt("null", "{}", .{inst}); } + +test "named arguments" { + try testFmt("hello world!", "{} world{c}", .{ "hello", '!' }); + try testFmt("hello world!", "{[greeting]} world{[punctuation]c}", .{ .punctuation = '!', .greeting = "hello" }); + try testFmt("hello world!", "{[1]} world{[0]c}", .{ '!', "hello" }); +} + +test "runtime width specifier" { + var width: usize = 9; + try testFmt("~~hello~~", "{:~^[1]}", .{ "hello", width }); + try testFmt("~~hello~~", "{:~^[width]}", .{ .string = "hello", .width = width }); +} + +test "runtime precision specifier" { + var number: f32 = 3.1415; + var precision: usize = 2; + try testFmt("3.14e+00", "{:1.[1]}", .{ number, precision }); + try testFmt("3.14e+00", "{:1.[precision]}", .{ .number = number, .precision = precision }); +} |
