authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-03-13 19:41:50 -07:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-03-13 19:41:50 -07:00
log688f401934b1c4f1ed855d2bc1ba04bc61e71f7e
treed5207c87182977375b77584996df0a999909c72a
parente75714203d4aadd2e5e12dffc6ea18b62c372204
signaturebadge-check Signed by SSH key SHA256:4hHJbtBRU58AYXwjL7fkz2fnQHdiye8x1QpTCQ0sHNw

add Writable.print


3 files changed, 964 insertions(+), 0 deletions(-)

fmt.zig created+957
......@@ -0,0 +1,957 @@
1//! String formatting and parsing.
2
3const std = @import("std");
4const builtin = @import("builtin");
5const extras = @import("extras");
6
7/// Renders fmt string with args, calling `writer` with slices of bytes.
8/// If `writer` returns an error, the error is returned from `format` and
9/// `writer` is not called again.
10///
11/// The format string must be comptime-known and may contain placeholders following
12/// this format:
13/// `{[argument][specifier]:[fill][alignment][width].[precision]}`
14///
15/// Above, each word including its surrounding [ and ] is a parameter which you have to replace with something:
16///
17/// - *argument* is either the numeric index or the field name of the argument that should be inserted
18/// - when using a field name, you are required to enclose the field name (an identifier) in square
19/// brackets, e.g. {[score]...} as opposed to the numeric index form which can be written e.g. {2...}
20/// - *specifier* is a type-dependent formatting option that determines how a type should formatted (see below)
21/// - *fill* is a single unicode codepoint which is used to pad the formatted text
22/// - *alignment* is one of the three bytes '<', '^', or '>' to make the text left-, center-, or right-aligned, respectively
23/// - *width* is the total width of the field in unicode codepoints
24/// - *precision* specifies how many decimals a formatted number should have
25///
26/// Note that most of the parameters are optional and may be omitted. Also you can leave out separators like `:` and `.` when
27/// all parameters after the separator are omitted.
28/// Only exception is the *fill* parameter. If a non-zero *fill* character is required at the same time as *width* is specified,
29/// one has to specify *alignment* as well, as otherwise the digit following `:` is interpreted as *width*, not *fill*.
30///
31/// The *specifier* has several options for types:
32/// - `x` and `X`: output numeric value in hexadecimal notation
33/// - `s`:
34/// - for pointer-to-many and C pointers of u8, print as a C-string using zero-termination
35/// - for slices of u8, print the entire slice as a string without zero-termination
36/// - `e`: output floating point value in scientific notation
37/// - `d`: output numeric value in decimal notation
38/// - `b`: output integer value in binary notation
39/// - `o`: output integer value in octal notation
40/// - `c`: output integer as an ASCII character. Integer type must have 8 bits at max.
41/// - `u`: output integer as an UTF-8 sequence. Integer type must have 21 bits at max.
42/// - `?`: output optional value as either the unwrapped value, or `null`; may be followed by a format specifier for the underlying value.
43/// - `!`: output error union value as either the unwrapped value, or the formatted error value; may be followed by a format specifier for the underlying value.
44/// - `*`: output the address of the value instead of the value itself.
45/// - `any`: output a value of any type using its default format.
46///
47/// If a formatted user type contains a function of the type
48/// ```
49/// fn format(value: ?, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void
50/// ```
51/// with `?` being the type formatted, this function will be called instead of the default implementation.
52/// This allows user types to be formatted in a logical manner instead of dumping all fields of the type.
53///
54/// A user type may be a `struct`, `vector`, `union` or `enum` type.
55///
56/// To print literal curly braces, escape them by writing them twice, e.g. `{{` or `}}`.
57pub fn format(
58 writer: anytype,
59 comptime fmt: []const u8,
60 args: anytype,
61) !void {
62 const ArgsType = @TypeOf(args);
63 const args_type_info = @typeInfo(ArgsType);
64 if (args_type_info != .@"struct") {
65 @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
66 }
67
68 const fields_info = args_type_info.@"struct".fields;
69 if (fields_info.len > max_format_args) {
70 @compileError("32 arguments max are supported per format call");
71 }
72
73 @setEvalBranchQuota(2000000);
74 comptime var arg_state: ArgState = .{ .args_len = fields_info.len };
75 comptime var i = 0;
76 comptime var literal: []const u8 = "";
77 inline while (true) {
78 const start_index = i;
79
80 inline while (i < fmt.len) : (i += 1) {
81 switch (fmt[i]) {
82 '{', '}' => break,
83 else => {},
84 }
85 }
86
87 comptime var end_index = i;
88 comptime var unescape_brace = false;
89
90 // Handle {{ and }}, those are un-escaped as single braces
91 if (i + 1 < fmt.len and fmt[i + 1] == fmt[i]) {
92 unescape_brace = true;
93 // Make the first brace part of the literal...
94 end_index += 1;
95 // ...and skip both
96 i += 2;
97 }
98
99 literal = literal ++ fmt[start_index..end_index];
100
101 // We've already skipped the other brace, restart the loop
102 if (unescape_brace) continue;
103
104 // Write out the literal
105 if (literal.len != 0) {
106 try writer.writeAll(literal);
107 literal = "";
108 }
109
110 if (i >= fmt.len) break;
111
112 if (fmt[i] == '}') {
113 @compileError("missing opening {");
114 }
115
116 // Get past the {
117 comptime std.debug.assert(fmt[i] == '{');
118 i += 1;
119
120 const fmt_begin = i;
121 // Find the closing brace
122 inline while (i < fmt.len and fmt[i] != '}') : (i += 1) {}
123 const fmt_end = i;
124
125 if (i >= fmt.len) {
126 @compileError("missing closing }");
127 }
128
129 // Get past the }
130 comptime std.debug.assert(fmt[i] == '}');
131 i += 1;
132
133 const placeholder = comptime Placeholder.parse(fmt[fmt_begin..fmt_end].*);
134 const arg_pos = comptime switch (placeholder.arg) {
135 .none => null,
136 .number => |pos| pos,
137 .named => |arg_name| std.meta.fieldIndex(ArgsType, arg_name) orelse
138 @compileError("no argument with name '" ++ arg_name ++ "'"),
139 };
140
141 const width = switch (placeholder.width) {
142 .none => null,
143 .number => |v| v,
144 .named => |arg_name| blk: {
145 const arg_i = comptime std.meta.fieldIndex(ArgsType, arg_name) orelse
146 @compileError("no argument with name '" ++ arg_name ++ "'");
147 _ = comptime arg_state.nextArg(arg_i) orelse @compileError("too few arguments");
148 break :blk @field(args, arg_name);
149 },
150 };
151
152 const precision = switch (placeholder.precision) {
153 .none => null,
154 .number => |v| v,
155 .named => |arg_name| blk: {
156 const arg_i = comptime std.meta.fieldIndex(ArgsType, arg_name) orelse
157 @compileError("no argument with name '" ++ arg_name ++ "'");
158 _ = comptime arg_state.nextArg(arg_i) orelse @compileError("too few arguments");
159 break :blk @field(args, arg_name);
160 },
161 };
162
163 const arg_to_print = comptime arg_state.nextArg(arg_pos) orelse @compileError("too few arguments");
164
165 try formatType(
166 @field(args, fields_info[arg_to_print].name),
167 placeholder.specifier_arg,
168 FormatOptions{
169 .fill = placeholder.fill,
170 .alignment = placeholder.alignment,
171 .width = width,
172 .precision = precision,
173 },
174 writer,
175 std.options.fmt_max_depth,
176 );
177 }
178
179 if (comptime arg_state.hasUnusedArgs()) {
180 const missing_count = arg_state.args_len - @popCount(arg_state.used_args);
181 switch (missing_count) {
182 0 => unreachable,
183 1 => @compileError("unused argument in '" ++ fmt ++ "'"),
184 else => @compileError(comptimePrint("{d}", .{missing_count}) ++ " unused arguments in '" ++ fmt ++ "'"),
185 }
186 }
187}
188
189pub const FormatOptions = struct {
190 precision: ?usize = null,
191 width: ?usize = null,
192 alignment: Alignment = default_alignment,
193 fill: u21 = default_fill_char,
194};
195
196pub const Alignment = enum { left, center, right };
197pub const Case = enum { lower, upper };
198
199const default_max_depth = 3;
200const default_alignment = .right;
201const default_fill_char = ' ';
202
203const ArgSetType = u32;
204const max_format_args = @typeInfo(ArgSetType).int.bits;
205
206pub const ArgState = struct {
207 next_arg: usize = 0,
208 used_args: ArgSetType = 0,
209 args_len: usize,
210
211 fn hasUnusedArgs(self: *@This()) bool {
212 return @popCount(self.used_args) != self.args_len;
213 }
214
215 fn nextArg(self: *@This(), arg_index: ?usize) ?usize {
216 const next_index = arg_index orelse init: {
217 const arg = self.next_arg;
218 self.next_arg += 1;
219 break :init arg;
220 };
221
222 if (next_index >= self.args_len) {
223 return null;
224 }
225
226 // Mark this argument as used
227 self.used_args |= @as(ArgSetType, 1) << @as(u5, @intCast(next_index));
228 return next_index;
229 }
230};
231
232pub const Placeholder = struct {
233 specifier_arg: []const u8,
234 fill: u21,
235 alignment: Alignment,
236 arg: Specifier,
237 width: Specifier,
238 precision: Specifier,
239
240 fn parse(comptime str: anytype) Placeholder {
241 const view = std.unicode.Utf8View.initComptime(&str);
242 comptime var parser = Parser{
243 .iter = view.iterator(),
244 };
245
246 // Parse the positional argument number
247 const arg = comptime parser.specifier() catch |err|
248 @compileError(@errorName(err));
249
250 // Parse the format specifier
251 const specifier_arg = comptime parser.until(':');
252
253 // Skip the colon, if present
254 if (comptime parser.char()) |ch| {
255 if (ch != ':') {
256 @compileError("expected : or }, found '" ++ std.unicode.utf8EncodeComptime(ch) ++ "'");
257 }
258 }
259
260 // Parse the fill character, if present.
261 // When the width field is also specified, the fill character must
262 // be followed by an alignment specifier, unless it's '0' (zero)
263 // (in which case it's handled as part of the width specifier)
264 var fill: ?u21 = comptime if (parser.peek(1)) |ch|
265 switch (ch) {
266 '<', '^', '>' => parser.char(),
267 else => null,
268 }
269 else
270 null;
271
272 // Parse the alignment parameter
273 const alignment: ?Alignment = comptime if (parser.peek(0)) |ch| init: {
274 switch (ch) {
275 '<', '^', '>' => {
276 // consume the character
277 break :init switch (parser.char().?) {
278 '<' => .left,
279 '^' => .center,
280 else => .right,
281 };
282 },
283 else => break :init null,
284 }
285 } else null;
286
287 // When none of the fill character and the alignment specifier have
288 // been provided, check whether the width starts with a zero.
289 if (fill == null and alignment == null) {
290 fill = comptime if (parser.peek(0) == '0') '0' else null;
291 }
292
293 // Parse the width parameter
294 const width = comptime parser.specifier() catch |err|
295 @compileError(@errorName(err));
296
297 // Skip the dot, if present
298 if (comptime parser.char()) |ch| {
299 if (ch != '.') {
300 @compileError("expected . or }, found '" ++ std.unicode.utf8EncodeComptime(ch) ++ "'");
301 }
302 }
303
304 // Parse the precision parameter
305 const precision = comptime parser.specifier() catch |err|
306 @compileError(@errorName(err));
307
308 if (comptime parser.char()) |ch| {
309 @compileError("extraneous trailing character '" ++ std.unicode.utf8EncodeComptime(ch) ++ "'");
310 }
311
312 return Placeholder{
313 .specifier_arg = cacheString(specifier_arg[0..specifier_arg.len].*),
314 .fill = fill orelse default_fill_char,
315 .alignment = alignment orelse default_alignment,
316 .arg = arg,
317 .width = width,
318 .precision = precision,
319 };
320 }
321};
322
323pub const Specifier = union(enum) {
324 none,
325 number: usize,
326 named: []const u8,
327};
328
329/// A stream based parser for format strings.
330///
331/// Allows to implement formatters compatible with std.fmt without replicating
332/// the standard library behavior.
333pub const Parser = struct {
334 iter: std.unicode.Utf8Iterator,
335
336 // Returns a decimal number or null if the current character is not a
337 // digit
338 fn number(self: *@This()) ?usize {
339 var r: ?usize = null;
340
341 while (self.peek(0)) |code_point| {
342 switch (code_point) {
343 '0'...'9' => {
344 if (r == null) r = 0;
345 r.? *= 10;
346 r.? += code_point - '0';
347 },
348 else => break,
349 }
350 _ = self.iter.nextCodepoint();
351 }
352
353 return r;
354 }
355
356 // Returns a substring of the input starting from the current position
357 // and ending where `ch` is found or until the end if not found
358 fn until(self: *@This(), ch: u21) []const u8 {
359 const start = self.iter.i;
360 while (self.peek(0)) |code_point| {
361 if (code_point == ch)
362 break;
363 _ = self.iter.nextCodepoint();
364 }
365 return self.iter.bytes[start..self.iter.i];
366 }
367
368 // Returns the character pointed to by the iterator if available, or
369 // null otherwise
370 fn char(self: *@This()) ?u21 {
371 if (self.iter.nextCodepoint()) |code_point| {
372 return code_point;
373 }
374 return null;
375 }
376
377 // Returns true if the iterator points to an existing character and
378 // false otherwise
379 fn maybe(self: *@This(), val: u21) bool {
380 if (self.peek(0) == val) {
381 _ = self.iter.nextCodepoint();
382 return true;
383 }
384 return false;
385 }
386
387 // Returns a decimal number or null if the current character is not a
388 // digit
389 fn specifier(self: *@This()) !Specifier {
390 if (self.maybe('[')) {
391 const arg_name = self.until(']');
392
393 if (!self.maybe(']'))
394 return @field(anyerror, "Expected closing ]");
395
396 return Specifier{ .named = arg_name };
397 }
398 if (self.number()) |i|
399 return Specifier{ .number = i };
400
401 return Specifier{ .none = {} };
402 }
403
404 // Returns the n-th next character or null if that's past the end
405 fn peek(self: *@This(), n: usize) ?u21 {
406 const original_i = self.iter.i;
407 defer self.iter.i = original_i;
408
409 var i: usize = 0;
410 var code_point: ?u21 = null;
411 while (i <= n) : (i += 1) {
412 code_point = self.iter.nextCodepoint();
413 if (code_point == null) return null;
414 }
415 return code_point;
416 }
417};
418
419fn cacheString(str: anytype) []const u8 {
420 return &str;
421}
422
423fn formatType(value: anytype, comptime fmt: []const u8, options: FormatOptions, writer: anytype, max_depth: usize) extras.Pointee(@TypeOf(writer)).WriteError!void {
424 const T = @TypeOf(value);
425 const actual_fmt = comptime if (std.mem.eql(u8, fmt, "any"))
426 defaultSpec(T)
427 else if (fmt.len != 0 and (fmt[0] == '?' or fmt[0] == '!')) switch (@typeInfo(T)) {
428 .optional, .error_union => fmt,
429 else => stripOptionalOrErrorUnionSpec(fmt),
430 } else fmt;
431
432 if (comptime std.mem.eql(u8, actual_fmt, "*")) {
433 return formatAddress(value, options, writer);
434 }
435
436 if (std.meta.hasMethod(T, "nprint")) {
437 return value.nprint(writer);
438 }
439
440 switch (@typeInfo(T)) {
441 .comptime_int, .int, .comptime_float, .float => {
442 return formatValue(value, actual_fmt, options, writer);
443 },
444 .void => {
445 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
446 return formatBuf("void", options, writer);
447 },
448 .bool => {
449 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
450 return formatBuf(if (value) "true" else "false", options, writer);
451 },
452 .optional => {
453 if (actual_fmt.len == 0 or actual_fmt[0] != '?')
454 @compileError("cannot format optional without a specifier (i.e. {?} or {any})");
455 const remaining_fmt = comptime stripOptionalOrErrorUnionSpec(actual_fmt);
456 if (value) |payload| {
457 return formatType(payload, remaining_fmt, options, writer, max_depth);
458 } else {
459 return formatBuf("null", options, writer);
460 }
461 },
462 .error_union => {
463 if (actual_fmt.len == 0 or actual_fmt[0] != '!')
464 @compileError("cannot format error union without a specifier (i.e. {!} or {any})");
465 const remaining_fmt = comptime stripOptionalOrErrorUnionSpec(actual_fmt);
466 if (value) |payload| {
467 return formatType(payload, remaining_fmt, options, writer, max_depth);
468 } else |err| {
469 return formatType(err, "", options, writer, max_depth);
470 }
471 },
472 .error_set => {
473 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
474 try writer.writeAll("error.");
475 return writer.writeAll(@errorName(value));
476 },
477 .@"enum" => |enumInfo| {
478 try writer.writeAll(@typeName(T));
479 if (enumInfo.is_exhaustive) {
480 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
481 try writer.writeAll(".");
482 try writer.writeAll(@tagName(value));
483 return;
484 }
485
486 // Use @tagName only if value is one of known fields
487 @setEvalBranchQuota(3 * enumInfo.fields.len);
488 inline for (enumInfo.fields) |enumField| {
489 if (@intFromEnum(value) == enumField.value) {
490 try writer.writeAll(".");
491 try writer.writeAll(@tagName(value));
492 return;
493 }
494 }
495
496 try writer.writeAll("(");
497 try formatType(@intFromEnum(value), actual_fmt, options, writer, max_depth);
498 try writer.writeAll(")");
499 },
500 .@"union" => |info| {
501 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
502 try writer.writeAll(@typeName(T));
503 if (max_depth == 0) {
504 return writer.writeAll("{ ... }");
505 }
506 if (info.tag_type) |UnionTagType| {
507 try writer.writeAll("{ .");
508 try writer.writeAll(@tagName(@as(UnionTagType, value)));
509 try writer.writeAll(" = ");
510 inline for (info.fields) |u_field| {
511 if (value == @field(UnionTagType, u_field.name)) {
512 try formatType(@field(value, u_field.name), "any", options, writer, max_depth - 1);
513 }
514 }
515 try writer.writeAll(" }");
516 } else {
517 try format(writer, "@{x}", .{@intFromPtr(&value)});
518 }
519 },
520 .@"struct" => |info| {
521 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
522 if (info.is_tuple) {
523 // Skip the type and field names when formatting tuples.
524 if (max_depth == 0) {
525 return writer.writeAll("{ ... }");
526 }
527 try writer.writeAll("{");
528 inline for (info.fields, 0..) |f, i| {
529 if (i == 0) {
530 try writer.writeAll(" ");
531 } else {
532 try writer.writeAll(", ");
533 }
534 try formatType(@field(value, f.name), "any", options, writer, max_depth - 1);
535 }
536 return writer.writeAll(" }");
537 }
538 try writer.writeAll(@typeName(T));
539 if (max_depth == 0) {
540 return writer.writeAll("{ ... }");
541 }
542 try writer.writeAll("{");
543 inline for (info.fields, 0..) |f, i| {
544 if (i == 0) {
545 try writer.writeAll(" .");
546 } else {
547 try writer.writeAll(", .");
548 }
549 try writer.writeAll(f.name);
550 try writer.writeAll(" = ");
551 try formatType(@field(value, f.name), "any", options, writer, max_depth - 1);
552 }
553 try writer.writeAll(" }");
554 },
555 .pointer => |ptr_info| switch (ptr_info.size) {
556 .one => switch (@typeInfo(ptr_info.child)) {
557 .array, .@"enum", .@"union", .@"struct" => {
558 return formatType(value.*, actual_fmt, options, writer, max_depth);
559 },
560 else => return format(writer, "{s}@{x}", .{ @typeName(ptr_info.child), @intFromPtr(value) }),
561 },
562 .many, .c => {
563 if (actual_fmt.len == 0)
564 @compileError("cannot format pointer without a specifier (i.e. {s} or {*})");
565 if (ptr_info.sentinel() != null) {
566 return formatType(std.mem.span(value), actual_fmt, options, writer, max_depth);
567 }
568 if (actual_fmt[0] == 's' and ptr_info.child == u8) {
569 return formatBuf(std.mem.span(value), options, writer);
570 }
571 invalidFmtError(fmt, value);
572 },
573 .slice => {
574 if (actual_fmt.len == 0)
575 @compileError("cannot format slice without a specifier (i.e. {s} or {any})");
576 if (max_depth == 0) {
577 return writer.writeAll("{ ... }");
578 }
579 if (actual_fmt[0] == 's' and ptr_info.child == u8) {
580 return formatBuf(value, options, writer);
581 }
582 try writer.writeAll("{ ");
583 for (value, 0..) |elem, i| {
584 try formatType(elem, actual_fmt, options, writer, max_depth - 1);
585 if (i != value.len - 1) {
586 try writer.writeAll(", ");
587 }
588 }
589 try writer.writeAll(" }");
590 },
591 },
592 .array => |info| {
593 if (actual_fmt.len == 0)
594 @compileError("cannot format array without a specifier (i.e. {s} or {any})");
595 if (max_depth == 0) {
596 return writer.writeAll("{ ... }");
597 }
598 if (actual_fmt[0] == 's' and info.child == u8) {
599 return formatBuf(&value, options, writer);
600 }
601 try writer.writeAll("{ ");
602 for (value, 0..) |elem, i| {
603 try formatType(elem, actual_fmt, options, writer, max_depth - 1);
604 if (i < value.len - 1) {
605 try writer.writeAll(", ");
606 }
607 }
608 try writer.writeAll(" }");
609 },
610 .vector => |info| {
611 if (max_depth == 0) {
612 return writer.writeAll("{ ... }");
613 }
614 try writer.writeAll("{ ");
615 var i: usize = 0;
616 while (i < info.len) : (i += 1) {
617 try formatType(value[i], actual_fmt, options, writer, max_depth - 1);
618 if (i < info.len - 1) {
619 try writer.writeAll(", ");
620 }
621 }
622 try writer.writeAll(" }");
623 },
624 .@"fn" => @compileError("unable to format function body type, use '*const " ++ @typeName(T) ++ "' for a function pointer type"),
625 .type => {
626 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
627 return formatBuf(@typeName(value), options, writer);
628 },
629 .enum_literal => {
630 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
631 const buffer = [_]u8{'.'} ++ @tagName(value);
632 return formatBuf(buffer, options, writer);
633 },
634 .null => {
635 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
636 return formatBuf("null", options, writer);
637 },
638 else => @compileError("unable to format type '" ++ @typeName(T) ++ "'"),
639 }
640}
641
642pub inline fn comptimePrint(comptime fmt: []const u8, args: anytype) *const [count(fmt, args):0]u8 {
643 comptime {
644 var buf: [count(fmt, args):0]u8 = undefined;
645 _ = bufPrint(&buf, fmt, args) catch unreachable;
646 buf[buf.len] = 0;
647 const final = buf;
648 return &final;
649 }
650}
651
652fn defaultSpec(comptime T: type) [:0]const u8 {
653 switch (@typeInfo(T)) {
654 .array, .vector => return "any",
655 .pointer => |ptr_info| switch (ptr_info.size) {
656 .one => switch (@typeInfo(ptr_info.child)) {
657 .array => return "any",
658 else => {},
659 },
660 .many, .c => return "*",
661 .slice => return "any",
662 },
663 .optional => |info| return "?" ++ defaultSpec(info.child),
664 .error_union => |info| return "!" ++ defaultSpec(info.payload),
665 else => {},
666 }
667 return "";
668}
669
670fn stripOptionalOrErrorUnionSpec(comptime fmt: []const u8) []const u8 {
671 return if (std.mem.eql(u8, fmt[1..], "any"))
672 "any"
673 else
674 fmt[1..];
675}
676
677fn formatAddress(value: anytype, options: FormatOptions, writer: anytype) @TypeOf(writer).Error!void {
678 _ = options;
679 const T = @TypeOf(value);
680
681 switch (@typeInfo(T)) {
682 .pointer => |info| {
683 try writer.writeAll(@typeName(info.child) ++ "@");
684 if (info.size == .slice)
685 try formatInt(@intFromPtr(value.ptr), 16, .lower, FormatOptions{}, writer)
686 else
687 try formatInt(@intFromPtr(value), 16, .lower, FormatOptions{}, writer);
688 return;
689 },
690 .optional => |info| {
691 if (@typeInfo(info.child) == .pointer) {
692 try writer.writeAll(@typeName(info.child) ++ "@");
693 try formatInt(@intFromPtr(value), 16, .lower, FormatOptions{}, writer);
694 return;
695 }
696 },
697 else => {},
698 }
699
700 @compileError("cannot format non-pointer type " ++ @typeName(T) ++ " with * specifier");
701}
702
703pub fn formatInt(
704 value: anytype,
705 base: u8,
706 case: Case,
707 options: FormatOptions,
708 writer: anytype,
709) !void {
710 std.debug.assert(base >= 2);
711
712 const int_value = if (@TypeOf(value) == comptime_int) blk: {
713 const Int = std.math.IntFittingRange(value, value);
714 break :blk @as(Int, value);
715 } else value;
716
717 const value_info = @typeInfo(@TypeOf(int_value)).int;
718
719 // The type must have the same size as `base` or be wider in order for the
720 // division to work
721 const min_int_bits = comptime @max(value_info.bits, 8);
722 const MinInt = std.meta.Int(.unsigned, min_int_bits);
723
724 const abs_value = @abs(int_value);
725 // The worst case in terms of space needed is base 2, plus 1 for the sign
726 var buf: [1 + @max(@as(comptime_int, value_info.bits), 1)]u8 = undefined;
727
728 var a: MinInt = abs_value;
729 var index: usize = buf.len;
730
731 if (base == 10) {
732 while (a >= 100) : (a = @divTrunc(a, 100)) {
733 index -= 2;
734 buf[index..][0..2].* = digits2(@intCast(a % 100));
735 }
736
737 if (a < 10) {
738 index -= 1;
739 buf[index] = '0' + @as(u8, @intCast(a));
740 } else {
741 index -= 2;
742 buf[index..][0..2].* = digits2(@intCast(a));
743 }
744 } else {
745 while (true) {
746 const digit = a % base;
747 index -= 1;
748 buf[index] = digitToChar(@intCast(digit), case);
749 a /= base;
750 if (a == 0) break;
751 }
752 }
753
754 if (value_info.signedness == .signed) {
755 if (value < 0) {
756 // Negative integer
757 index -= 1;
758 buf[index] = '-';
759 } else if (options.width == null or options.width.? == 0) {
760 // Positive integer, omit the plus sign
761 } else {
762 // Positive integer
763 index -= 1;
764 buf[index] = '+';
765 }
766 }
767
768 return formatBuf(buf[index..], options, writer);
769}
770
771fn formatValue(
772 value: anytype,
773 comptime fmt: []const u8,
774 options: FormatOptions,
775 writer: anytype,
776) !void {
777 const T = @TypeOf(value);
778 switch (@typeInfo(T)) {
779 // .float, .comptime_float => return formatFloatValue(value, fmt, options, writer),
780 .float, .comptime_float => @compileError("TODO"),
781 .int, .comptime_int => return formatIntValue(value, fmt, options, writer),
782 .bool => return formatBuf(if (value) "true" else "false", options, writer),
783 else => comptime unreachable,
784 }
785}
786
787fn invalidFmtError(comptime fmt: []const u8, value: anytype) void {
788 @compileError("invalid format string '" ++ fmt ++ "' for type '" ++ @typeName(@TypeOf(value)) ++ "'");
789}
790
791fn formatBuf(
792 buf: []const u8,
793 options: FormatOptions,
794 writer: anytype,
795) !void {
796 if (options.width) |min_width| {
797 // In case of error assume the buffer content is ASCII-encoded
798 const width = std.unicode.utf8CountCodepoints(buf) catch buf.len;
799 const padding = if (width < min_width) min_width - width else 0;
800
801 if (padding == 0)
802 return writer.writeAll(buf);
803
804 var fill_buffer: [4]u8 = undefined;
805 const fill_utf8 = if (std.unicode.utf8Encode(options.fill, &fill_buffer)) |len|
806 fill_buffer[0..len]
807 else |err| switch (err) {
808 error.Utf8CannotEncodeSurrogateHalf,
809 error.CodepointTooLarge,
810 => &std.unicode.utf8EncodeComptime(std.unicode.replacement_character),
811 };
812 switch (options.alignment) {
813 .left => {
814 try writer.writeAll(buf);
815 try writer.writeNTimes(fill_utf8, padding);
816 },
817 .center => {
818 const left_padding = padding / 2;
819 const right_padding = (padding + 1) / 2;
820 try writer.writeNTimes(fill_utf8, left_padding);
821 try writer.writeAll(buf);
822 try writer.writeNTimes(fill_utf8, right_padding);
823 },
824 .right => {
825 try writer.writeNTimes(fill_utf8, padding);
826 try writer.writeAll(buf);
827 },
828 }
829 } else {
830 // Fast path, avoid counting the number of codepoints
831 try writer.writeAll(buf);
832 }
833}
834
835/// Count the characters needed for format. Useful for preallocating memory
836fn count(comptime fmt: []const u8, args: anytype) u64 {
837 var counting_writer = std.io.countingWriter(std.io.null_writer);
838 format(counting_writer.writer().any(), fmt, args) catch unreachable;
839 return counting_writer.bytes_written;
840}
841
842/// Print a Formatter string into `buf`. Actually just a thin wrapper around `format` and `fixedBufferStream`.
843/// Returns a slice of the bytes printed to.
844fn bufPrint(buf: []u8, comptime fmt: []const u8, args: anytype) ![]u8 {
845 var fbs = std.io.fixedBufferStream(buf);
846 format(fbs.writer().any(), fmt, args) catch |err| switch (err) {
847 error.NoSpaceLeft => return error.NoSpaceLeft,
848 else => unreachable,
849 };
850 return fbs.getWritten();
851}
852
853const digits2_alphabet = blk: {
854 var data: [200]u8 = @splat(0);
855 for (0..10) |m| {
856 for (0..10) |n| {
857 data[m * 10 + n + 0] = m + '0';
858 data[m * 10 + n + 1] = n + '0';
859 }
860 }
861 const result = data;
862 break :blk result;
863};
864
865/// Converts values in the range [0, 100) to a base 10 string.
866fn digits2(value: u8) [2]u8 {
867 return digits2_alphabet[value * 2 ..][0..2].*;
868}
869
870fn digitToChar(digit: u8, case: Case) u8 {
871 return switch (digit) {
872 0...9 => digit + '0',
873 10...35 => digit + ((if (case == .upper) @as(u8, 'A') else @as(u8, 'a')) - 10),
874 else => unreachable,
875 };
876}
877
878fn formatIntValue(value: anytype, comptime fmt: []const u8, options: FormatOptions, writer: anytype) !void {
879 comptime var base = 10;
880 comptime var case: Case = .lower;
881
882 const int_value = if (@TypeOf(value) == comptime_int) blk: {
883 const Int = std.math.IntFittingRange(value, value);
884 break :blk @as(Int, value);
885 } else value;
886
887 if (fmt.len == 0 or comptime std.mem.eql(u8, fmt, "d")) {
888 base = 10;
889 case = .lower;
890 } else if (comptime std.mem.eql(u8, fmt, "c")) {
891 if (@typeInfo(@TypeOf(int_value)).int.bits <= 8) {
892 return formatAsciiChar(@as(u8, int_value), options, writer);
893 } else {
894 @compileError("cannot print integer that is larger than 8 bits as an ASCII character");
895 }
896 } else if (comptime std.mem.eql(u8, fmt, "u")) {
897 if (@typeInfo(@TypeOf(int_value)).int.bits <= 21) {
898 return formatUnicodeCodepoint(@as(u21, int_value), options, writer);
899 } else {
900 @compileError("cannot print integer that is larger than 21 bits as an UTF-8 sequence");
901 }
902 } else if (comptime std.mem.eql(u8, fmt, "b")) {
903 base = 2;
904 case = .lower;
905 } else if (comptime std.mem.eql(u8, fmt, "x")) {
906 base = 16;
907 case = .lower;
908 } else if (comptime std.mem.eql(u8, fmt, "X")) {
909 base = 16;
910 case = .upper;
911 } else if (comptime std.mem.eql(u8, fmt, "o")) {
912 base = 8;
913 case = .lower;
914 } else {
915 invalidFmtError(fmt, value);
916 }
917
918 return formatInt(int_value, base, case, options, writer);
919}
920
921fn formatAsciiChar(c: u8, options: FormatOptions, writer: anytype) !void {
922 return formatBuf(@as(*const [1]u8, &c), options, writer);
923}
924
925fn formatUnicodeCodepoint(c: u21, options: FormatOptions, writer: anytype) !void {
926 var buf: [4]u8 = undefined;
927 const len = std.unicode.utf8Encode(c, &buf) catch |err| switch (err) {
928 error.Utf8CannotEncodeSurrogateHalf, error.CodepointTooLarge => {
929 return formatBuf(&std.unicode.utf8EncodeComptime(std.unicode.replacement_character), options, writer);
930 },
931 };
932 return formatBuf(buf[0..len], options, writer);
933}
934
935fn formatFloatValue(value: anytype, comptime fmt: []const u8, options: FormatOptions, writer: anytype) !void {
936 var buf: [std.fmt.format_float.bufferSize(.decimal, f64)]u8 = undefined;
937
938 if (fmt.len == 0 or comptime std.mem.eql(u8, fmt, "e")) {
939 const s = std.fmt.formatFloat(&buf, value, .{ .mode = .scientific, .precision = options.precision }) catch |err| switch (err) {
940 error.BufferTooSmall => "(float)",
941 };
942 return formatBuf(s, options, writer);
943 } else if (comptime std.mem.eql(u8, fmt, "d")) {
944 const s = std.fmt.formatFloat(&buf, value, .{ .mode = .decimal, .precision = options.precision }) catch |err| switch (err) {
945 error.BufferTooSmall => "(float)",
946 };
947 return formatBuf(s, options, writer);
948 } else if (comptime std.mem.eql(u8, fmt, "x")) {
949 var buf_stream = std.io.fixedBufferStream(&buf);
950 std.fmt.formatFloatHexadecimal(value, options, buf_stream.writer()) catch |err| switch (err) {
951 error.NoSpaceLeft => unreachable,
952 };
953 return formatBuf(buf_stream.getWritten(), options, writer);
954 } else {
955 invalidFmtError(fmt, value);
956 }
957}
nio.zig+6
......@@ -3,6 +3,8 @@ const builtin = @import("builtin");
33const extras = @import("extras");
44const sys_linux = @import("sys-linux");
55
6pub const fmt = @import("./fmt.zig");
7
68const sys = switch (builtin.target.os.tag) {
79 .linux => sys_linux,
810 else => unreachable,
......@@ -271,6 +273,10 @@ pub fn Writable(T: type, this_kind: enum { _var, _const, _bare }) type {
271273
272274 return writeAll(self, buf[index..]);
273275 }
276
277 pub fn print(self: Self, comptime format: []const u8, args: anytype) Error!void {
278 return fmt.format(self, format, args);
279 }
274280 };
275281}
276282
zigmod.yml+1
......@@ -6,3 +6,4 @@ description: Nektro's IO, an alternative to std.io
66min_zig_version: 0.14.0
77dependencies:
88 - src: git https://github.com/nektro/zig-sys-linux
9 - src: git https://github.com/nektro/zig-extras