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;...@@ -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;
...@@ -84,43 +85,48 @@ pub fn format(...@@ -84,43 +85,48 @@ pub fn format(
84 args: anytype,85 args: anytype,
85) !void {86) !void {
86 const ArgSetType = u32;87 const ArgSetType = u32;
87 if (@typeInfo(@TypeOf(args)) != .Struct) {88
88 @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));
89 }93 }
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) {
91 @compileError("32 arguments max are supported per format call");97 @compileError("32 arguments max are supported per format call");
92 }98 }
9399
94 comptime var arg_state: struct {100 comptime var arg_state: struct {
95 next_arg: usize = 0,101 next_arg: usize = 0,
96 used_args: ArgSetType = 0,102 used_args: usize = 0,
97 args_len: usize = args.len,103 args_len: usize = fields_info.len,
98104
99 fn hasUnusedArgs(comptime self: *@This()) bool {105 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;
101 }107 }
102108
103 fn nextArg(comptime self: *@This(), comptime pos_arg: ?usize) comptime_int {109 fn nextArg(comptime self: *@This(), comptime arg_index: ?usize) comptime_int {
104 const next_idx = pos_arg orelse blk: {110 const next_index = arg_index orelse init: {
105 const arg = self.next_arg;111 const arg = self.next_arg;
106 self.next_arg += 1;112 self.next_arg += 1;
107 break :blk arg;113 break :init arg;
108 };114 };
109115
110 if (next_idx >= self.args_len) {116 if (next_index >= self.args_len) {
111 @compileError("Too few arguments");117 @compileError("Too few arguments");
112 }118 }
113119
114 // Mark this argument as used120 // 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;
118 }124 }
119 } = .{};125 } = .{};
120126
121 comptime var parser: struct {127 comptime var parser: struct {
122 buf: []const u8 = undefined,128 buf: []const u8 = undefined,
123 pos: usize = 0,129 pos: comptime_int = 0,
124130
125 // Returns a decimal number or null if the current character is not a131 // Returns a decimal number or null if the current character is not a
126 // digit132 // digit
...@@ -165,13 +171,21 @@ pub fn format(...@@ -165,13 +171,21 @@ pub fn format(
165 return null;171 return null;
166 }172 }
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
168 // Returns the n-th next character or null if that's past the end182 // Returns the n-th next character or null if that's past the end
169 fn peek(comptime self: *@This(), comptime n: usize) ?u8 {183 fn peek(comptime self: *@This(), comptime n: usize) ?u8 {
170 return if (self.pos + n < self.buf.len) self.buf[self.pos + n] else null;184 return if (self.pos + n < self.buf.len) self.buf[self.pos + n] else null;
171 }185 }
172 } = .{};186 } = .{};
173187
174 comptime var options: FormatOptions = .{};188 var options: FormatOptions = .{};
175189
176 @setEvalBranchQuota(2000000);190 @setEvalBranchQuota(2000000);
177191
...@@ -209,7 +223,7 @@ pub fn format(...@@ -209,7 +223,7 @@ pub fn format(
209 if (i >= fmt.len) break;223 if (i >= fmt.len) break;
210224
211 if (fmt[i] == '}') {225 if (fmt[i] == '}') {
212 @compileError("missing opening {");226 @compileError("Missing opening {");
213 }227 }
214228
215 // Get past the {229 // Get past the {
...@@ -222,7 +236,7 @@ pub fn format(...@@ -222,7 +236,7 @@ pub fn format(
222 comptime const fmt_end = i;236 comptime const fmt_end = i;
223237
224 if (i >= fmt.len) {238 if (i >= fmt.len) {
225 @compileError("missing closing }");239 @compileError("Missing closing }");
226 }240 }
227241
228 // Get past the }242 // Get past the }
...@@ -236,15 +250,29 @@ pub fn format(...@@ -236,15 +250,29 @@ pub fn format(
236 parser.pos = 0;250 parser.pos = 0;
237251
238 // Parse the positional argument number252 // 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
241 // Parse the format specifier268 // Parse the format specifier
242 comptime var specifier_arg = comptime parser.until(':');269 comptime const specifier_arg = comptime parser.until(':');
243270
244 // Skip the colon, if present271 // Skip the colon, if present
245 if (comptime parser.char()) |ch| {272 if (comptime parser.char()) |ch| {
246 if (ch != ':')273 if (ch != ':') {
247 @compileError("expected : or }, found '" ++ [1]u8{ch} ++ "'");274 @compileError("Expected : or }, found '" ++ [1]u8{ch} ++ "'");
275 }
248 }276 }
249277
250 // Parse the fill character278 // Parse the fill character
...@@ -276,26 +304,57 @@ pub fn format(...@@ -276,26 +304,57 @@ pub fn format(
276 }304 }
277305
278 // Parse the width parameter306 // Parse the width parameter
279 comptime var opt_width_arg = comptime parser.number();307 options.width = init: {
280 options.width = opt_width_arg;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
282 // Skip the dot, if present325 // Skip the dot, if present
283 if (comptime parser.char()) |ch| {326 if (comptime parser.char()) |ch| {
284 if (ch != '.')327 if (ch != '.') {
285 @compileError("expected . or }, found '" ++ [1]u8{ch} ++ "'");328 @compileError("Expected . or }, found '" ++ [1]u8{ch} ++ "'");
329 }
286 }330 }
287331
288 // Parse the precision parameter332 // Parse the precision parameter
289 comptime var opt_precision_arg = comptime parser.number();333 options.precision = init: {
290 options.precision = opt_precision_arg;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
292 if (comptime parser.char()) |ch| {351 if (comptime parser.char()) |ch| {
293 @compileError("extraneous trailing character '" ++ [1]u8{ch} ++ "'");352 @compileError("Extraneous trailing character '" ++ [1]u8{ch} ++ "'");
294 }353 }
295354
296 const arg_to_print = comptime arg_state.nextArg(opt_pos_arg);355 const arg_to_print = comptime arg_state.nextArg(opt_pos_arg);
297 try formatType(356 try formatType(
298 args[arg_to_print],357 @field(args, fields_info[arg_to_print].name),
299 specifier_arg,358 specifier_arg,
300 options,359 options,
301 writer,360 writer,
...@@ -1992,3 +2051,22 @@ test "null" {...@@ -1992,3 +2051,22 @@ test "null" {
1992 const inst = null;2051 const inst = null;
1993 try testFmt("null", "{}", .{inst});2052 try testFmt("null", "{}", .{inst});
1994}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}