From 0fb7a0a94bf6f9e329008d8b5b819a8d1d7124b0 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 19 Jul 2025 17:33:44 -0700 Subject: [PATCH] std.zon: better namespace for Serializer --- lib/std/zon.zig | 1 + lib/std/zon/Serializer.zig | 929 ++++++++++++++++++++++++++++++++++++ lib/std/zon/stringify.zig | 936 +------------------------------------ src/main.zig | 9 +- src/print_env.zig | 49 +- src/print_targets.zig | 12 +- src/translate_c.zig | 2 +- 7 files changed, 957 insertions(+), 981 deletions(-) create mode 100644 lib/std/zon/Serializer.zig diff --git a/lib/std/zon.zig b/lib/std/zon.zig index 252331057a..9ac02cf741 100644 --- a/lib/std/zon.zig +++ b/lib/std/zon.zig @@ -38,6 +38,7 @@ pub const parse = @import("zon/parse.zig"); pub const stringify = @import("zon/stringify.zig"); +pub const Serializer = @import("zon/Serializer.zig"); test { _ = parse; diff --git a/lib/std/zon/Serializer.zig b/lib/std/zon/Serializer.zig new file mode 100644 index 0000000000..b65b13bf97 --- /dev/null +++ b/lib/std/zon/Serializer.zig @@ -0,0 +1,929 @@ +//! Lower level control over serialization, you can create a new instance with `serializer`. +//! +//! Useful when you want control over which fields are serialized, how they're represented, +//! or want to write a ZON object that does not exist in memory. +//! +//! You can serialize values with `value`. To serialize recursive types, the following are provided: +//! * `valueMaxDepth` +//! * `valueArbitraryDepth` +//! +//! You can also serialize values using specific notations: +//! * `int` +//! * `float` +//! * `codePoint` +//! * `tuple` +//! * `tupleMaxDepth` +//! * `tupleArbitraryDepth` +//! * `string` +//! * `multilineString` +//! +//! For manual serialization of containers, see: +//! * `beginStruct` +//! * `beginTuple` + +options: Options = .{}, +indent_level: u8 = 0, +writer: *Writer, + +const Serializer = @This(); +const std = @import("std"); +const assert = std.debug.assert; +const Writer = std.Io.Writer; + +pub const Error = Writer.Error; +pub const DepthError = Error || error{ExceededMaxDepth}; + +pub const Options = struct { + /// If false, only syntactically necessary whitespace is emitted. + whitespace: bool = true, +}; + +/// Options for manual serialization of container types. +pub const ContainerOptions = struct { + /// The whitespace style that should be used for this container. Ignored if whitespace is off. + whitespace_style: union(enum) { + /// If true, wrap every field. If false do not. + wrap: bool, + /// Automatically decide whether to wrap or not based on the number of fields. Following + /// the standard rule of thumb, containers with more than two fields are wrapped. + fields: usize, + } = .{ .wrap = true }, + + fn shouldWrap(self: ContainerOptions) bool { + return switch (self.whitespace_style) { + .wrap => |wrap| wrap, + .fields => |fields| fields > 2, + }; + } +}; + +/// Options for serialization of an individual value. +/// +/// See `SerializeOptions` for more information on these options. +pub const ValueOptions = struct { + emit_codepoint_literals: EmitCodepointLiterals = .never, + emit_strings_as_containers: bool = false, + emit_default_optional_fields: bool = true, +}; + +/// Determines when to emit Unicode code point literals as opposed to integer literals. +pub const EmitCodepointLiterals = enum { + /// Never emit Unicode code point literals. + never, + /// Emit Unicode code point literals for any `u8` in the printable ASCII range. + printable_ascii, + /// Emit Unicode code point literals for any unsigned integer with 21 bits or fewer + /// whose value is a valid non-surrogate code point. + always, + + /// If the value should be emitted as a Unicode codepoint, return it as a u21. + fn emitAsCodepoint(self: @This(), val: anytype) ?u21 { + // Rule out incompatible integer types + switch (@typeInfo(@TypeOf(val))) { + .int => |int_info| if (int_info.signedness == .signed or int_info.bits > 21) { + return null; + }, + .comptime_int => {}, + else => comptime unreachable, + } + + // Return null if the value shouldn't be printed as a Unicode codepoint, or the value casted + // to a u21 if it should. + switch (self) { + .always => { + const c = std.math.cast(u21, val) orelse return null; + if (!std.unicode.utf8ValidCodepoint(c)) return null; + return c; + }, + .printable_ascii => { + const c = std.math.cast(u8, val) orelse return null; + if (!std.ascii.isPrint(c)) return null; + return c; + }, + .never => { + return null; + }, + } + } +}; + +/// Serialize a value, similar to `serialize`. +pub fn value(self: *Serializer, val: anytype, options: ValueOptions) Error!void { + comptime assert(!typeIsRecursive(@TypeOf(val))); + return self.valueArbitraryDepth(val, options); +} + +/// Serialize a value, similar to `serializeMaxDepth`. +/// Can return `error.ExceededMaxDepth`. +pub fn valueMaxDepth(self: *Serializer, val: anytype, options: ValueOptions, depth: usize) DepthError!void { + try checkValueDepth(val, depth); + return self.valueArbitraryDepth(val, options); +} + +/// Serialize a value, similar to `serializeArbitraryDepth`. +pub fn valueArbitraryDepth(self: *Serializer, val: anytype, options: ValueOptions) Error!void { + comptime assert(canSerializeType(@TypeOf(val))); + switch (@typeInfo(@TypeOf(val))) { + .int, .comptime_int => if (options.emit_codepoint_literals.emitAsCodepoint(val)) |c| { + self.codePoint(c) catch |err| switch (err) { + error.InvalidCodepoint => unreachable, // Already validated + else => |e| return e, + }; + } else { + try self.int(val); + }, + .float, .comptime_float => try self.float(val), + .bool, .null => try self.writer.print("{}", .{val}), + .enum_literal => try self.ident(@tagName(val)), + .@"enum" => try self.ident(@tagName(val)), + .pointer => |pointer| { + // Try to serialize as a string + const item: ?type = switch (@typeInfo(pointer.child)) { + .array => |array| array.child, + else => if (pointer.size == .slice) pointer.child else null, + }; + if (item == u8 and + (pointer.sentinel() == null or pointer.sentinel() == 0) and + !options.emit_strings_as_containers) + { + return try self.string(val); + } + + // Serialize as either a tuple or as the child type + switch (pointer.size) { + .slice => try self.tupleImpl(val, options), + .one => try self.valueArbitraryDepth(val.*, options), + else => comptime unreachable, + } + }, + .array => { + var container = try self.beginTuple( + .{ .whitespace_style = .{ .fields = val.len } }, + ); + for (val) |item_val| { + try container.fieldArbitraryDepth(item_val, options); + } + try container.end(); + }, + .@"struct" => |@"struct"| if (@"struct".is_tuple) { + var container = try self.beginTuple( + .{ .whitespace_style = .{ .fields = @"struct".fields.len } }, + ); + inline for (val) |field_value| { + try container.fieldArbitraryDepth(field_value, options); + } + try container.end(); + } else { + // Decide which fields to emit + const fields, const skipped: [@"struct".fields.len]bool = if (options.emit_default_optional_fields) b: { + break :b .{ @"struct".fields.len, @splat(false) }; + } else b: { + var fields = @"struct".fields.len; + var skipped: [@"struct".fields.len]bool = @splat(false); + inline for (@"struct".fields, &skipped) |field_info, *skip| { + if (field_info.default_value_ptr) |ptr| { + const default: *const field_info.type = @ptrCast(@alignCast(ptr)); + const field_value = @field(val, field_info.name); + if (std.meta.eql(field_value, default.*)) { + skip.* = true; + fields -= 1; + } + } + } + break :b .{ fields, skipped }; + }; + + // Emit those fields + var container = try self.beginStruct( + .{ .whitespace_style = .{ .fields = fields } }, + ); + inline for (@"struct".fields, skipped) |field_info, skip| { + if (!skip) { + try container.fieldArbitraryDepth( + field_info.name, + @field(val, field_info.name), + options, + ); + } + } + try container.end(); + }, + .@"union" => |@"union"| { + comptime assert(@"union".tag_type != null); + switch (val) { + inline else => |pl, tag| if (@TypeOf(pl) == void) + try self.writer.print(".{s}", .{@tagName(tag)}) + else { + var container = try self.beginStruct(.{ .whitespace_style = .{ .fields = 1 } }); + + try container.fieldArbitraryDepth( + @tagName(tag), + pl, + options, + ); + + try container.end(); + }, + } + }, + .optional => if (val) |inner| { + try self.valueArbitraryDepth(inner, options); + } else { + try self.writer.writeAll("null"); + }, + .vector => |vector| { + var container = try self.beginTuple( + .{ .whitespace_style = .{ .fields = vector.len } }, + ); + for (0..vector.len) |i| { + try container.fieldArbitraryDepth(val[i], options); + } + try container.end(); + }, + + else => comptime unreachable, + } +} + +/// Serialize an integer. +pub fn int(self: *Serializer, val: anytype) Error!void { + try self.writer.printInt(val, 10, .lower, .{}); +} + +/// Serialize a float. +pub fn float(self: *Serializer, val: anytype) Error!void { + switch (@typeInfo(@TypeOf(val))) { + .float => if (std.math.isNan(val)) { + return self.writer.writeAll("nan"); + } else if (std.math.isPositiveInf(val)) { + return self.writer.writeAll("inf"); + } else if (std.math.isNegativeInf(val)) { + return self.writer.writeAll("-inf"); + } else if (std.math.isNegativeZero(val)) { + return self.writer.writeAll("-0.0"); + } else { + try self.writer.print("{d}", .{val}); + }, + .comptime_float => if (val == 0) { + return self.writer.writeAll("0"); + } else { + try self.writer.print("{d}", .{val}); + }, + else => comptime unreachable, + } +} + +/// Serialize `name` as an identifier prefixed with `.`. +/// +/// Escapes the identifier if necessary. +pub fn ident(self: *Serializer, name: []const u8) Error!void { + try self.writer.print(".{f}", .{std.zig.fmtIdPU(name)}); +} + +pub const CodePointError = Error || error{InvalidCodepoint}; + +/// Serialize `val` as a Unicode codepoint. +/// +/// Returns `error.InvalidCodepoint` if `val` is not a valid Unicode codepoint. +pub fn codePoint(self: *Serializer, val: u21) CodePointError!void { + try self.writer.print("'{f}'", .{std.zig.fmtChar(val)}); +} + +/// Like `value`, but always serializes `val` as a tuple. +/// +/// Will fail at comptime if `val` is not a tuple, array, pointer to an array, or slice. +pub fn tuple(self: *Serializer, val: anytype, options: ValueOptions) Error!void { + comptime assert(!typeIsRecursive(@TypeOf(val))); + try self.tupleArbitraryDepth(val, options); +} + +/// Like `tuple`, but recursive types are allowed. +/// +/// Returns `error.ExceededMaxDepth` if `depth` is exceeded. +pub fn tupleMaxDepth( + self: *Serializer, + val: anytype, + options: ValueOptions, + depth: usize, +) DepthError!void { + try checkValueDepth(val, depth); + try self.tupleArbitraryDepth(val, options); +} + +/// Like `tuple`, but recursive types are allowed. +/// +/// It is the caller's responsibility to ensure that `val` does not contain cycles. +pub fn tupleArbitraryDepth( + self: *Serializer, + val: anytype, + options: ValueOptions, +) Error!void { + try self.tupleImpl(val, options); +} + +fn tupleImpl(self: *Serializer, val: anytype, options: ValueOptions) Error!void { + comptime assert(canSerializeType(@TypeOf(val))); + switch (@typeInfo(@TypeOf(val))) { + .@"struct" => { + var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } }); + inline for (val) |item_val| { + try container.fieldArbitraryDepth(item_val, options); + } + try container.end(); + }, + .pointer, .array => { + var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } }); + for (val) |item_val| { + try container.fieldArbitraryDepth(item_val, options); + } + try container.end(); + }, + else => comptime unreachable, + } +} + +/// Like `value`, but always serializes `val` as a string. +pub fn string(self: *Serializer, val: []const u8) Error!void { + try self.writer.print("\"{f}\"", .{std.zig.fmtString(val)}); +} + +/// Options for formatting multiline strings. +pub const MultilineStringOptions = struct { + /// If top level is true, whitespace before and after the multiline string is elided. + /// If it is true, a newline is printed, then the value, followed by a newline, and if + /// whitespace is true any necessary indentation follows. + top_level: bool = false, +}; + +pub const MultilineStringError = Error || error{InnerCarriageReturn}; + +/// Like `value`, but always serializes to a multiline string literal. +/// +/// Returns `error.InnerCarriageReturn` if `val` contains a CR not followed by a newline, +/// since multiline strings cannot represent CR without a following newline. +pub fn multilineString( + self: *Serializer, + val: []const u8, + options: MultilineStringOptions, +) MultilineStringError!void { + // Make sure the string does not contain any carriage returns not followed by a newline + var i: usize = 0; + while (i < val.len) : (i += 1) { + if (val[i] == '\r') { + if (i + 1 < val.len) { + if (val[i + 1] == '\n') { + i += 1; + continue; + } + } + return error.InnerCarriageReturn; + } + } + + if (!options.top_level) { + try self.newline(); + try self.indent(); + } + + try self.writer.writeAll("\\\\"); + for (val) |c| { + if (c != '\r') { + try self.writer.writeByte(c); // We write newlines here even if whitespace off + if (c == '\n') { + try self.indent(); + try self.writer.writeAll("\\\\"); + } + } + } + + if (!options.top_level) { + try self.writer.writeByte('\n'); // Even if whitespace off + try self.indent(); + } +} + +/// Create a `Struct` for writing ZON structs field by field. +pub fn beginStruct(self: *Serializer, options: ContainerOptions) Error!Struct { + return Struct.begin(self, options); +} + +/// Creates a `Tuple` for writing ZON tuples field by field. +pub fn beginTuple(self: *Serializer, options: ContainerOptions) Error!Tuple { + return Tuple.begin(self, options); +} + +fn indent(self: *Serializer) Error!void { + if (self.options.whitespace) { + try self.writer.splatByteAll(' ', 4 * self.indent_level); + } +} + +fn newline(self: *Serializer) Error!void { + if (self.options.whitespace) { + try self.writer.writeByte('\n'); + } +} + +fn newlineOrSpace(self: *Serializer, len: usize) Error!void { + if (self.containerShouldWrap(len)) { + try self.newline(); + } else { + try self.space(); + } +} + +fn space(self: *Serializer) Error!void { + if (self.options.whitespace) { + try self.writer.writeByte(' '); + } +} + +/// Writes ZON tuples field by field. +pub const Tuple = struct { + container: Container, + + fn begin(parent: *Serializer, options: ContainerOptions) Error!Tuple { + return .{ + .container = try Container.begin(parent, .anon, options), + }; + } + + /// Finishes serializing the tuple. + /// + /// Prints a trailing comma as configured when appropriate, and the closing bracket. + pub fn end(self: *Tuple) Error!void { + try self.container.end(); + self.* = undefined; + } + + /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `value`. + pub fn field( + self: *Tuple, + val: anytype, + options: ValueOptions, + ) Error!void { + try self.container.field(null, val, options); + } + + /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `valueMaxDepth`. + /// Returns `error.ExceededMaxDepth` if `depth` is exceeded. + pub fn fieldMaxDepth( + self: *Tuple, + val: anytype, + options: ValueOptions, + depth: usize, + ) DepthError!void { + try self.container.fieldMaxDepth(null, val, options, depth); + } + + /// Serialize a field. Equivalent to calling `fieldPrefix` followed by + /// `valueArbitraryDepth`. + pub fn fieldArbitraryDepth( + self: *Tuple, + val: anytype, + options: ValueOptions, + ) Error!void { + try self.container.fieldArbitraryDepth(null, val, options); + } + + /// Starts a field with a struct as a value. Returns the struct. + pub fn beginStructField( + self: *Tuple, + options: ContainerOptions, + ) Error!Struct { + try self.fieldPrefix(); + return self.container.serializer.beginStruct(options); + } + + /// Starts a field with a tuple as a value. Returns the tuple. + pub fn beginTupleField( + self: *Tuple, + options: ContainerOptions, + ) Error!Tuple { + try self.fieldPrefix(); + return self.container.serializer.beginTuple(options); + } + + /// Print a field prefix. This prints any necessary commas, and whitespace as + /// configured. Useful if you want to serialize the field value yourself. + pub fn fieldPrefix(self: *Tuple) Error!void { + try self.container.fieldPrefix(null); + } +}; + +/// Writes ZON structs field by field. +pub const Struct = struct { + container: Container, + + fn begin(parent: *Serializer, options: ContainerOptions) Error!Struct { + return .{ + .container = try Container.begin(parent, .named, options), + }; + } + + /// Finishes serializing the struct. + /// + /// Prints a trailing comma as configured when appropriate, and the closing bracket. + pub fn end(self: *Struct) Error!void { + try self.container.end(); + self.* = undefined; + } + + /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `value`. + pub fn field( + self: *Struct, + name: []const u8, + val: anytype, + options: ValueOptions, + ) Error!void { + try self.container.field(name, val, options); + } + + /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `valueMaxDepth`. + /// Returns `error.ExceededMaxDepth` if `depth` is exceeded. + pub fn fieldMaxDepth( + self: *Struct, + name: []const u8, + val: anytype, + options: ValueOptions, + depth: usize, + ) DepthError!void { + try self.container.fieldMaxDepth(name, val, options, depth); + } + + /// Serialize a field. Equivalent to calling `fieldPrefix` followed by + /// `valueArbitraryDepth`. + pub fn fieldArbitraryDepth( + self: *Struct, + name: []const u8, + val: anytype, + options: ValueOptions, + ) Error!void { + try self.container.fieldArbitraryDepth(name, val, options); + } + + /// Starts a field with a struct as a value. Returns the struct. + pub fn beginStructField( + self: *Struct, + name: []const u8, + options: ContainerOptions, + ) Error!Struct { + try self.fieldPrefix(name); + return self.container.serializer.beginStruct(options); + } + + /// Starts a field with a tuple as a value. Returns the tuple. + pub fn beginTupleField( + self: *Struct, + name: []const u8, + options: ContainerOptions, + ) Error!Tuple { + try self.fieldPrefix(name); + return self.container.serializer.beginTuple(options); + } + + /// Print a field prefix. This prints any necessary commas, the field name (escaped if + /// necessary) and whitespace as configured. Useful if you want to serialize the field + /// value yourself. + pub fn fieldPrefix(self: *Struct, name: []const u8) Error!void { + try self.container.fieldPrefix(name); + } +}; + +const Container = struct { + const FieldStyle = enum { named, anon }; + + serializer: *Serializer, + field_style: FieldStyle, + options: ContainerOptions, + empty: bool, + + fn begin( + sz: *Serializer, + field_style: FieldStyle, + options: ContainerOptions, + ) Error!Container { + if (options.shouldWrap()) sz.indent_level +|= 1; + try sz.writer.writeAll(".{"); + return .{ + .serializer = sz, + .field_style = field_style, + .options = options, + .empty = true, + }; + } + + fn end(self: *Container) Error!void { + if (self.options.shouldWrap()) self.serializer.indent_level -|= 1; + if (!self.empty) { + if (self.options.shouldWrap()) { + if (self.serializer.options.whitespace) { + try self.serializer.writer.writeByte(','); + } + try self.serializer.newline(); + try self.serializer.indent(); + } else if (!self.shouldElideSpaces()) { + try self.serializer.space(); + } + } + try self.serializer.writer.writeByte('}'); + self.* = undefined; + } + + fn fieldPrefix(self: *Container, name: ?[]const u8) Error!void { + if (!self.empty) { + try self.serializer.writer.writeByte(','); + } + self.empty = false; + if (self.options.shouldWrap()) { + try self.serializer.newline(); + } else if (!self.shouldElideSpaces()) { + try self.serializer.space(); + } + if (self.options.shouldWrap()) try self.serializer.indent(); + if (name) |n| { + try self.serializer.ident(n); + try self.serializer.space(); + try self.serializer.writer.writeByte('='); + try self.serializer.space(); + } + } + + fn field( + self: *Container, + name: ?[]const u8, + val: anytype, + options: ValueOptions, + ) Error!void { + comptime assert(!typeIsRecursive(@TypeOf(val))); + try self.fieldArbitraryDepth(name, val, options); + } + + /// Returns `error.ExceededMaxDepth` if `depth` is exceeded. + fn fieldMaxDepth( + self: *Container, + name: ?[]const u8, + val: anytype, + options: ValueOptions, + depth: usize, + ) DepthError!void { + try checkValueDepth(val, depth); + try self.fieldArbitraryDepth(name, val, options); + } + + fn fieldArbitraryDepth( + self: *Container, + name: ?[]const u8, + val: anytype, + options: ValueOptions, + ) Error!void { + try self.fieldPrefix(name); + try self.serializer.valueArbitraryDepth(val, options); + } + + fn shouldElideSpaces(self: *const Container) bool { + return switch (self.options.whitespace_style) { + .fields => |fields| self.field_style != .named and fields == 1, + else => false, + }; + } +}; + +test Serializer { + var discarding: Writer.Discarding = .init(&.{}); + var s: Serializer = .{ .writer = &discarding.writer }; + var vec2 = try s.beginStruct(.{}); + try vec2.field("x", 1.5, .{}); + try vec2.fieldPrefix("prefix"); + try s.value(2.5, .{}); + try vec2.end(); +} + +inline fn typeIsRecursive(comptime T: type) bool { + return comptime typeIsRecursiveInner(T, &.{}); +} + +fn typeIsRecursiveInner(comptime T: type, comptime prev_visited: []const type) bool { + for (prev_visited) |V| { + if (V == T) return true; + } + const visited = prev_visited ++ .{T}; + + return switch (@typeInfo(T)) { + .pointer => |pointer| typeIsRecursiveInner(pointer.child, visited), + .optional => |optional| typeIsRecursiveInner(optional.child, visited), + .array => |array| typeIsRecursiveInner(array.child, visited), + .vector => |vector| typeIsRecursiveInner(vector.child, visited), + .@"struct" => |@"struct"| for (@"struct".fields) |field| { + if (typeIsRecursiveInner(field.type, visited)) break true; + } else false, + .@"union" => |@"union"| inline for (@"union".fields) |field| { + if (typeIsRecursiveInner(field.type, visited)) break true; + } else false, + else => false, + }; +} + +test typeIsRecursive { + try std.testing.expect(!typeIsRecursive(bool)); + try std.testing.expect(!typeIsRecursive(struct { x: i32, y: i32 })); + try std.testing.expect(!typeIsRecursive(struct { i32, i32 })); + try std.testing.expect(typeIsRecursive(struct { x: i32, y: i32, z: *@This() })); + try std.testing.expect(typeIsRecursive(struct { + a: struct { + const A = @This(); + b: struct { + c: *struct { + a: ?A, + }, + }, + }, + })); + try std.testing.expect(typeIsRecursive(struct { + a: [3]*@This(), + })); + try std.testing.expect(typeIsRecursive(struct { + a: union { a: i32, b: *@This() }, + })); +} + +fn checkValueDepth(val: anytype, depth: usize) error{ExceededMaxDepth}!void { + if (depth == 0) return error.ExceededMaxDepth; + const child_depth = depth - 1; + + switch (@typeInfo(@TypeOf(val))) { + .pointer => |pointer| switch (pointer.size) { + .one => try checkValueDepth(val.*, child_depth), + .slice => for (val) |item| { + try checkValueDepth(item, child_depth); + }, + .c, .many => {}, + }, + .array => for (val) |item| { + try checkValueDepth(item, child_depth); + }, + .@"struct" => |@"struct"| inline for (@"struct".fields) |field_info| { + try checkValueDepth(@field(val, field_info.name), child_depth); + }, + .@"union" => |@"union"| if (@"union".tag_type == null) { + return; + } else switch (val) { + inline else => |payload| { + return checkValueDepth(payload, child_depth); + }, + }, + .optional => if (val) |inner| try checkValueDepth(inner, child_depth), + else => {}, + } +} + +fn expectValueDepthEquals(expected: usize, v: anytype) !void { + try checkValueDepth(v, expected); + try std.testing.expectError(error.ExceededMaxDepth, checkValueDepth(v, expected - 1)); +} + +test checkValueDepth { + try expectValueDepthEquals(1, 10); + try expectValueDepthEquals(2, .{ .x = 1, .y = 2 }); + try expectValueDepthEquals(2, .{ 1, 2 }); + try expectValueDepthEquals(3, .{ 1, .{ 2, 3 } }); + try expectValueDepthEquals(3, .{ .{ 1, 2 }, 3 }); + try expectValueDepthEquals(3, .{ .x = 0, .y = 1, .z = .{ .x = 3 } }); + try expectValueDepthEquals(3, .{ .x = 0, .y = .{ .x = 1 }, .z = 2 }); + try expectValueDepthEquals(3, .{ .x = .{ .x = 0 }, .y = 1, .z = 2 }); + try expectValueDepthEquals(2, @as(?u32, 1)); + try expectValueDepthEquals(1, @as(?u32, null)); + try expectValueDepthEquals(1, null); + try expectValueDepthEquals(2, &1); + try expectValueDepthEquals(3, &@as(?u32, 1)); + + const Union = union(enum) { + x: u32, + y: struct { x: u32 }, + }; + try expectValueDepthEquals(2, Union{ .x = 1 }); + try expectValueDepthEquals(3, Union{ .y = .{ .x = 1 } }); + + const Recurse = struct { r: ?*const @This() }; + try expectValueDepthEquals(2, Recurse{ .r = null }); + try expectValueDepthEquals(5, Recurse{ .r = &Recurse{ .r = null } }); + try expectValueDepthEquals(8, Recurse{ .r = &Recurse{ .r = &Recurse{ .r = null } } }); + + try expectValueDepthEquals(2, @as([]const u8, &.{ 1, 2, 3 })); + try expectValueDepthEquals(3, @as([]const []const u8, &.{&.{ 1, 2, 3 }})); +} + +inline fn canSerializeType(T: type) bool { + comptime return canSerializeTypeInner(T, &.{}, false); +} + +fn canSerializeTypeInner( + T: type, + /// Visited structs and unions, to avoid infinite recursion. + /// Tracking more types is unnecessary, and a little complex due to optional nesting. + visited: []const type, + parent_is_optional: bool, +) bool { + return switch (@typeInfo(T)) { + .bool, + .int, + .float, + .comptime_float, + .comptime_int, + .null, + .enum_literal, + => true, + + .noreturn, + .void, + .type, + .undefined, + .error_union, + .error_set, + .@"fn", + .frame, + .@"anyframe", + .@"opaque", + => false, + + .@"enum" => |@"enum"| @"enum".is_exhaustive, + + .pointer => |pointer| switch (pointer.size) { + .one => canSerializeTypeInner(pointer.child, visited, parent_is_optional), + .slice => canSerializeTypeInner(pointer.child, visited, false), + .many, .c => false, + }, + + .optional => |optional| if (parent_is_optional) + false + else + canSerializeTypeInner(optional.child, visited, true), + + .array => |array| canSerializeTypeInner(array.child, visited, false), + .vector => |vector| canSerializeTypeInner(vector.child, visited, false), + + .@"struct" => |@"struct"| { + for (visited) |V| if (T == V) return true; + const new_visited = visited ++ .{T}; + for (@"struct".fields) |field| { + if (!canSerializeTypeInner(field.type, new_visited, false)) return false; + } + return true; + }, + .@"union" => |@"union"| { + for (visited) |V| if (T == V) return true; + const new_visited = visited ++ .{T}; + if (@"union".tag_type == null) return false; + for (@"union".fields) |field| { + if (field.type != void and !canSerializeTypeInner(field.type, new_visited, false)) { + return false; + } + } + return true; + }, + }; +} + +test canSerializeType { + try std.testing.expect(!comptime canSerializeType(void)); + try std.testing.expect(!comptime canSerializeType(struct { f: [*]u8 })); + try std.testing.expect(!comptime canSerializeType(struct { error{foo} })); + try std.testing.expect(!comptime canSerializeType(union(enum) { a: void, f: [*c]u8 })); + try std.testing.expect(!comptime canSerializeType(@Vector(0, [*c]u8))); + try std.testing.expect(!comptime canSerializeType(*?[*c]u8)); + try std.testing.expect(!comptime canSerializeType(enum(u8) { _ })); + try std.testing.expect(!comptime canSerializeType(union { foo: void })); + try std.testing.expect(comptime canSerializeType(union(enum) { foo: void })); + try std.testing.expect(comptime canSerializeType(comptime_float)); + try std.testing.expect(comptime canSerializeType(comptime_int)); + try std.testing.expect(!comptime canSerializeType(struct { comptime foo: ??u8 = null })); + try std.testing.expect(comptime canSerializeType(@TypeOf(.foo))); + try std.testing.expect(comptime canSerializeType(?u8)); + try std.testing.expect(comptime canSerializeType(*?*u8)); + try std.testing.expect(comptime canSerializeType(?struct { + foo: ?struct { + ?union(enum) { + a: ?@Vector(0, ?*u8), + }, + ?struct { + f: ?[]?u8, + }, + }, + })); + try std.testing.expect(!comptime canSerializeType(??u8)); + try std.testing.expect(!comptime canSerializeType(?*?u8)); + try std.testing.expect(!comptime canSerializeType(*?*?*u8)); + try std.testing.expect(comptime canSerializeType(struct { x: comptime_int = 2 })); + try std.testing.expect(comptime canSerializeType(struct { x: comptime_float = 2 })); + try std.testing.expect(comptime canSerializeType(struct { comptime_int })); + try std.testing.expect(comptime canSerializeType(struct { comptime x: @TypeOf(.foo) = .foo })); + const Recursive = struct { foo: ?*@This() }; + try std.testing.expect(comptime canSerializeType(Recursive)); + + // Make sure we validate nested optional before we early out due to already having seen + // a type recursion! + try std.testing.expect(!comptime canSerializeType(struct { + add_to_visited: ?u8, + retrieve_from_visited: ??u8, + })); +} diff --git a/lib/std/zon/stringify.zig b/lib/std/zon/stringify.zig index 4cb2640063..785a303f22 100644 --- a/lib/std/zon/stringify.zig +++ b/lib/std/zon/stringify.zig @@ -23,13 +23,13 @@ const std = @import("std"); const assert = std.debug.assert; const Writer = std.Io.Writer; +const Serializer = std.zon.Serializer; -/// Options for `serialize`. pub const SerializeOptions = struct { /// If false, whitespace is omitted. Otherwise whitespace is emitted in standard Zig style. whitespace: bool = true, /// Determines when to emit Unicode code point literals as opposed to integer literals. - emit_codepoint_literals: EmitCodepointLiterals = .never, + emit_codepoint_literals: Serializer.EmitCodepointLiterals = .never, /// If true, slices of `u8`s, and pointers to arrays of `u8` are serialized as containers. /// Otherwise they are serialized as string literals. emit_strings_as_containers: bool = false, @@ -93,102 +93,6 @@ pub fn serializeArbitraryDepth( }); } -inline fn typeIsRecursive(comptime T: type) bool { - return comptime typeIsRecursiveInner(T, &.{}); -} - -fn typeIsRecursiveInner(comptime T: type, comptime prev_visited: []const type) bool { - for (prev_visited) |V| { - if (V == T) return true; - } - const visited = prev_visited ++ .{T}; - - return switch (@typeInfo(T)) { - .pointer => |pointer| typeIsRecursiveInner(pointer.child, visited), - .optional => |optional| typeIsRecursiveInner(optional.child, visited), - .array => |array| typeIsRecursiveInner(array.child, visited), - .vector => |vector| typeIsRecursiveInner(vector.child, visited), - .@"struct" => |@"struct"| for (@"struct".fields) |field| { - if (typeIsRecursiveInner(field.type, visited)) break true; - } else false, - .@"union" => |@"union"| inline for (@"union".fields) |field| { - if (typeIsRecursiveInner(field.type, visited)) break true; - } else false, - else => false, - }; -} - -inline fn canSerializeType(T: type) bool { - comptime return canSerializeTypeInner(T, &.{}, false); -} - -fn canSerializeTypeInner( - T: type, - /// Visited structs and unions, to avoid infinite recursion. - /// Tracking more types is unnecessary, and a little complex due to optional nesting. - visited: []const type, - parent_is_optional: bool, -) bool { - return switch (@typeInfo(T)) { - .bool, - .int, - .float, - .comptime_float, - .comptime_int, - .null, - .enum_literal, - => true, - - .noreturn, - .void, - .type, - .undefined, - .error_union, - .error_set, - .@"fn", - .frame, - .@"anyframe", - .@"opaque", - => false, - - .@"enum" => |@"enum"| @"enum".is_exhaustive, - - .pointer => |pointer| switch (pointer.size) { - .one => canSerializeTypeInner(pointer.child, visited, parent_is_optional), - .slice => canSerializeTypeInner(pointer.child, visited, false), - .many, .c => false, - }, - - .optional => |optional| if (parent_is_optional) - false - else - canSerializeTypeInner(optional.child, visited, true), - - .array => |array| canSerializeTypeInner(array.child, visited, false), - .vector => |vector| canSerializeTypeInner(vector.child, visited, false), - - .@"struct" => |@"struct"| { - for (visited) |V| if (T == V) return true; - const new_visited = visited ++ .{T}; - for (@"struct".fields) |field| { - if (!canSerializeTypeInner(field.type, new_visited, false)) return false; - } - return true; - }, - .@"union" => |@"union"| { - for (visited) |V| if (T == V) return true; - const new_visited = visited ++ .{T}; - if (@"union".tag_type == null) return false; - for (@"union".fields) |field| { - if (field.type != void and !canSerializeTypeInner(field.type, new_visited, false)) { - return false; - } - } - return true; - }, - }; -} - fn isNestedOptional(T: type) bool { comptime switch (@typeInfo(T)) { .optional => |optional| return isNestedOptionalInner(optional.child), @@ -210,842 +114,6 @@ fn isNestedOptionalInner(T: type) bool { } } -test "std.zon stringify canSerializeType" { - try std.testing.expect(!comptime canSerializeType(void)); - try std.testing.expect(!comptime canSerializeType(struct { f: [*]u8 })); - try std.testing.expect(!comptime canSerializeType(struct { error{foo} })); - try std.testing.expect(!comptime canSerializeType(union(enum) { a: void, f: [*c]u8 })); - try std.testing.expect(!comptime canSerializeType(@Vector(0, [*c]u8))); - try std.testing.expect(!comptime canSerializeType(*?[*c]u8)); - try std.testing.expect(!comptime canSerializeType(enum(u8) { _ })); - try std.testing.expect(!comptime canSerializeType(union { foo: void })); - try std.testing.expect(comptime canSerializeType(union(enum) { foo: void })); - try std.testing.expect(comptime canSerializeType(comptime_float)); - try std.testing.expect(comptime canSerializeType(comptime_int)); - try std.testing.expect(!comptime canSerializeType(struct { comptime foo: ??u8 = null })); - try std.testing.expect(comptime canSerializeType(@TypeOf(.foo))); - try std.testing.expect(comptime canSerializeType(?u8)); - try std.testing.expect(comptime canSerializeType(*?*u8)); - try std.testing.expect(comptime canSerializeType(?struct { - foo: ?struct { - ?union(enum) { - a: ?@Vector(0, ?*u8), - }, - ?struct { - f: ?[]?u8, - }, - }, - })); - try std.testing.expect(!comptime canSerializeType(??u8)); - try std.testing.expect(!comptime canSerializeType(?*?u8)); - try std.testing.expect(!comptime canSerializeType(*?*?*u8)); - try std.testing.expect(comptime canSerializeType(struct { x: comptime_int = 2 })); - try std.testing.expect(comptime canSerializeType(struct { x: comptime_float = 2 })); - try std.testing.expect(comptime canSerializeType(struct { comptime_int })); - try std.testing.expect(comptime canSerializeType(struct { comptime x: @TypeOf(.foo) = .foo })); - const Recursive = struct { foo: ?*@This() }; - try std.testing.expect(comptime canSerializeType(Recursive)); - - // Make sure we validate nested optional before we early out due to already having seen - // a type recursion! - try std.testing.expect(!comptime canSerializeType(struct { - add_to_visited: ?u8, - retrieve_from_visited: ??u8, - })); -} - -test "std.zon typeIsRecursive" { - try std.testing.expect(!typeIsRecursive(bool)); - try std.testing.expect(!typeIsRecursive(struct { x: i32, y: i32 })); - try std.testing.expect(!typeIsRecursive(struct { i32, i32 })); - try std.testing.expect(typeIsRecursive(struct { x: i32, y: i32, z: *@This() })); - try std.testing.expect(typeIsRecursive(struct { - a: struct { - const A = @This(); - b: struct { - c: *struct { - a: ?A, - }, - }, - }, - })); - try std.testing.expect(typeIsRecursive(struct { - a: [3]*@This(), - })); - try std.testing.expect(typeIsRecursive(struct { - a: union { a: i32, b: *@This() }, - })); -} - -fn checkValueDepth(val: anytype, depth: usize) error{ExceededMaxDepth}!void { - if (depth == 0) return error.ExceededMaxDepth; - const child_depth = depth - 1; - - switch (@typeInfo(@TypeOf(val))) { - .pointer => |pointer| switch (pointer.size) { - .one => try checkValueDepth(val.*, child_depth), - .slice => for (val) |item| { - try checkValueDepth(item, child_depth); - }, - .c, .many => {}, - }, - .array => for (val) |item| { - try checkValueDepth(item, child_depth); - }, - .@"struct" => |@"struct"| inline for (@"struct".fields) |field_info| { - try checkValueDepth(@field(val, field_info.name), child_depth); - }, - .@"union" => |@"union"| if (@"union".tag_type == null) { - return; - } else switch (val) { - inline else => |payload| { - return checkValueDepth(payload, child_depth); - }, - }, - .optional => if (val) |inner| try checkValueDepth(inner, child_depth), - else => {}, - } -} - -fn expectValueDepthEquals(expected: usize, value: anytype) !void { - try checkValueDepth(value, expected); - try std.testing.expectError(error.ExceededMaxDepth, checkValueDepth(value, expected - 1)); -} - -test "std.zon checkValueDepth" { - try expectValueDepthEquals(1, 10); - try expectValueDepthEquals(2, .{ .x = 1, .y = 2 }); - try expectValueDepthEquals(2, .{ 1, 2 }); - try expectValueDepthEquals(3, .{ 1, .{ 2, 3 } }); - try expectValueDepthEquals(3, .{ .{ 1, 2 }, 3 }); - try expectValueDepthEquals(3, .{ .x = 0, .y = 1, .z = .{ .x = 3 } }); - try expectValueDepthEquals(3, .{ .x = 0, .y = .{ .x = 1 }, .z = 2 }); - try expectValueDepthEquals(3, .{ .x = .{ .x = 0 }, .y = 1, .z = 2 }); - try expectValueDepthEquals(2, @as(?u32, 1)); - try expectValueDepthEquals(1, @as(?u32, null)); - try expectValueDepthEquals(1, null); - try expectValueDepthEquals(2, &1); - try expectValueDepthEquals(3, &@as(?u32, 1)); - - const Union = union(enum) { - x: u32, - y: struct { x: u32 }, - }; - try expectValueDepthEquals(2, Union{ .x = 1 }); - try expectValueDepthEquals(3, Union{ .y = .{ .x = 1 } }); - - const Recurse = struct { r: ?*const @This() }; - try expectValueDepthEquals(2, Recurse{ .r = null }); - try expectValueDepthEquals(5, Recurse{ .r = &Recurse{ .r = null } }); - try expectValueDepthEquals(8, Recurse{ .r = &Recurse{ .r = &Recurse{ .r = null } } }); - - try expectValueDepthEquals(2, @as([]const u8, &.{ 1, 2, 3 })); - try expectValueDepthEquals(3, @as([]const []const u8, &.{&.{ 1, 2, 3 }})); -} - -/// Determines when to emit Unicode code point literals as opposed to integer literals. -pub const EmitCodepointLiterals = enum { - /// Never emit Unicode code point literals. - never, - /// Emit Unicode code point literals for any `u8` in the printable ASCII range. - printable_ascii, - /// Emit Unicode code point literals for any unsigned integer with 21 bits or fewer - /// whose value is a valid non-surrogate code point. - always, - - /// If the value should be emitted as a Unicode codepoint, return it as a u21. - fn emitAsCodepoint(self: @This(), val: anytype) ?u21 { - // Rule out incompatible integer types - switch (@typeInfo(@TypeOf(val))) { - .int => |int_info| if (int_info.signedness == .signed or int_info.bits > 21) { - return null; - }, - .comptime_int => {}, - else => comptime unreachable, - } - - // Return null if the value shouldn't be printed as a Unicode codepoint, or the value casted - // to a u21 if it should. - switch (self) { - .always => { - const c = std.math.cast(u21, val) orelse return null; - if (!std.unicode.utf8ValidCodepoint(c)) return null; - return c; - }, - .printable_ascii => { - const c = std.math.cast(u8, val) orelse return null; - if (!std.ascii.isPrint(c)) return null; - return c; - }, - .never => { - return null; - }, - } - } -}; - -/// Options for serialization of an individual value. -/// -/// See `SerializeOptions` for more information on these options. -pub const ValueOptions = struct { - emit_codepoint_literals: EmitCodepointLiterals = .never, - emit_strings_as_containers: bool = false, - emit_default_optional_fields: bool = true, -}; - -/// Options for manual serialization of container types. -pub const SerializeContainerOptions = struct { - /// The whitespace style that should be used for this container. Ignored if whitespace is off. - whitespace_style: union(enum) { - /// If true, wrap every field. If false do not. - wrap: bool, - /// Automatically decide whether to wrap or not based on the number of fields. Following - /// the standard rule of thumb, containers with more than two fields are wrapped. - fields: usize, - } = .{ .wrap = true }, - - fn shouldWrap(self: SerializeContainerOptions) bool { - return switch (self.whitespace_style) { - .wrap => |wrap| wrap, - .fields => |fields| fields > 2, - }; - } -}; - -/// Lower level control over serialization, you can create a new instance with `serializer`. -/// -/// Useful when you want control over which fields are serialized, how they're represented, -/// or want to write a ZON object that does not exist in memory. -/// -/// You can serialize values with `value`. To serialize recursive types, the following are provided: -/// * `valueMaxDepth` -/// * `valueArbitraryDepth` -/// -/// You can also serialize values using specific notations: -/// * `int` -/// * `float` -/// * `codePoint` -/// * `tuple` -/// * `tupleMaxDepth` -/// * `tupleArbitraryDepth` -/// * `string` -/// * `multilineString` -/// -/// For manual serialization of containers, see: -/// * `beginStruct` -/// * `beginTuple` -pub const Serializer = struct { - options: Options = .{}, - indent_level: u8 = 0, - writer: *Writer, - - pub const Error = Writer.Error; - pub const DepthError = Error || error{ExceededMaxDepth}; - - pub const Options = struct { - /// If false, only syntactically necessary whitespace is emitted. - whitespace: bool = true, - }; - - /// Serialize a value, similar to `serialize`. - pub fn value(self: *Serializer, val: anytype, options: ValueOptions) Error!void { - comptime assert(!typeIsRecursive(@TypeOf(val))); - return self.valueArbitraryDepth(val, options); - } - - /// Serialize a value, similar to `serializeMaxDepth`. - /// Can return `error.ExceededMaxDepth`. - pub fn valueMaxDepth(self: *Serializer, val: anytype, options: ValueOptions, depth: usize) DepthError!void { - try checkValueDepth(val, depth); - return self.valueArbitraryDepth(val, options); - } - - /// Serialize a value, similar to `serializeArbitraryDepth`. - pub fn valueArbitraryDepth(self: *Serializer, val: anytype, options: ValueOptions) Error!void { - comptime assert(canSerializeType(@TypeOf(val))); - switch (@typeInfo(@TypeOf(val))) { - .int, .comptime_int => if (options.emit_codepoint_literals.emitAsCodepoint(val)) |c| { - self.codePoint(c) catch |err| switch (err) { - error.InvalidCodepoint => unreachable, // Already validated - else => |e| return e, - }; - } else { - try self.int(val); - }, - .float, .comptime_float => try self.float(val), - .bool, .null => try self.writer.print("{}", .{val}), - .enum_literal => try self.ident(@tagName(val)), - .@"enum" => try self.ident(@tagName(val)), - .pointer => |pointer| { - // Try to serialize as a string - const item: ?type = switch (@typeInfo(pointer.child)) { - .array => |array| array.child, - else => if (pointer.size == .slice) pointer.child else null, - }; - if (item == u8 and - (pointer.sentinel() == null or pointer.sentinel() == 0) and - !options.emit_strings_as_containers) - { - return try self.string(val); - } - - // Serialize as either a tuple or as the child type - switch (pointer.size) { - .slice => try self.tupleImpl(val, options), - .one => try self.valueArbitraryDepth(val.*, options), - else => comptime unreachable, - } - }, - .array => { - var container = try self.beginTuple( - .{ .whitespace_style = .{ .fields = val.len } }, - ); - for (val) |item_val| { - try container.fieldArbitraryDepth(item_val, options); - } - try container.end(); - }, - .@"struct" => |@"struct"| if (@"struct".is_tuple) { - var container = try self.beginTuple( - .{ .whitespace_style = .{ .fields = @"struct".fields.len } }, - ); - inline for (val) |field_value| { - try container.fieldArbitraryDepth(field_value, options); - } - try container.end(); - } else { - // Decide which fields to emit - const fields, const skipped: [@"struct".fields.len]bool = if (options.emit_default_optional_fields) b: { - break :b .{ @"struct".fields.len, @splat(false) }; - } else b: { - var fields = @"struct".fields.len; - var skipped: [@"struct".fields.len]bool = @splat(false); - inline for (@"struct".fields, &skipped) |field_info, *skip| { - if (field_info.default_value_ptr) |ptr| { - const default: *const field_info.type = @ptrCast(@alignCast(ptr)); - const field_value = @field(val, field_info.name); - if (std.meta.eql(field_value, default.*)) { - skip.* = true; - fields -= 1; - } - } - } - break :b .{ fields, skipped }; - }; - - // Emit those fields - var container = try self.beginStruct( - .{ .whitespace_style = .{ .fields = fields } }, - ); - inline for (@"struct".fields, skipped) |field_info, skip| { - if (!skip) { - try container.fieldArbitraryDepth( - field_info.name, - @field(val, field_info.name), - options, - ); - } - } - try container.end(); - }, - .@"union" => |@"union"| { - comptime assert(@"union".tag_type != null); - switch (val) { - inline else => |pl, tag| if (@TypeOf(pl) == void) - try self.writer.print(".{s}", .{@tagName(tag)}) - else { - var container = try self.beginStruct(.{ .whitespace_style = .{ .fields = 1 } }); - - try container.fieldArbitraryDepth( - @tagName(tag), - pl, - options, - ); - - try container.end(); - }, - } - }, - .optional => if (val) |inner| { - try self.valueArbitraryDepth(inner, options); - } else { - try self.writer.writeAll("null"); - }, - .vector => |vector| { - var container = try self.beginTuple( - .{ .whitespace_style = .{ .fields = vector.len } }, - ); - for (0..vector.len) |i| { - try container.fieldArbitraryDepth(val[i], options); - } - try container.end(); - }, - - else => comptime unreachable, - } - } - - /// Serialize an integer. - pub fn int(self: *Serializer, val: anytype) Error!void { - try self.writer.printInt(val, 10, .lower, .{}); - } - - /// Serialize a float. - pub fn float(self: *Serializer, val: anytype) Error!void { - switch (@typeInfo(@TypeOf(val))) { - .float => if (std.math.isNan(val)) { - return self.writer.writeAll("nan"); - } else if (std.math.isPositiveInf(val)) { - return self.writer.writeAll("inf"); - } else if (std.math.isNegativeInf(val)) { - return self.writer.writeAll("-inf"); - } else if (std.math.isNegativeZero(val)) { - return self.writer.writeAll("-0.0"); - } else { - try self.writer.print("{d}", .{val}); - }, - .comptime_float => if (val == 0) { - return self.writer.writeAll("0"); - } else { - try self.writer.print("{d}", .{val}); - }, - else => comptime unreachable, - } - } - - /// Serialize `name` as an identifier prefixed with `.`. - /// - /// Escapes the identifier if necessary. - pub fn ident(self: *Serializer, name: []const u8) Error!void { - try self.writer.print(".{f}", .{std.zig.fmtIdPU(name)}); - } - - pub const CodePointError = Error || error{InvalidCodepoint}; - - /// Serialize `val` as a Unicode codepoint. - /// - /// Returns `error.InvalidCodepoint` if `val` is not a valid Unicode codepoint. - pub fn codePoint(self: *Serializer, val: u21) CodePointError!void { - try self.writer.print("'{f}'", .{std.zig.fmtChar(val)}); - } - - /// Like `value`, but always serializes `val` as a tuple. - /// - /// Will fail at comptime if `val` is not a tuple, array, pointer to an array, or slice. - pub fn tuple(self: *Serializer, val: anytype, options: ValueOptions) Error!void { - comptime assert(!typeIsRecursive(@TypeOf(val))); - try self.tupleArbitraryDepth(val, options); - } - - /// Like `tuple`, but recursive types are allowed. - /// - /// Returns `error.ExceededMaxDepth` if `depth` is exceeded. - pub fn tupleMaxDepth( - self: *Serializer, - val: anytype, - options: ValueOptions, - depth: usize, - ) DepthError!void { - try checkValueDepth(val, depth); - try self.tupleArbitraryDepth(val, options); - } - - /// Like `tuple`, but recursive types are allowed. - /// - /// It is the caller's responsibility to ensure that `val` does not contain cycles. - pub fn tupleArbitraryDepth( - self: *Serializer, - val: anytype, - options: ValueOptions, - ) Error!void { - try self.tupleImpl(val, options); - } - - fn tupleImpl(self: *Serializer, val: anytype, options: ValueOptions) Error!void { - comptime assert(canSerializeType(@TypeOf(val))); - switch (@typeInfo(@TypeOf(val))) { - .@"struct" => { - var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } }); - inline for (val) |item_val| { - try container.fieldArbitraryDepth(item_val, options); - } - try container.end(); - }, - .pointer, .array => { - var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } }); - for (val) |item_val| { - try container.fieldArbitraryDepth(item_val, options); - } - try container.end(); - }, - else => comptime unreachable, - } - } - - /// Like `value`, but always serializes `val` as a string. - pub fn string(self: *Serializer, val: []const u8) Error!void { - try self.writer.print("\"{f}\"", .{std.zig.fmtString(val)}); - } - - /// Options for formatting multiline strings. - pub const MultilineStringOptions = struct { - /// If top level is true, whitespace before and after the multiline string is elided. - /// If it is true, a newline is printed, then the value, followed by a newline, and if - /// whitespace is true any necessary indentation follows. - top_level: bool = false, - }; - - pub const MultilineStringError = Error || error{InnerCarriageReturn}; - - /// Like `value`, but always serializes to a multiline string literal. - /// - /// Returns `error.InnerCarriageReturn` if `val` contains a CR not followed by a newline, - /// since multiline strings cannot represent CR without a following newline. - pub fn multilineString( - self: *Serializer, - val: []const u8, - options: MultilineStringOptions, - ) MultilineStringError!void { - // Make sure the string does not contain any carriage returns not followed by a newline - var i: usize = 0; - while (i < val.len) : (i += 1) { - if (val[i] == '\r') { - if (i + 1 < val.len) { - if (val[i + 1] == '\n') { - i += 1; - continue; - } - } - return error.InnerCarriageReturn; - } - } - - if (!options.top_level) { - try self.newline(); - try self.indent(); - } - - try self.writer.writeAll("\\\\"); - for (val) |c| { - if (c != '\r') { - try self.writer.writeByte(c); // We write newlines here even if whitespace off - if (c == '\n') { - try self.indent(); - try self.writer.writeAll("\\\\"); - } - } - } - - if (!options.top_level) { - try self.writer.writeByte('\n'); // Even if whitespace off - try self.indent(); - } - } - - /// Create a `Struct` for writing ZON structs field by field. - pub fn beginStruct( - self: *Serializer, - options: SerializeContainerOptions, - ) Error!Struct { - return Struct.begin(self, options); - } - - /// Creates a `Tuple` for writing ZON tuples field by field. - pub fn beginTuple( - self: *Serializer, - options: SerializeContainerOptions, - ) Error!Tuple { - return Tuple.begin(self, options); - } - - fn indent(self: *Serializer) Error!void { - if (self.options.whitespace) { - try self.writer.splatByteAll(' ', 4 * self.indent_level); - } - } - - fn newline(self: *Serializer) Error!void { - if (self.options.whitespace) { - try self.writer.writeByte('\n'); - } - } - - fn newlineOrSpace(self: *Serializer, len: usize) Error!void { - if (self.containerShouldWrap(len)) { - try self.newline(); - } else { - try self.space(); - } - } - - fn space(self: *Serializer) Error!void { - if (self.options.whitespace) { - try self.writer.writeByte(' '); - } - } - - /// Writes ZON tuples field by field. - pub const Tuple = struct { - container: Container, - - fn begin(parent: *Serializer, options: SerializeContainerOptions) Error!Tuple { - return .{ - .container = try Container.begin(parent, .anon, options), - }; - } - - /// Finishes serializing the tuple. - /// - /// Prints a trailing comma as configured when appropriate, and the closing bracket. - pub fn end(self: *Tuple) Error!void { - try self.container.end(); - self.* = undefined; - } - - /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `value`. - pub fn field( - self: *Tuple, - val: anytype, - options: ValueOptions, - ) Error!void { - try self.container.field(null, val, options); - } - - /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `valueMaxDepth`. - /// Returns `error.ExceededMaxDepth` if `depth` is exceeded. - pub fn fieldMaxDepth( - self: *Tuple, - val: anytype, - options: ValueOptions, - depth: usize, - ) DepthError!void { - try self.container.fieldMaxDepth(null, val, options, depth); - } - - /// Serialize a field. Equivalent to calling `fieldPrefix` followed by - /// `valueArbitraryDepth`. - pub fn fieldArbitraryDepth( - self: *Tuple, - val: anytype, - options: ValueOptions, - ) Error!void { - try self.container.fieldArbitraryDepth(null, val, options); - } - - /// Starts a field with a struct as a value. Returns the struct. - pub fn beginStructField( - self: *Tuple, - options: SerializeContainerOptions, - ) Error!Struct { - try self.fieldPrefix(); - return self.container.serializer.beginStruct(options); - } - - /// Starts a field with a tuple as a value. Returns the tuple. - pub fn beginTupleField( - self: *Tuple, - options: SerializeContainerOptions, - ) Error!Tuple { - try self.fieldPrefix(); - return self.container.serializer.beginTuple(options); - } - - /// Print a field prefix. This prints any necessary commas, and whitespace as - /// configured. Useful if you want to serialize the field value yourself. - pub fn fieldPrefix(self: *Tuple) Error!void { - try self.container.fieldPrefix(null); - } - }; - - /// Writes ZON structs field by field. - pub const Struct = struct { - container: Container, - - fn begin(parent: *Serializer, options: SerializeContainerOptions) Error!Struct { - return .{ - .container = try Container.begin(parent, .named, options), - }; - } - - /// Finishes serializing the struct. - /// - /// Prints a trailing comma as configured when appropriate, and the closing bracket. - pub fn end(self: *Struct) Error!void { - try self.container.end(); - self.* = undefined; - } - - /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `value`. - pub fn field( - self: *Struct, - name: []const u8, - val: anytype, - options: ValueOptions, - ) Error!void { - try self.container.field(name, val, options); - } - - /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `valueMaxDepth`. - /// Returns `error.ExceededMaxDepth` if `depth` is exceeded. - pub fn fieldMaxDepth( - self: *Struct, - name: []const u8, - val: anytype, - options: ValueOptions, - depth: usize, - ) DepthError!void { - try self.container.fieldMaxDepth(name, val, options, depth); - } - - /// Serialize a field. Equivalent to calling `fieldPrefix` followed by - /// `valueArbitraryDepth`. - pub fn fieldArbitraryDepth( - self: *Struct, - name: []const u8, - val: anytype, - options: ValueOptions, - ) Error!void { - try self.container.fieldArbitraryDepth(name, val, options); - } - - /// Starts a field with a struct as a value. Returns the struct. - pub fn beginStructField( - self: *Struct, - name: []const u8, - options: SerializeContainerOptions, - ) Error!Struct { - try self.fieldPrefix(name); - return self.container.serializer.beginStruct(options); - } - - /// Starts a field with a tuple as a value. Returns the tuple. - pub fn beginTupleField( - self: *Struct, - name: []const u8, - options: SerializeContainerOptions, - ) Error!Tuple { - try self.fieldPrefix(name); - return self.container.serializer.beginTuple(options); - } - - /// Print a field prefix. This prints any necessary commas, the field name (escaped if - /// necessary) and whitespace as configured. Useful if you want to serialize the field - /// value yourself. - pub fn fieldPrefix(self: *Struct, name: []const u8) Error!void { - try self.container.fieldPrefix(name); - } - }; - - const Container = struct { - const FieldStyle = enum { named, anon }; - - serializer: *Serializer, - field_style: FieldStyle, - options: SerializeContainerOptions, - empty: bool, - - fn begin( - sz: *Serializer, - field_style: FieldStyle, - options: SerializeContainerOptions, - ) Error!Container { - if (options.shouldWrap()) sz.indent_level +|= 1; - try sz.writer.writeAll(".{"); - return .{ - .serializer = sz, - .field_style = field_style, - .options = options, - .empty = true, - }; - } - - fn end(self: *Container) Error!void { - if (self.options.shouldWrap()) self.serializer.indent_level -|= 1; - if (!self.empty) { - if (self.options.shouldWrap()) { - if (self.serializer.options.whitespace) { - try self.serializer.writer.writeByte(','); - } - try self.serializer.newline(); - try self.serializer.indent(); - } else if (!self.shouldElideSpaces()) { - try self.serializer.space(); - } - } - try self.serializer.writer.writeByte('}'); - self.* = undefined; - } - - fn fieldPrefix(self: *Container, name: ?[]const u8) Error!void { - if (!self.empty) { - try self.serializer.writer.writeByte(','); - } - self.empty = false; - if (self.options.shouldWrap()) { - try self.serializer.newline(); - } else if (!self.shouldElideSpaces()) { - try self.serializer.space(); - } - if (self.options.shouldWrap()) try self.serializer.indent(); - if (name) |n| { - try self.serializer.ident(n); - try self.serializer.space(); - try self.serializer.writer.writeByte('='); - try self.serializer.space(); - } - } - - fn field( - self: *Container, - name: ?[]const u8, - val: anytype, - options: ValueOptions, - ) Error!void { - comptime assert(!typeIsRecursive(@TypeOf(val))); - try self.fieldArbitraryDepth(name, val, options); - } - - /// Returns `error.ExceededMaxDepth` if `depth` is exceeded. - fn fieldMaxDepth( - self: *Container, - name: ?[]const u8, - val: anytype, - options: ValueOptions, - depth: usize, - ) DepthError!void { - try checkValueDepth(val, depth); - try self.fieldArbitraryDepth(name, val, options); - } - - fn fieldArbitraryDepth( - self: *Container, - name: ?[]const u8, - val: anytype, - options: ValueOptions, - ) Error!void { - try self.fieldPrefix(name); - try self.serializer.valueArbitraryDepth(val, options); - } - - fn shouldElideSpaces(self: *const Container) bool { - return switch (self.options.whitespace_style) { - .fields => |fields| self.field_style != .named and fields == 1, - else => false, - }; - } - }; -}; - -test Serializer { - var discarding: Writer.Discarding = .init(&.{}); - var s: Serializer = .{ .writer = &discarding.writer }; - var vec2 = try s.beginStruct(.{}); - try vec2.field("x", 1.5, .{}); - try vec2.fieldPrefix("prefix"); - try s.value(2.5, .{}); - try vec2.end(); -} - fn expectSerializeEqual( expected: []const u8, value: anytype, diff --git a/src/main.zig b/src/main.zig index 019ae8bc7e..2eb5762a0c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -344,8 +344,9 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { } else if (mem.eql(u8, cmd, "targets")) { dev.check(.targets_command); const host = std.zig.resolveTargetQueryOrFatal(.{}); - const stdout = fs.File.stdout().deprecatedWriter(); - return @import("print_targets.zig").cmdTargets(arena, cmd_args, stdout, &host); + var stdout_writer = fs.File.stdout().writer(&stdio_buffer); + try @import("print_targets.zig").cmdTargets(arena, cmd_args, &stdout_writer.interface, &host); + return stdout_writer.interface.flush(); } else if (mem.eql(u8, cmd, "version")) { dev.check(.version_command); try fs.File.stdout().writeAll(build_options.version ++ "\n"); @@ -356,7 +357,9 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { } else if (mem.eql(u8, cmd, "env")) { dev.check(.env_command); verifyLibcxxCorrectlyLinked(); - return @import("print_env.zig").cmdEnv(arena, cmd_args); + var stdout_writer = fs.File.stdout().writer(&stdio_buffer); + try @import("print_env.zig").cmdEnv(arena, &stdout_writer.interface); + return stdout_writer.interface.flush(); } else if (mem.eql(u8, cmd, "reduce")) { return jitCmd(gpa, arena, cmd_args, .{ .cmd_name = "reduce", diff --git a/src/print_env.zig b/src/print_env.zig index d1546abc6c..d1251c0d62 100644 --- a/src/print_env.zig +++ b/src/print_env.zig @@ -4,8 +4,7 @@ const introspect = @import("introspect.zig"); const Allocator = std.mem.Allocator; const fatal = std.process.fatal; -pub fn cmdEnv(arena: Allocator, args: []const []const u8) !void { - _ = args; +pub fn cmdEnv(arena: Allocator, out: *std.Io.Writer) !void { const cwd_path = try introspect.getResolvedCwd(arena); const self_exe_path = try std.fs.selfExePathAlloc(arena); @@ -21,41 +20,21 @@ pub fn cmdEnv(arena: Allocator, args: []const []const u8) !void { const host = try std.zig.system.resolveTargetQuery(.{}); const triple = try host.zigTriple(arena); - var buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&buffer); + var serializer: std.zon.Serializer = .{ .writer = out }; + var root = try serializer.beginStruct(.{}); - var jws: std.json.Stringify = .{ .writer = &stdout_writer.interface, .options = .{ .whitespace = .indent_1 } }; - - try jws.beginObject(); - - try jws.objectField("zig_exe"); - try jws.write(self_exe_path); - - try jws.objectField("lib_dir"); - try jws.write(zig_lib_directory.path.?); - - try jws.objectField("std_dir"); - try jws.write(zig_std_dir); - - try jws.objectField("global_cache_dir"); - try jws.write(global_cache_dir); - - try jws.objectField("version"); - try jws.write(build_options.version); - - try jws.objectField("target"); - try jws.write(triple); - - try jws.objectField("env"); - try jws.beginObject(); + try root.field("zig_exe", self_exe_path, .{}); + try root.field("lib_dir", zig_lib_directory.path.?, .{}); + try root.field("std_dir", zig_std_dir, .{}); + try root.field("global_cache_dir", global_cache_dir, .{}); + try root.field("version", build_options.version, .{}); + try root.field("target", triple, .{}); + var env = try root.beginStructField("env", .{}); inline for (@typeInfo(std.zig.EnvVar).@"enum".fields) |field| { - try jws.objectField(field.name); - try jws.write(try @field(std.zig.EnvVar, field.name).get(arena)); + try env.field(field.name, try @field(std.zig.EnvVar, field.name).get(arena), .{}); } - try jws.endObject(); + try env.end(); + try root.end(); - try jws.endObject(); - - try stdout_writer.interface.writeByte('\n'); - try stdout_writer.interface.flush(); + try out.writeByte('\n'); } diff --git a/src/print_targets.zig b/src/print_targets.zig index e234aeda26..e04842bb7e 100644 --- a/src/print_targets.zig +++ b/src/print_targets.zig @@ -14,8 +14,7 @@ const introspect = @import("introspect.zig"); pub fn cmdTargets( allocator: Allocator, args: []const []const u8, - /// Output stream - stdout: anytype, + out: *std.Io.Writer, native_target: *const Target, ) !void { _ = args; @@ -38,12 +37,10 @@ pub fn cmdTargets( const glibc_abi = try glibc.loadMetaData(allocator, abilists_contents); defer glibc_abi.destroy(allocator); - var bw = io.bufferedWriter(stdout); - const w = bw.writer(); - var sz = std.zon.stringify.serializer(w, .{}); + var serializer: std.zon.Serializer = .{ .writer = out }; { - var root_obj = try sz.beginStruct(.{}); + var root_obj = try serializer.beginStruct(.{}); try root_obj.field("arch", meta.fieldNames(Target.Cpu.Arch), .{}); try root_obj.field("os", meta.fieldNames(Target.Os.Tag), .{}); @@ -136,6 +133,5 @@ pub fn cmdTargets( try root_obj.end(); } - try w.writeByte('\n'); - return bw.flush(); + try out.writeByte('\n'); } diff --git a/src/translate_c.zig b/src/translate_c.zig index 301e0a219d..f1f3ad8659 100644 --- a/src/translate_c.zig +++ b/src/translate_c.zig @@ -3338,7 +3338,7 @@ fn transPredefinedExpr(c: *Context, scope: *Scope, expr: *const clang.Predefined fn transCreateCharLitNode(c: *Context, narrow: bool, val: u32) TransError!Node { return Tag.char_literal.create(c.arena, if (narrow) - try std.fmt.allocPrint(c.arena, "'{f}'", .{std.zig.fmtChar(&.{@as(u8, @intCast(val))})}) + try std.fmt.allocPrint(c.arena, "'{f}'", .{std.zig.fmtChar(@intCast(val))}) else try std.fmt.allocPrint(c.arena, "'\\u{{{x}}}'", .{val})); }