Skip to content

Commit 7461699

Browse files
committed
std: Implement named arguments & runtime width/precision
1 parent c69d45c commit 7461699

File tree

1 file changed

+106
-28
lines changed

1 file changed

+106
-28
lines changed

lib/std/fmt.zig

Lines changed: 106 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const std = @import("std.zig");
77
const math = std.math;
88
const assert = std.debug.assert;
99
const mem = std.mem;
10+
const meta = std.meta;
1011
const builtin = @import("builtin");
1112
const errol = @import("fmt/errol.zig");
1213
const lossyCast = std.math.lossyCast;
@@ -82,43 +83,48 @@ pub fn format(
8283
args: anytype,
8384
) !void {
8485
const ArgSetType = u32;
85-
if (@typeInfo(@TypeOf(args)) != .Struct) {
86-
@compileError("Expected tuple or struct argument, found " ++ @typeName(@TypeOf(args)));
86+
87+
const ArgsType = @TypeOf(args);
88+
// XXX: meta.trait.is(.Struct)(ArgsType) doesn't seem to work...
89+
if (@typeInfo(ArgsType) != .Struct) {
90+
@compileError("Expected tuple or struct argument, found " ++ @typeName(ArgsType));
8791
}
88-
if (args.len > @typeInfo(ArgSetType).Int.bits) {
92+
93+
const fields_info = meta.fields(ArgsType);
94+
if (fields_info.len > @typeInfo(ArgSetType).Int.bits) {
8995
@compileError("32 arguments max are supported per format call");
9096
}
9197

9298
comptime var arg_state: struct {
9399
next_arg: usize = 0,
94-
used_args: ArgSetType = 0,
95-
args_len: usize = args.len,
100+
used_args: usize = 0,
101+
args_len: usize = fields_info.len,
96102

97103
fn hasUnusedArgs(comptime self: *@This()) bool {
98-
return (@popCount(ArgSetType, self.used_args) != self.args_len);
104+
return @popCount(ArgSetType, self.used_args) != self.args_len;
99105
}
100106

101-
fn nextArg(comptime self: *@This(), comptime pos_arg: ?usize) comptime_int {
102-
const next_idx = pos_arg orelse blk: {
107+
fn nextArg(comptime self: *@This(), comptime arg_index: ?usize) comptime_int {
108+
const next_index = arg_index orelse init: {
103109
const arg = self.next_arg;
104110
self.next_arg += 1;
105-
break :blk arg;
111+
break :init arg;
106112
};
107113

108-
if (next_idx >= self.args_len) {
114+
if (next_index >= self.args_len) {
109115
@compileError("Too few arguments");
110116
}
111117

112118
// Mark this argument as used
113-
self.used_args |= 1 << next_idx;
119+
self.used_args |= 1 << next_index;
114120

115-
return next_idx;
121+
return next_index;
116122
}
117123
} = .{};
118124

119125
comptime var parser: struct {
120126
buf: []const u8 = undefined,
121-
pos: usize = 0,
127+
pos: comptime_int = 0,
122128

123129
// Returns a decimal number or null if the current character is not a
124130
// digit
@@ -163,13 +169,21 @@ pub fn format(
163169
return null;
164170
}
165171

172+
fn maybe(comptime self: *@This(), comptime val: u8) bool {
173+
if (self.pos < self.buf.len and self.buf[self.pos] == val) {
174+
self.pos += 1;
175+
return true;
176+
}
177+
return false;
178+
}
179+
166180
// Returns the n-th next character or null if that's past the end
167181
fn peek(comptime self: *@This(), comptime n: usize) ?u8 {
168182
return if (self.pos + n < self.buf.len) self.buf[self.pos + n] else null;
169183
}
170184
} = .{};
171185

172-
comptime var options: FormatOptions = .{};
186+
var options: FormatOptions = .{};
173187

174188
@setEvalBranchQuota(2000000);
175189

@@ -207,7 +221,7 @@ pub fn format(
207221
if (i >= fmt.len) break;
208222

209223
if (fmt[i] == '}') {
210-
@compileError("missing opening {");
224+
@compileError("Missing opening {");
211225
}
212226

213227
// Get past the {
@@ -220,7 +234,7 @@ pub fn format(
220234
comptime const fmt_end = i;
221235

222236
if (i >= fmt.len) {
223-
@compileError("missing closing }");
237+
@compileError("Missing closing }");
224238
}
225239

226240
// Get past the }
@@ -234,15 +248,29 @@ pub fn format(
234248
parser.pos = 0;
235249

236250
// Parse the positional argument number
237-
comptime var opt_pos_arg = comptime parser.number();
251+
comptime const opt_pos_arg = init: {
252+
if (comptime parser.maybe('[')) {
253+
comptime const arg_name = parser.until(']');
254+
255+
if (!comptime parser.maybe(']')) {
256+
@compileError("Expected closing ]");
257+
}
258+
259+
break :init comptime meta.fieldIndex(ArgsType, arg_name) orelse
260+
@compileError("No argument with name '" ++ arg_name ++ "'");
261+
} else {
262+
break :init comptime parser.number();
263+
}
264+
};
238265

239266
// Parse the format specifier
240-
comptime var specifier_arg = comptime parser.until(':');
267+
comptime const specifier_arg = comptime parser.until(':');
241268

242269
// Skip the colon, if present
243270
if (comptime parser.char()) |ch| {
244-
if (ch != ':')
245-
@compileError("expected : or }, found '" ++ [1]u8{ch} ++ "'");
271+
if (ch != ':') {
272+
@compileError("Expected : or }, found '" ++ [1]u8{ch} ++ "'");
273+
}
246274
}
247275

248276
// Parse the fill character
@@ -274,26 +302,57 @@ pub fn format(
274302
}
275303

276304
// Parse the width parameter
277-
comptime var opt_width_arg = comptime parser.number();
278-
options.width = opt_width_arg;
305+
options.width = init: {
306+
if (comptime parser.maybe('[')) {
307+
comptime const arg_name = parser.until(']');
308+
309+
if (!comptime parser.maybe(']')) {
310+
@compileError("Expected closing ]");
311+
}
312+
313+
comptime const index = meta.fieldIndex(ArgsType, arg_name) orelse
314+
@compileError("No argument with name '" ++ arg_name ++ "'");
315+
const arg_index = comptime arg_state.nextArg(index);
316+
317+
break :init @field(args, fields_info[arg_index].name);
318+
} else {
319+
break :init comptime parser.number();
320+
}
321+
};
279322

280323
// Skip the dot, if present
281324
if (comptime parser.char()) |ch| {
282-
if (ch != '.')
283-
@compileError("expected . or }, found '" ++ [1]u8{ch} ++ "'");
325+
if (ch != '.') {
326+
@compileError("Expected . or }, found '" ++ [1]u8{ch} ++ "'");
327+
}
284328
}
285329

286330
// Parse the precision parameter
287-
comptime var opt_precision_arg = comptime parser.number();
288-
options.precision = opt_precision_arg;
331+
options.precision = init: {
332+
if (comptime parser.maybe('[')) {
333+
comptime const arg_name = parser.until(']');
334+
335+
if (!comptime parser.maybe(']')) {
336+
@compileError("Expected closing ]");
337+
}
338+
339+
comptime const arg_i = meta.fieldIndex(ArgsType, arg_name) orelse
340+
@compileError("No argument with name '" ++ arg_name ++ "'");
341+
const arg_to_use = comptime arg_state.nextArg(arg_i);
342+
343+
break :init @field(args, fields_info[arg_to_use].name);
344+
} else {
345+
break :init comptime parser.number();
346+
}
347+
};
289348

290349
if (comptime parser.char()) |ch| {
291-
@compileError("extraneous trailing character '" ++ [1]u8{ch} ++ "'");
350+
@compileError("Extraneous trailing character '" ++ [1]u8{ch} ++ "'");
292351
}
293352

294353
const arg_to_print = comptime arg_state.nextArg(opt_pos_arg);
295354
try formatType(
296-
args[arg_to_print],
355+
@field(args, fields_info[arg_to_print].name),
297356
specifier_arg,
298357
options,
299358
writer,
@@ -1937,3 +1996,22 @@ test "null" {
19371996
const inst = null;
19381997
try testFmt("null", "{}", .{inst});
19391998
}
1999+
2000+
test "named arguments" {
2001+
try testFmt("hello world!", "{} world{c}", .{ "hello", '!' });
2002+
try testFmt("hello world!", "{[greeting]} world{[punctuation]c}", .{ .punctuation = '!', .greeting = "hello" });
2003+
try testFmt("hello world!", "{[1]} world{[0]c}", .{ '!', "hello" });
2004+
}
2005+
2006+
test "runtime width specifier" {
2007+
var width: usize = 9;
2008+
try testFmt("~~hello~~", "{:~^[1]}", .{ "hello", width });
2009+
try testFmt("~~hello~~", "{:~^[width]}", .{ .string = "hello", .width = width });
2010+
}
2011+
2012+
test "runtime precision specifier" {
2013+
var number: f32 = 3.1415;
2014+
var precision: usize = 2;
2015+
try testFmt("3.14e+00", "{:1.[1]}", .{ number, precision });
2016+
try testFmt("3.14e+00", "{:1.[precision]}", .{ .number = number, .precision = precision });
2017+
}

0 commit comments

Comments
 (0)