Skip to content

Commit 17a7a62

Browse files
committed
std: Implement named arguments & runtime width/precision
1 parent 47f9b39 commit 17a7a62

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;
@@ -78,43 +79,48 @@ pub fn format(
7879
args: anytype,
7980
) !void {
8081
const ArgSetType = u32;
81-
if (@typeInfo(@TypeOf(args)) != .Struct) {
82-
@compileError("Expected tuple or struct argument, found " ++ @typeName(@TypeOf(args)));
82+
83+
const ArgsType = @TypeOf(args);
84+
// XXX: meta.trait.is(.Struct)(ArgsType) doesn't seem to work...
85+
if (@typeInfo(ArgsType) != .Struct) {
86+
@compileError("Expected tuple or struct argument, found " ++ @typeName(ArgsType));
8387
}
84-
if (args.len > @typeInfo(ArgSetType).Int.bits) {
88+
89+
const fields_info = meta.fields(ArgsType);
90+
if (fields_info.len > @typeInfo(ArgSetType).Int.bits) {
8591
@compileError("32 arguments max are supported per format call");
8692
}
8793

8894
comptime var arg_state: struct {
8995
next_arg: usize = 0,
90-
used_args: ArgSetType = 0,
91-
args_len: usize = args.len,
96+
used_args: usize = 0,
97+
args_len: usize = fields_info.len,
9298

9399
fn hasUnusedArgs(comptime self: *@This()) bool {
94-
return (@popCount(ArgSetType, self.used_args) != self.args_len);
100+
return @popCount(ArgSetType, self.used_args) != self.args_len;
95101
}
96102

97-
fn nextArg(comptime self: *@This(), comptime pos_arg: ?usize) comptime_int {
98-
const next_idx = pos_arg orelse blk: {
103+
fn nextArg(comptime self: *@This(), comptime arg_index: ?usize) comptime_int {
104+
const next_index = arg_index orelse init: {
99105
const arg = self.next_arg;
100106
self.next_arg += 1;
101-
break :blk arg;
107+
break :init arg;
102108
};
103109

104-
if (next_idx >= self.args_len) {
110+
if (next_index >= self.args_len) {
105111
@compileError("Too few arguments");
106112
}
107113

108114
// Mark this argument as used
109-
self.used_args |= 1 << next_idx;
115+
self.used_args |= 1 << next_index;
110116

111-
return next_idx;
117+
return next_index;
112118
}
113119
} = .{};
114120

115121
comptime var parser: struct {
116122
buf: []const u8 = undefined,
117-
pos: usize = 0,
123+
pos: comptime_int = 0,
118124

119125
// Returns a decimal number or null if the current character is not a
120126
// digit
@@ -159,13 +165,21 @@ pub fn format(
159165
return null;
160166
}
161167

168+
fn maybe(comptime self: *@This(), comptime val: u8) bool {
169+
if (self.pos < self.buf.len and self.buf[self.pos] == val) {
170+
self.pos += 1;
171+
return true;
172+
}
173+
return false;
174+
}
175+
162176
// Returns the n-th next character or null if that's past the end
163177
fn peek(comptime self: *@This(), comptime n: usize) ?u8 {
164178
return if (self.pos + n < self.buf.len) self.buf[self.pos + n] else null;
165179
}
166180
} = .{};
167181

168-
comptime var options: FormatOptions = .{};
182+
var options: FormatOptions = .{};
169183

170184
@setEvalBranchQuota(2000000);
171185

@@ -203,7 +217,7 @@ pub fn format(
203217
if (i >= fmt.len) break;
204218

205219
if (fmt[i] == '}') {
206-
@compileError("missing opening {");
220+
@compileError("Missing opening {");
207221
}
208222

209223
// Get past the {
@@ -216,7 +230,7 @@ pub fn format(
216230
comptime const fmt_end = i;
217231

218232
if (i >= fmt.len) {
219-
@compileError("missing closing }");
233+
@compileError("Missing closing }");
220234
}
221235

222236
// Get past the }
@@ -230,15 +244,29 @@ pub fn format(
230244
parser.pos = 0;
231245

232246
// Parse the positional argument number
233-
comptime var opt_pos_arg = comptime parser.number();
247+
comptime const opt_pos_arg = init: {
248+
if (comptime parser.maybe('[')) {
249+
comptime const arg_name = parser.until(']');
250+
251+
if (!comptime parser.maybe(']')) {
252+
@compileError("Expected closing ]");
253+
}
254+
255+
break :init comptime meta.fieldIndex(ArgsType, arg_name) orelse
256+
@compileError("No argument with name '" ++ arg_name ++ "'");
257+
} else {
258+
break :init comptime parser.number();
259+
}
260+
};
234261

235262
// Parse the format specifier
236-
comptime var specifier_arg = comptime parser.until(':');
263+
comptime const specifier_arg = comptime parser.until(':');
237264

238265
// Skip the colon, if present
239266
if (comptime parser.char()) |ch| {
240-
if (ch != ':')
241-
@compileError("expected : or }, found '" ++ [1]u8{ch} ++ "'");
267+
if (ch != ':') {
268+
@compileError("Expected : or }, found '" ++ [1]u8{ch} ++ "'");
269+
}
242270
}
243271

244272
// Parse the fill character
@@ -270,26 +298,57 @@ pub fn format(
270298
}
271299

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

276319
// Skip the dot, if present
277320
if (comptime parser.char()) |ch| {
278-
if (ch != '.')
279-
@compileError("expected . or }, found '" ++ [1]u8{ch} ++ "'");
321+
if (ch != '.') {
322+
@compileError("Expected . or }, found '" ++ [1]u8{ch} ++ "'");
323+
}
280324
}
281325

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

286345
if (comptime parser.char()) |ch| {
287-
@compileError("extraneous trailing character '" ++ [1]u8{ch} ++ "'");
346+
@compileError("Extraneous trailing character '" ++ [1]u8{ch} ++ "'");
288347
}
289348

290349
const arg_to_print = comptime arg_state.nextArg(opt_pos_arg);
291350
try formatType(
292-
args[arg_to_print],
351+
@field(args, fields_info[arg_to_print].name),
293352
specifier_arg,
294353
options,
295354
writer,
@@ -1803,3 +1862,22 @@ test "sci float padding" {
18031862
try testFmt("center-pad: *3.141e+00*\n", "center-pad: {e:*^11.3}\n", .{number});
18041863
try testFmt("right-pad: 3.141e+00**\n", "right-pad: {e:*<11.3}\n", .{number});
18051864
}
1865+
1866+
test "named arguments" {
1867+
try testFmt("hello world!", "{} world{c}", .{ "hello", '!' });
1868+
try testFmt("hello world!", "{[greeting]} world{[punctuation]c}", .{ .punctuation = '!', .greeting = "hello" });
1869+
try testFmt("hello world!", "{[1]} world{[0]c}", .{ '!', "hello" });
1870+
}
1871+
1872+
test "runtime width specifier" {
1873+
var width: usize = 9;
1874+
try testFmt("~~hello~~", "{:~^[1]}", .{ "hello", width });
1875+
try testFmt("~~hello~~", "{:~^[width]}", .{ .string = "hello", .width = width });
1876+
}
1877+
1878+
test "runtime precision specifier" {
1879+
var number: f32 = 3.1415;
1880+
var precision: usize = 2;
1881+
try testFmt("3.14e+00", "{:1.[1]}", .{ number, precision });
1882+
try testFmt("3.14e+00", "{:1.[precision]}", .{ .number = number, .precision = precision });
1883+
}

0 commit comments

Comments
 (0)