authorgravatar for thatlemon@gmail.comLemonBoy <thatlemon@gmail.com> 2020-09-24 16:40:33+02:00
committergravatar for thatlemon@gmail.comLemonBoy <thatlemon@gmail.com> 2020-11-20 08:36:47+01:00
log3da6b1218a126c8c6fa043731c9a3c872b42249d
tree88b7b35af29f7686f1af48ac0c27ec550ed28401
parent6b39167fdc2aa4c4c57d0459f3de792e001562aa

std: Implement named arguments & runtime width/precision


1 files changed, 106 insertions(+), 28 deletions(-)

lib/std/fmt.zig+106-28
......@@ -8,6 +8,7 @@ const math = std.math;
88const assert = std.debug.assert;
99const mem = std.mem;
1010const unicode = std.unicode;
11const meta = std.meta;
1112const builtin = @import("builtin");
1213const errol = @import("fmt/errol.zig");
1314const lossyCast = std.math.lossyCast;
......@@ -84,43 +85,48 @@ pub fn format(
8485 args: anytype,
8586) !void {
8687 const ArgSetType = u32;
87 if (@typeInfo(@TypeOf(args)) != .Struct) {
88 @compileError("Expected tuple or struct argument, found " ++ @typeName(@TypeOf(args)));
88
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));
8993 }
90 if (args.len > @typeInfo(ArgSetType).Int.bits) {
94
95 const fields_info = meta.fields(ArgsType);
96 if (fields_info.len > @typeInfo(ArgSetType).Int.bits) {
9197 @compileError("32 arguments max are supported per format call");
9298 }
9399
94100 comptime var arg_state: struct {
95101 next_arg: usize = 0,
96 used_args: ArgSetType = 0,
97 args_len: usize = args.len,
102 used_args: usize = 0,
103 args_len: usize = fields_info.len,
98104
99105 fn hasUnusedArgs(comptime self: *@This()) bool {
100 return (@popCount(ArgSetType, self.used_args) != self.args_len);
106 return @popCount(ArgSetType, self.used_args) != self.args_len;
101107 }
102108
103 fn nextArg(comptime self: *@This(), comptime pos_arg: ?usize) comptime_int {
104 const next_idx = pos_arg orelse blk: {
109 fn nextArg(comptime self: *@This(), comptime arg_index: ?usize) comptime_int {
110 const next_index = arg_index orelse init: {
105111 const arg = self.next_arg;
106112 self.next_arg += 1;
107 break :blk arg;
113 break :init arg;
108114 };
109115
110 if (next_idx >= self.args_len) {
116 if (next_index >= self.args_len) {
111117 @compileError("Too few arguments");
112118 }
113119
114120 // Mark this argument as used
115 self.used_args |= 1 << next_idx;
121 self.used_args |= 1 << next_index;
116122
117 return next_idx;
123 return next_index;
118124 }
119125 } = .{};
120126
121127 comptime var parser: struct {
122128 buf: []const u8 = undefined,
123 pos: usize = 0,
129 pos: comptime_int = 0,
124130
125131 // Returns a decimal number or null if the current character is not a
126132 // digit
......@@ -165,13 +171,21 @@ pub fn format(
165171 return null;
166172 }
167173
174 fn maybe(comptime self: *@This(), comptime val: u8) bool {
175 if (self.pos < self.buf.len and self.buf[self.pos] == val) {
176 self.pos += 1;
177 return true;
178 }
179 return false;
180 }
181
168182 // Returns the n-th next character or null if that's past the end
169183 fn peek(comptime self: *@This(), comptime n: usize) ?u8 {
170184 return if (self.pos + n < self.buf.len) self.buf[self.pos + n] else null;
171185 }
172186 } = .{};
173187
174 comptime var options: FormatOptions = .{};
188 var options: FormatOptions = .{};
175189
176190 @setEvalBranchQuota(2000000);
177191
......@@ -209,7 +223,7 @@ pub fn format(
209223 if (i >= fmt.len) break;
210224
211225 if (fmt[i] == '}') {
212 @compileError("missing opening {");
226 @compileError("Missing opening {");
213227 }
214228
215229 // Get past the {
......@@ -222,7 +236,7 @@ pub fn format(
222236 comptime const fmt_end = i;
223237
224238 if (i >= fmt.len) {
225 @compileError("missing closing }");
239 @compileError("Missing closing }");
226240 }
227241
228242 // Get past the }
......@@ -236,15 +250,29 @@ pub fn format(
236250 parser.pos = 0;
237251
238252 // Parse the positional argument number
239 comptime var opt_pos_arg = comptime parser.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 };
240267
241268 // Parse the format specifier
242 comptime var specifier_arg = comptime parser.until(':');
269 comptime const specifier_arg = comptime parser.until(':');
243270
244271 // Skip the colon, if present
245272 if (comptime parser.char()) |ch| {
246 if (ch != ':')
247 @compileError("expected : or }, found '" ++ [1]u8{ch} ++ "'");
273 if (ch != ':') {
274 @compileError("Expected : or }, found '" ++ [1]u8{ch} ++ "'");
275 }
248276 }
249277
250278 // Parse the fill character
......@@ -276,26 +304,57 @@ pub fn format(
276304 }
277305
278306 // Parse the width parameter
279 comptime var opt_width_arg = comptime parser.number();
280 options.width = opt_width_arg;
307 options.width = init: {
308 if (comptime parser.maybe('[')) {
309 comptime const arg_name = parser.until(']');
310
311 if (!comptime parser.maybe(']')) {
312 @compileError("Expected closing ]");
313 }
314
315 comptime const index = meta.fieldIndex(ArgsType, arg_name) orelse
316 @compileError("No argument with name '" ++ arg_name ++ "'");
317 const arg_index = comptime arg_state.nextArg(index);
318
319 break :init @field(args, fields_info[arg_index].name);
320 } else {
321 break :init comptime parser.number();
322 }
323 };
281324
282325 // Skip the dot, if present
283326 if (comptime parser.char()) |ch| {
284 if (ch != '.')
285 @compileError("expected . or }, found '" ++ [1]u8{ch} ++ "'");
327 if (ch != '.') {
328 @compileError("Expected . or }, found '" ++ [1]u8{ch} ++ "'");
329 }
286330 }
287331
288332 // Parse the precision parameter
289 comptime var opt_precision_arg = comptime parser.number();
290 options.precision = opt_precision_arg;
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 };
291350
292351 if (comptime parser.char()) |ch| {
293 @compileError("extraneous trailing character '" ++ [1]u8{ch} ++ "'");
352 @compileError("Extraneous trailing character '" ++ [1]u8{ch} ++ "'");
294353 }
295354
296355 const arg_to_print = comptime arg_state.nextArg(opt_pos_arg);
297356 try formatType(
298 args[arg_to_print],
357 @field(args, fields_info[arg_to_print].name),
299358 specifier_arg,
300359 options,
301360 writer,
......@@ -1992,3 +2051,22 @@ test "null" {
19922051 const inst = null;
19932052 try testFmt("null", "{}", .{inst});
19942053}
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}