authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-04-28 17:38:10-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-01 16:35:28-07:00
logaef0434c014d85d4f5ab8afa931ea1848c8bbd16
treefe02afbbb88b9c071ea570cf7ff1ae508fb17fcf
parentd8cea032455228a12e3413068aff46b06fb02a59

formatted printing: fix handling of nested format functions


2 files changed, 86 insertions(+), 87 deletions(-)

lib/std/io/BufferedWriter.zig+84-82
...@@ -136,6 +136,15 @@ pub fn writableSliceGreedy(bw: *BufferedWriter, minimum_length: usize) Writer.Er...@@ -136,6 +136,15 @@ pub fn writableSliceGreedy(bw: *BufferedWriter, minimum_length: usize) Writer.Er
136 return bw.buffer[bw.end..];136 return bw.buffer[bw.end..];
137}137}
138138
139pub fn ensureUnusedCapacity(bw: *BufferedWriter, n: usize) Writer.Error!void {
140 _ = try writableSliceGreedy(bw, n);
141}
142
143pub fn undo(bw: *BufferedWriter, n: usize) void {
144 bw.end -= n;
145 bw.count -= n;
146}
147
139/// After calling `writableSliceGreedy`, this function tracks how many bytes148/// After calling `writableSliceGreedy`, this function tracks how many bytes
140/// were written to it.149/// were written to it.
141///150///
...@@ -797,18 +806,13 @@ pub fn printValue(...@@ -797,18 +806,13 @@ pub fn printValue(
797 max_depth: usize,806 max_depth: usize,
798) Writer.Error!void {807) Writer.Error!void {
799 const T = @TypeOf(value);808 const T = @TypeOf(value);
800 const actual_fmt = comptime if (std.mem.eql(u8, fmt, ANY))809
801 defaultFormatString(T)810 if (comptime std.mem.eql(u8, fmt, "*")) {
802 else if (fmt.len != 0 and (fmt[0] == '?' or fmt[0] == '!')) switch (@typeInfo(T)) {
803 .optional, .error_union => fmt,
804 else => stripOptionalOrErrorUnionSpec(fmt),
805 } else fmt;
806
807 if (comptime std.mem.eql(u8, actual_fmt, "*")) {
808 return bw.printAddress(value);811 return bw.printAddress(value);
809 }812 }
810813
811 if (std.meta.hasMethod(T, "format")) {814 const is_any = comptime std.mem.eql(u8, fmt, ANY);
815 if (!is_any and std.meta.hasMethod(T, "format")) {
812 if (fmt.len > 0 and fmt[0] == 'f') {816 if (fmt.len > 0 and fmt[0] == 'f') {
813 return value.format(bw, fmt[1..]);817 return value.format(bw, fmt[1..]);
814 } else if (fmt.len == 0) {818 } else if (fmt.len == 0) {
...@@ -818,20 +822,23 @@ pub fn printValue(...@@ -818,20 +822,23 @@ pub fn printValue(
818 }822 }
819823
820 switch (@typeInfo(T)) {824 switch (@typeInfo(T)) {
821 .float, .comptime_float => return bw.printFloat(actual_fmt, options, value),825 .float, .comptime_float => return bw.printFloat(if (is_any) "d" else fmt, options, value),
822 .int, .comptime_int => return bw.printInt(actual_fmt, options, value),826 .int, .comptime_int => return bw.printInt(if (is_any) "d" else fmt, options, value),
823 .bool => {827 .bool => {
824 if (actual_fmt.len != 0) invalidFmtError(fmt, value);828 if (!is_any and fmt.len != 0) invalidFmtError(fmt, value);
825 return bw.alignBufferOptions(if (value) "true" else "false", options);829 return bw.alignBufferOptions(if (value) "true" else "false", options);
826 },830 },
827 .void => {831 .void => {
828 if (actual_fmt.len != 0) invalidFmtError(fmt, value);832 if (!is_any and fmt.len != 0) invalidFmtError(fmt, value);
829 return bw.alignBufferOptions("void", options);833 return bw.alignBufferOptions("void", options);
830 },834 },
831 .optional => {835 .optional => {
832 if (actual_fmt.len == 0 or actual_fmt[0] != '?')836 const remaining_fmt = comptime if (fmt.len > 0 and fmt[0] == '?')
837 stripOptionalOrErrorUnionSpec(fmt)
838 else if (is_any)
839 ANY
840 else
833 @compileError("cannot print optional without a specifier (i.e. {?} or {any})");841 @compileError("cannot print optional without a specifier (i.e. {?} or {any})");
834 const remaining_fmt = comptime stripOptionalOrErrorUnionSpec(actual_fmt);
835 if (value) |payload| {842 if (value) |payload| {
836 return bw.printValue(remaining_fmt, options, payload, max_depth);843 return bw.printValue(remaining_fmt, options, payload, max_depth);
837 } else {844 } else {
...@@ -839,9 +846,12 @@ pub fn printValue(...@@ -839,9 +846,12 @@ pub fn printValue(
839 }846 }
840 },847 },
841 .error_union => {848 .error_union => {
842 if (actual_fmt.len == 0 or actual_fmt[0] != '!')849 const remaining_fmt = comptime if (fmt.len > 0 and fmt[0] == '!')
843 @compileError("cannot format error union without a specifier (i.e. {!} or {any})");850 stripOptionalOrErrorUnionSpec(fmt)
844 const remaining_fmt = comptime stripOptionalOrErrorUnionSpec(actual_fmt);851 else if (is_any)
852 ANY
853 else
854 @compileError("cannot print error union without a specifier (i.e. {!} or {any})");
845 if (value) |payload| {855 if (value) |payload| {
846 return bw.printValue(remaining_fmt, options, payload, max_depth);856 return bw.printValue(remaining_fmt, options, payload, max_depth);
847 } else |err| {857 } else |err| {
...@@ -849,40 +859,43 @@ pub fn printValue(...@@ -849,40 +859,43 @@ pub fn printValue(
849 }859 }
850 },860 },
851 .error_set => {861 .error_set => {
852 if (actual_fmt.len > 0 and actual_fmt[0] == 's') {862 if (fmt.len == 1 and fmt[0] == 's') return bw.writeAll(@errorName(value));
853 return bw.writeAll(@errorName(value));863 if (!is_any and fmt.len != 0) invalidFmtError(fmt, value);
854 } else if (actual_fmt.len != 0) {864 try printErrorSet(bw, value);
855 invalidFmtError(fmt, value);
856 } else {
857 try bw.writeAll("error.");
858 try bw.writeAll(@errorName(value));
859 }
860 },865 },
861 .@"enum" => |enum_info| {866 .@"enum" => {
862 try bw.writeAll(@typeName(T));867 if (fmt.len == 1 and fmt[0] == 's') {
863 if (enum_info.is_exhaustive) {
864 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
865 try bw.writeAll(".");
866 try bw.writeAll(@tagName(value));868 try bw.writeAll(@tagName(value));
867 return;869 return;
868 }870 }
869871 if (!is_any) {
870 // Use @tagName only if value is one of known fields872 if (fmt.len != 0) return printValue(bw, fmt, options, @intFromEnum(value), max_depth);
873 return printValue(bw, ANY, options, value, max_depth);
874 }
875 const enum_info = @typeInfo(T).@"enum";
876 if (enum_info.is_exhaustive) {
877 var vecs: [3][]const u8 = .{ @typeName(T), ".", @tagName(value) };
878 try bw.writeVecAll(&vecs);
879 return;
880 }
881 try bw.writeAll(@typeName(T));
871 @setEvalBranchQuota(3 * enum_info.fields.len);882 @setEvalBranchQuota(3 * enum_info.fields.len);
872 inline for (enum_info.fields) |enumField| {883 inline for (enum_info.fields) |field| {
873 if (@intFromEnum(value) == enumField.value) {884 if (@intFromEnum(value) == field.value) {
874 try bw.writeAll(".");885 try bw.writeAll(".");
875 try bw.writeAll(@tagName(value));886 try bw.writeAll(@tagName(value));
876 return;887 return;
877 }888 }
878 }889 }
879
880 try bw.writeByte('(');890 try bw.writeByte('(');
881 try bw.printValue(actual_fmt, options, @intFromEnum(value), max_depth);891 try bw.printValue(ANY, options, @intFromEnum(value), max_depth);
882 try bw.writeByte(')');892 try bw.writeByte(')');
883 },893 },
884 .@"union" => |info| {894 .@"union" => |info| {
885 if (actual_fmt.len != 0) invalidFmtError(fmt, value);895 if (!is_any) {
896 if (fmt.len != 0) invalidFmtError(fmt, value);
897 return printValue(bw, ANY, options, value, max_depth);
898 }
886 try bw.writeAll(@typeName(T));899 try bw.writeAll(@typeName(T));
887 if (max_depth == 0) {900 if (max_depth == 0) {
888 try bw.writeAll("{ ... }");901 try bw.writeAll("{ ... }");
...@@ -904,7 +917,10 @@ pub fn printValue(...@@ -904,7 +917,10 @@ pub fn printValue(
904 }917 }
905 },918 },
906 .@"struct" => |info| {919 .@"struct" => |info| {
907 if (actual_fmt.len != 0) invalidFmtError(fmt, value);920 if (!is_any) {
921 if (fmt.len != 0) invalidFmtError(fmt, value);
922 return printValue(bw, ANY, options, value, max_depth);
923 }
908 if (info.is_tuple) {924 if (info.is_tuple) {
909 // Skip the type and field names when formatting tuples.925 // Skip the type and field names when formatting tuples.
910 if (max_depth == 0) {926 if (max_depth == 0) {
...@@ -944,7 +960,7 @@ pub fn printValue(...@@ -944,7 +960,7 @@ pub fn printValue(
944 .pointer => |ptr_info| switch (ptr_info.size) {960 .pointer => |ptr_info| switch (ptr_info.size) {
945 .one => switch (@typeInfo(ptr_info.child)) {961 .one => switch (@typeInfo(ptr_info.child)) {
946 .array, .@"enum", .@"union", .@"struct" => {962 .array, .@"enum", .@"union", .@"struct" => {
947 return bw.printValue(actual_fmt, options, value.*, max_depth);963 return bw.printValue(fmt, options, value.*, max_depth);
948 },964 },
949 else => {965 else => {
950 var buffers: [2][]const u8 = .{ @typeName(ptr_info.child), "@" };966 var buffers: [2][]const u8 = .{ @typeName(ptr_info.child), "@" };
...@@ -954,37 +970,36 @@ pub fn printValue(...@@ -954,37 +970,36 @@ pub fn printValue(
954 },970 },
955 },971 },
956 .many, .c => {972 .many, .c => {
957 if (actual_fmt.len == 0)973 if (ptr_info.sentinel() != null)
958 @compileError("cannot format pointer without a specifier (i.e. {s} or {*})");974 return bw.printValue(fmt, options, std.mem.span(value), max_depth);
959 if (ptr_info.sentinel() != null) {975 if (fmt.len == 1 and fmt[0] == 's' and ptr_info.child == u8)
960 return bw.printValue(actual_fmt, options, std.mem.span(value), max_depth);
961 }
962 if (actual_fmt[0] == 's' and ptr_info.child == u8) {
963 return bw.alignBufferOptions(std.mem.span(value), options);976 return bw.alignBufferOptions(std.mem.span(value), options);
964 }977 if (!is_any and fmt.len == 0)
965 invalidFmtError(fmt, value);978 @compileError("cannot format pointer without a specifier (i.e. {s} or {*})");
979 if (!is_any and fmt.len != 0)
980 invalidFmtError(fmt, value);
981 try bw.printAddress(value);
966 },982 },
967 .slice => {983 .slice => {
968 if (actual_fmt.len == 0)984 if (!is_any and fmt.len == 0)
969 @compileError("cannot format slice without a specifier (i.e. {s}, {x}, {b64}, or {any})");985 @compileError("cannot format slice without a specifier (i.e. {s}, {x}, {b64}, or {any})");
970 if (max_depth == 0) {986 if (max_depth == 0)
971 return bw.writeAll("{ ... }");987 return bw.writeAll("{ ... }");
972 }988 if (ptr_info.child == u8) switch (fmt.len) {
973 if (ptr_info.child == u8) switch (actual_fmt.len) {989 1 => switch (fmt[0]) {
974 1 => switch (actual_fmt[0]) {
975 's' => return bw.alignBufferOptions(value, options),990 's' => return bw.alignBufferOptions(value, options),
976 'x' => return bw.printHex(value, .lower),991 'x' => return bw.printHex(value, .lower),
977 'X' => return bw.printHex(value, .upper),992 'X' => return bw.printHex(value, .upper),
978 else => {},993 else => {},
979 },994 },
980 3 => if (actual_fmt[0] == 'b' and actual_fmt[1] == '6' and actual_fmt[2] == '4') {995 3 => if (fmt[0] == 'b' and fmt[1] == '6' and fmt[2] == '4') {
981 return bw.printBase64(value);996 return bw.printBase64(value);
982 },997 },
983 else => {},998 else => {},
984 };999 };
985 try bw.writeAll("{ ");1000 try bw.writeAll("{ ");
986 for (value, 0..) |elem, i| {1001 for (value, 0..) |elem, i| {
987 try bw.printValue(actual_fmt, options, elem, max_depth - 1);1002 try bw.printValue(fmt, options, elem, max_depth - 1);
988 if (i != value.len - 1) {1003 if (i != value.len - 1) {
989 try bw.writeAll(", ");1004 try bw.writeAll(", ");
990 }1005 }
...@@ -993,23 +1008,23 @@ pub fn printValue(...@@ -993,23 +1008,23 @@ pub fn printValue(
993 },1008 },
994 },1009 },
995 .array => |info| {1010 .array => |info| {
996 if (actual_fmt.len == 0)1011 if (fmt.len == 0)
997 @compileError("cannot format array without a specifier (i.e. {s} or {any})");1012 @compileError("cannot format array without a specifier (i.e. {s} or {any})");
998 if (max_depth == 0) {1013 if (max_depth == 0) {
999 return bw.writeAll("{ ... }");1014 return bw.writeAll("{ ... }");
1000 }1015 }
1001 if (info.child == u8) {1016 if (info.child == u8) {
1002 if (actual_fmt[0] == 's') {1017 if (fmt[0] == 's') {
1003 return bw.alignBufferOptions(&value, options);1018 return bw.alignBufferOptions(&value, options);
1004 } else if (actual_fmt[0] == 'x') {1019 } else if (fmt[0] == 'x') {
1005 return bw.printHex(&value, .lower);1020 return bw.printHex(&value, .lower);
1006 } else if (actual_fmt[0] == 'X') {1021 } else if (fmt[0] == 'X') {
1007 return bw.printHex(&value, .upper);1022 return bw.printHex(&value, .upper);
1008 }1023 }
1009 }1024 }
1010 try bw.writeAll("{ ");1025 try bw.writeAll("{ ");
1011 for (value, 0..) |elem, i| {1026 for (value, 0..) |elem, i| {
1012 try bw.printValue(actual_fmt, options, elem, max_depth - 1);1027 try bw.printValue(fmt, options, elem, max_depth - 1);
1013 if (i < value.len - 1) {1028 if (i < value.len - 1) {
1014 try bw.writeAll(", ");1029 try bw.writeAll(", ");
1015 }1030 }
...@@ -1023,7 +1038,7 @@ pub fn printValue(...@@ -1023,7 +1038,7 @@ pub fn printValue(
1023 try bw.writeAll("{ ");1038 try bw.writeAll("{ ");
1024 var i: usize = 0;1039 var i: usize = 0;
1025 while (i < info.len) : (i += 1) {1040 while (i < info.len) : (i += 1) {
1026 try bw.printValue(actual_fmt, options, value[i], max_depth - 1);1041 try bw.printValue(fmt, options, value[i], max_depth - 1);
1027 if (i < info.len - 1) {1042 if (i < info.len - 1) {
1028 try bw.writeAll(", ");1043 try bw.writeAll(", ");
1029 }1044 }
...@@ -1032,22 +1047,27 @@ pub fn printValue(...@@ -1032,22 +1047,27 @@ pub fn printValue(
1032 },1047 },
1033 .@"fn" => @compileError("unable to format function body type, use '*const " ++ @typeName(T) ++ "' for a function pointer type"),1048 .@"fn" => @compileError("unable to format function body type, use '*const " ++ @typeName(T) ++ "' for a function pointer type"),
1034 .type => {1049 .type => {
1035 if (actual_fmt.len != 0) invalidFmtError(fmt, value);1050 if (!is_any and fmt.len != 0) invalidFmtError(fmt, value);
1036 return bw.alignBufferOptions(@typeName(value), options);1051 return bw.alignBufferOptions(@typeName(value), options);
1037 },1052 },
1038 .enum_literal => {1053 .enum_literal => {
1039 if (actual_fmt.len != 0) invalidFmtError(fmt, value);1054 if (!is_any and fmt.len != 0) invalidFmtError(fmt, value);
1040 const buffer = [_]u8{'.'} ++ @tagName(value);1055 const buffer = [_]u8{'.'} ++ @tagName(value);
1041 return bw.alignBufferOptions(buffer, options);1056 return bw.alignBufferOptions(buffer, options);
1042 },1057 },
1043 .null => {1058 .null => {
1044 if (actual_fmt.len != 0) invalidFmtError(fmt, value);1059 if (!is_any and fmt.len != 0) invalidFmtError(fmt, value);
1045 return bw.alignBufferOptions("null", options);1060 return bw.alignBufferOptions("null", options);
1046 },1061 },
1047 else => @compileError("unable to format type '" ++ @typeName(T) ++ "'"),1062 else => @compileError("unable to format type '" ++ @typeName(T) ++ "'"),
1048 }1063 }
1049}1064}
10501065
1066fn printErrorSet(bw: *BufferedWriter, error_set: anyerror) Writer.Error!void {
1067 var vecs: [2][]const u8 = .{ "error.", @errorName(error_set) };
1068 try bw.writeVecAll(&vecs);
1069}
1070
1051pub fn printInt(1071pub fn printInt(
1052 bw: *BufferedWriter,1072 bw: *BufferedWriter,
1053 comptime fmt: []const u8,1073 comptime fmt: []const u8,
...@@ -1376,24 +1396,6 @@ pub fn printByteSize(...@@ -1376,24 +1396,6 @@ pub fn printByteSize(
1376// This ANY const is a workaround for: https://github.com/ziglang/zig/issues/79481396// This ANY const is a workaround for: https://github.com/ziglang/zig/issues/7948
1377const ANY = "any";1397const ANY = "any";
13781398
1379fn defaultFormatString(comptime T: type) [:0]const u8 {
1380 switch (@typeInfo(T)) {
1381 .array, .vector => return ANY,
1382 .pointer => |ptr_info| switch (ptr_info.size) {
1383 .one => switch (@typeInfo(ptr_info.child)) {
1384 .array => return ANY,
1385 else => {},
1386 },
1387 .many, .c => return "*",
1388 .slice => return ANY,
1389 },
1390 .optional => |info| return "?" ++ defaultFormatString(info.child),
1391 .error_union => |info| return "!" ++ defaultFormatString(info.payload),
1392 else => {},
1393 }
1394 return "";
1395}
1396
1397fn stripOptionalOrErrorUnionSpec(comptime fmt: []const u8) []const u8 {1399fn stripOptionalOrErrorUnionSpec(comptime fmt: []const u8) []const u8 {
1398 return if (std.mem.eql(u8, fmt[1..], ANY))1400 return if (std.mem.eql(u8, fmt[1..], ANY))
1399 ANY1401 ANY
lib/std/testing.zig+2-5
...@@ -52,14 +52,11 @@ fn print(comptime fmt: []const u8, args: anytype) void {...@@ -52,14 +52,11 @@ fn print(comptime fmt: []const u8, args: anytype) void {
52/// and then returns a test failure error when actual_error_union is not expected_error.52/// and then returns a test failure error when actual_error_union is not expected_error.
53pub fn expectError(expected_error: anyerror, actual_error_union: anytype) !void {53pub fn expectError(expected_error: anyerror, actual_error_union: anytype) !void {
54 if (actual_error_union) |actual_payload| {54 if (actual_error_union) |actual_payload| {
55 print("expected error.{s}, found {any}\n", .{ @errorName(expected_error), actual_payload });55 print("expected {s}, found {any}\n", .{ expected_error, actual_payload });
56 return error.TestExpectedError;56 return error.TestExpectedError;
57 } else |actual_error| {57 } else |actual_error| {
58 if (expected_error != actual_error) {58 if (expected_error != actual_error) {
59 print("expected error.{s}, found error.{s}\n", .{59 print("expected {s}, found {s}\n", .{ expected_error, actual_error });
60 @errorName(expected_error),
61 @errorName(actual_error),
62 });
63 return error.TestUnexpectedError;60 return error.TestUnexpectedError;
64 }61 }
65 }62 }