authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2020-11-25 15:43:19-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2020-11-25 15:43:19-08:00
loga06afa457928fb7998db3ec9419d38fdbb909706
tree21a6a5f23dd580417a2401de559fd0208bb207eb
parentb7b3c1dfaea134a36276786878bf7fe5db63b92b
parent3da6b1218a126c8c6fa043731c9a3c872b42249d
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #6411 from LemonBoy/fff

More std.fmt goodness

1 files changed, 267 insertions(+), 201 deletions(-)

lib/std/fmt.zig+267-201
...@@ -8,6 +8,7 @@ const math = std.math;...@@ -8,6 +8,7 @@ const math = std.math;
8const assert = std.debug.assert;8const assert = std.debug.assert;
9const mem = std.mem;9const mem = std.mem;
10const unicode = std.unicode;10const unicode = std.unicode;
11const meta = std.meta;
11const builtin = @import("builtin");12const builtin = @import("builtin");
12const errol = @import("fmt/errol.zig");13const errol = @import("fmt/errol.zig");
13const lossyCast = std.math.lossyCast;14const lossyCast = std.math.lossyCast;
...@@ -27,18 +28,6 @@ pub const FormatOptions = struct {...@@ -27,18 +28,6 @@ pub const FormatOptions = struct {
27 fill: u8 = ' ',28 fill: u8 = ' ',
28};29};
2930
30fn peekIsAlign(comptime fmt: []const u8) bool {
31 // Should only be called during a state transition to the format segment.
32 comptime assert(fmt[0] == ':');
33
34 inline for (([_]u8{ 1, 2 })[0..]) |i| {
35 if (fmt.len > i and (fmt[i] == '<' or fmt[i] == '^' or fmt[i] == '>')) {
36 return true;
37 }
38 }
39 return false;
40}
41
42/// Renders fmt string with args, calling output with slices of bytes.31/// Renders fmt string with args, calling output with slices of bytes.
43/// If `output` returns an error, the error is returned from `format` and32/// If `output` returns an error, the error is returned from `format` and
44/// `output` is not called again.33/// `output` is not called again.
...@@ -96,232 +85,285 @@ pub fn format(...@@ -96,232 +85,285 @@ pub fn format(
96 args: anytype,85 args: anytype,
97) !void {86) !void {
98 const ArgSetType = u32;87 const ArgSetType = u32;
99 if (@typeInfo(@TypeOf(args)) != .Struct) {88
100 @compileError("Expected tuple or struct argument, found " ++ @typeName(@TypeOf(args)));89 const ArgsType = @TypeOf(args);
90 // XXX: meta.trait.is(.Struct)(ArgsType) doesn't seem to work...
91 if (@typeInfo(ArgsType) != .Struct) {
92 @compileError("Expected tuple or struct argument, found " ++ @typeName(ArgsType));
101 }93 }
102 if (args.len > @typeInfo(ArgSetType).Int.bits) {94
95 const fields_info = meta.fields(ArgsType);
96 if (fields_info.len > @typeInfo(ArgSetType).Int.bits) {
103 @compileError("32 arguments max are supported per format call");97 @compileError("32 arguments max are supported per format call");
104 }98 }
10599
106 const State = enum {
107 Start,
108 Positional,
109 CloseBrace,
110 Specifier,
111 FormatFillAndAlign,
112 FormatWidth,
113 FormatPrecision,
114 };
115
116 comptime var start_index = 0;
117 comptime var state = State.Start;
118 comptime var maybe_pos_arg: ?comptime_int = null;
119 comptime var specifier_start = 0;
120 comptime var specifier_end = 0;
121 comptime var options = FormatOptions{};
122 comptime var arg_state: struct {100 comptime var arg_state: struct {
123 next_arg: usize = 0,101 next_arg: usize = 0,
124 used_args: ArgSetType = 0,102 used_args: usize = 0,
125 args_len: usize = args.len,103 args_len: usize = fields_info.len,
126104
127 fn hasUnusedArgs(comptime self: *@This()) bool {105 fn hasUnusedArgs(comptime self: *@This()) bool {
128 return (@popCount(ArgSetType, self.used_args) != self.args_len);106 return @popCount(ArgSetType, self.used_args) != self.args_len;
129 }107 }
130108
131 fn nextArg(comptime self: *@This(), comptime pos_arg: ?comptime_int) comptime_int {109 fn nextArg(comptime self: *@This(), comptime arg_index: ?usize) comptime_int {
132 const next_idx = pos_arg orelse blk: {110 const next_index = arg_index orelse init: {
133 const arg = self.next_arg;111 const arg = self.next_arg;
134 self.next_arg += 1;112 self.next_arg += 1;
135 break :blk arg;113 break :init arg;
136 };114 };
137115
138 if (next_idx >= self.args_len) {116 if (next_index >= self.args_len) {
139 @compileError("Too few arguments");117 @compileError("Too few arguments");
140 }118 }
141119
142 // Mark this argument as used120 // Mark this argument as used
143 self.used_args |= 1 << next_idx;121 self.used_args |= 1 << next_index;
144122
145 return next_idx;123 return next_index;
146 }124 }
147 } = .{};125 } = .{};
148126
149 inline for (fmt) |c, i| {127 comptime var parser: struct {
150 switch (state) {128 buf: []const u8 = undefined,
151 .Start => switch (c) {129 pos: comptime_int = 0,
152 '{' => {130
153 if (start_index < i) {131 // Returns a decimal number or null if the current character is not a
154 try writer.writeAll(fmt[start_index..i]);132 // digit
155 }133 fn number(comptime self: *@This()) ?usize {
134 var r: ?usize = null;
135
136 while (self.pos < self.buf.len) : (self.pos += 1) {
137 switch (self.buf[self.pos]) {
138 '0'...'9' => {
139 if (r == null) r = 0;
140 r.? *= 10;
141 r.? += self.buf[self.pos] - '0';
142 },
143 else => break,
144 }
145 }
156146
157 start_index = i;147 return r;
158 specifier_start = i + 1;148 }
159 specifier_end = i + 1;
160 maybe_pos_arg = null;
161 state = .Positional;
162 options = FormatOptions{};
163 },
164 '}' => {
165 if (start_index < i) {
166 try writer.writeAll(fmt[start_index..i]);
167 }
168 state = .CloseBrace;
169 },
170 else => {},
171 },
172 .Positional => switch (c) {
173 '{' => {
174 state = .Start;
175 start_index = i;
176 },
177 ':' => {
178 state = if (comptime peekIsAlign(fmt[i..])) State.FormatFillAndAlign else State.FormatWidth;
179 specifier_end = i;
180 },
181 '0'...'9' => {
182 if (maybe_pos_arg == null) {
183 maybe_pos_arg = 0;
184 }
185149
186 maybe_pos_arg.? *= 10;150 // Returns a substring of the input starting from the current position
187 maybe_pos_arg.? += c - '0';151 // and ending where `ch` is found or until the end if not found
188 specifier_start = i + 1;152 fn until(comptime self: *@This(), comptime ch: u8) []const u8 {
153 const start = self.pos;
189154
190 if (maybe_pos_arg.? >= args.len) {155 if (start >= self.buf.len)
191 @compileError("Positional value refers to non-existent argument");156 return &[_]u8{};
192 }157
193 },158 while (self.pos < self.buf.len) : (self.pos += 1) {
194 '}' => {159 if (self.buf[self.pos] == ch) break;
195 const arg_to_print = comptime arg_state.nextArg(maybe_pos_arg);160 }
196161 return self.buf[start..self.pos];
197 try formatType(162 }
198 args[arg_to_print],163
199 fmt[0..0],164 // Returns one character, if available
200 options,165 fn char(comptime self: *@This()) ?u8 {
201 writer,166 if (self.pos < self.buf.len) {
202 default_max_depth,167 const ch = self.buf[self.pos];
203 );168 self.pos += 1;
204169 return ch;
205 state = .Start;170 }
206 start_index = i + 1;171 return null;
207 },172 }
208 else => {173
209 state = .Specifier;174 fn maybe(comptime self: *@This(), comptime val: u8) bool {
210 specifier_start = i;175 if (self.pos < self.buf.len and self.buf[self.pos] == val) {
211 },176 self.pos += 1;
212 },177 return true;
213 .CloseBrace => switch (c) {178 }
214 '}' => {179 return false;
215 state = .Start;180 }
216 start_index = i;181
217 },182 // Returns the n-th next character or null if that's past the end
218 else => @compileError("Single '}' encountered in format string"),183 fn peek(comptime self: *@This(), comptime n: usize) ?u8 {
219 },184 return if (self.pos + n < self.buf.len) self.buf[self.pos + n] else null;
220 .Specifier => switch (c) {185 }
221 ':' => {186 } = .{};
222 specifier_end = i;187
223 state = if (comptime peekIsAlign(fmt[i..])) State.FormatFillAndAlign else State.FormatWidth;188 var options: FormatOptions = .{};
224 },189
225 '}' => {190 @setEvalBranchQuota(2000000);
226 const arg_to_print = comptime arg_state.nextArg(maybe_pos_arg);191
227192 comptime var i = 0;
228 try formatType(193 inline while (i < fmt.len) {
229 args[arg_to_print],194 comptime const start_index = i;
230 fmt[specifier_start..i],195
231 options,196 inline while (i < fmt.len) : (i += 1) {
232 writer,197 switch (fmt[i]) {
233 default_max_depth,198 '{', '}' => break,
234 );
235 state = .Start;
236 start_index = i + 1;
237 },
238 else => {},199 else => {},
239 },200 }
240 // Only entered if the format string contains a fill/align segment.201 }
241 .FormatFillAndAlign => switch (c) {202
203 comptime var end_index = i;
204 comptime var unescape_brace = false;
205
206 // Handle {{ and }}, those are un-escaped as single braces
207 if (i + 1 < fmt.len and fmt[i + 1] == fmt[i]) {
208 unescape_brace = true;
209 // Make the first brace part of the literal...
210 end_index += 1;
211 // ...and skip both
212 i += 2;
213 }
214
215 // Write out the literal
216 if (start_index != end_index) {
217 try writer.writeAll(fmt[start_index..end_index]);
218 }
219
220 // We've already skipped the other brace, restart the loop
221 if (unescape_brace) continue;
222
223 if (i >= fmt.len) break;
224
225 if (fmt[i] == '}') {
226 @compileError("Missing opening {");
227 }
228
229 // Get past the {
230 comptime assert(fmt[i] == '{');
231 i += 1;
232
233 comptime const fmt_begin = i;
234 // Find the closing brace
235 inline while (i < fmt.len and fmt[i] != '}') : (i += 1) {}
236 comptime const fmt_end = i;
237
238 if (i >= fmt.len) {
239 @compileError("Missing closing }");
240 }
241
242 // Get past the }
243 comptime assert(fmt[i] == '}');
244 i += 1;
245
246 options = .{};
247
248 // Parse the format fragment between braces
249 parser.buf = fmt[fmt_begin..fmt_end];
250 parser.pos = 0;
251
252 // Parse the positional argument number
253 comptime const opt_pos_arg = init: {
254 if (comptime parser.maybe('[')) {
255 comptime const arg_name = parser.until(']');
256
257 if (!comptime parser.maybe(']')) {
258 @compileError("Expected closing ]");
259 }
260
261 break :init comptime meta.fieldIndex(ArgsType, arg_name) orelse
262 @compileError("No argument with name '" ++ arg_name ++ "'");
263 } else {
264 break :init comptime parser.number();
265 }
266 };
267
268 // Parse the format specifier
269 comptime const specifier_arg = comptime parser.until(':');
270
271 // Skip the colon, if present
272 if (comptime parser.char()) |ch| {
273 if (ch != ':') {
274 @compileError("Expected : or }, found '" ++ [1]u8{ch} ++ "'");
275 }
276 }
277
278 // Parse the fill character
279 // The fill parameter requires the alignment parameter to be specified
280 // too
281 if (comptime parser.peek(1)) |ch| {
282 if (comptime mem.indexOfScalar(u8, "<^>", ch) != null) {
283 options.fill = comptime parser.char().?;
284 }
285 }
286
287 // Parse the alignment parameter
288 if (comptime parser.peek(0)) |ch| {
289 switch (ch) {
242 '<' => {290 '<' => {
243 options.alignment = Alignment.Left;291 options.alignment = .Left;
244 state = .FormatWidth;292 _ = comptime parser.char();
245 },293 },
246 '^' => {294 '^' => {
247 options.alignment = Alignment.Center;295 options.alignment = .Center;
248 state = .FormatWidth;296 _ = comptime parser.char();
249 },297 },
250 '>' => {298 '>' => {
251 options.alignment = Alignment.Right;299 options.alignment = .Right;
252 state = .FormatWidth;300 _ = comptime parser.char();
253 },301 },
254 else => {302 else => {},
255 options.fill = c;303 }
256 },304 }
257 },
258 .FormatWidth => switch (c) {
259 '0'...'9' => {
260 if (options.width == null) {
261 options.width = 0;
262 }
263305
264 options.width.? *= 10;306 // Parse the width parameter
265 options.width.? += c - '0';307 options.width = init: {
266 },308 if (comptime parser.maybe('[')) {
267 '.' => {309 comptime const arg_name = parser.until(']');
268 state = .FormatPrecision;
269 },
270 '}' => {
271 const arg_to_print = comptime arg_state.nextArg(maybe_pos_arg);
272
273 try formatType(
274 args[arg_to_print],
275 fmt[specifier_start..specifier_end],
276 options,
277 writer,
278 default_max_depth,
279 );
280 state = .Start;
281 start_index = i + 1;
282 },
283 else => {
284 @compileError("Unexpected character in width value: " ++ [_]u8{c});
285 },
286 },
287 .FormatPrecision => switch (c) {
288 '0'...'9' => {
289 if (options.precision == null) {
290 options.precision = 0;
291 }
292310
293 options.precision.? *= 10;311 if (!comptime parser.maybe(']')) {
294 options.precision.? += c - '0';312 @compileError("Expected closing ]");
295 },313 }
296 '}' => {314
297 const arg_to_print = comptime arg_state.nextArg(maybe_pos_arg);315 comptime const index = meta.fieldIndex(ArgsType, arg_name) orelse
298316 @compileError("No argument with name '" ++ arg_name ++ "'");
299 try formatType(317 const arg_index = comptime arg_state.nextArg(index);
300 args[arg_to_print],318
301 fmt[specifier_start..specifier_end],319 break :init @field(args, fields_info[arg_index].name);
302 options,320 } else {
303 writer,321 break :init comptime parser.number();
304 default_max_depth,322 }
305 );323 };
306 state = .Start;324
307 start_index = i + 1;325 // Skip the dot, if present
308 },326 if (comptime parser.char()) |ch| {
309 else => {327 if (ch != '.') {
310 @compileError("Unexpected character in precision value: " ++ [_]u8{c});328 @compileError("Expected . or }, found '" ++ [1]u8{ch} ++ "'");
311 },329 }
312 },
313 }
314 }
315 comptime {
316 if (comptime arg_state.hasUnusedArgs()) {
317 @compileError("Unused arguments");
318 }330 }
319 if (state != State.Start) {331
320 @compileError("Incomplete format string: " ++ fmt);332 // Parse the precision parameter
333 options.precision = init: {
334 if (comptime parser.maybe('[')) {
335 comptime const arg_name = parser.until(']');
336
337 if (!comptime parser.maybe(']')) {
338 @compileError("Expected closing ]");
339 }
340
341 comptime const arg_i = meta.fieldIndex(ArgsType, arg_name) orelse
342 @compileError("No argument with name '" ++ arg_name ++ "'");
343 const arg_to_use = comptime arg_state.nextArg(arg_i);
344
345 break :init @field(args, fields_info[arg_to_use].name);
346 } else {
347 break :init comptime parser.number();
348 }
349 };
350
351 if (comptime parser.char()) |ch| {
352 @compileError("Extraneous trailing character '" ++ [1]u8{ch} ++ "'");
321 }353 }
354
355 const arg_to_print = comptime arg_state.nextArg(opt_pos_arg);
356 try formatType(
357 @field(args, fields_info[arg_to_print].name),
358 specifier_arg,
359 options,
360 writer,
361 default_max_depth,
362 );
322 }363 }
323 if (start_index < fmt.len) {364
324 try writer.writeAll(fmt[start_index..]);365 if (comptime arg_state.hasUnusedArgs()) {
366 @compileError("Unused arguments");
325 }367 }
326}368}
327369
...@@ -1371,6 +1413,11 @@ test "parse unsigned comptime" {...@@ -1371,6 +1413,11 @@ test "parse unsigned comptime" {
1371 }1413 }
1372}1414}
13731415
1416test "escaped braces" {
1417 try testFmt("escaped: {{foo}}\n", "escaped: {{{{foo}}}}\n", .{});
1418 try testFmt("escaped: {foo}\n", "escaped: {{foo}}\n", .{});
1419}
1420
1374test "optional" {1421test "optional" {
1375 {1422 {
1376 const value: ?i32 = 1234;1423 const value: ?i32 = 1234;
...@@ -2004,3 +2051,22 @@ test "null" {...@@ -2004,3 +2051,22 @@ test "null" {
2004 const inst = null;2051 const inst = null;
2005 try testFmt("null", "{}", .{inst});2052 try testFmt("null", "{}", .{inst});
2006}2053}
2054
2055test "named arguments" {
2056 try testFmt("hello world!", "{} world{c}", .{ "hello", '!' });
2057 try testFmt("hello world!", "{[greeting]} world{[punctuation]c}", .{ .punctuation = '!', .greeting = "hello" });
2058 try testFmt("hello world!", "{[1]} world{[0]c}", .{ '!', "hello" });
2059}
2060
2061test "runtime width specifier" {
2062 var width: usize = 9;
2063 try testFmt("~~hello~~", "{:~^[1]}", .{ "hello", width });
2064 try testFmt("~~hello~~", "{:~^[width]}", .{ .string = "hello", .width = width });
2065}
2066
2067test "runtime precision specifier" {
2068 var number: f32 = 3.1415;
2069 var precision: usize = 2;
2070 try testFmt("3.14e+00", "{:1.[1]}", .{ number, precision });
2071 try testFmt("3.14e+00", "{:1.[precision]}", .{ .number = number, .precision = precision });
2072}