This commit is contained in:
Jean Dao 2025-11-23 22:56:58 +00:00 committed by GitHub
commit 120811b6c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 200 additions and 106 deletions

View file

@ -122,7 +122,7 @@ pub fn valueMaxDepth(self: *Serializer, val: anytype, options: ValueOptions, dep
/// Serialize a value, similar to `serializeArbitraryDepth`. /// Serialize a value, similar to `serializeArbitraryDepth`.
pub fn valueArbitraryDepth(self: *Serializer, val: anytype, options: ValueOptions) Error!void { pub fn valueArbitraryDepth(self: *Serializer, val: anytype, options: ValueOptions) Error!void {
comptime assert(canSerializeType(@TypeOf(val))); comptime assertCanSerializeType(@TypeOf(val));
switch (@typeInfo(@TypeOf(val))) { switch (@typeInfo(@TypeOf(val))) {
.int, .comptime_int => if (options.emit_codepoint_literals.emitAsCodepoint(val)) |c| { .int, .comptime_int => if (options.emit_codepoint_literals.emitAsCodepoint(val)) |c| {
self.codePoint(c) catch |err| switch (err) { self.codePoint(c) catch |err| switch (err) {
@ -321,7 +321,7 @@ pub fn tupleArbitraryDepth(
} }
fn tupleImpl(self: *Serializer, val: anytype, options: ValueOptions) Error!void { fn tupleImpl(self: *Serializer, val: anytype, options: ValueOptions) Error!void {
comptime assert(canSerializeType(@TypeOf(val))); comptime assertCanSerializeType(@TypeOf(val));
switch (@typeInfo(@TypeOf(val))) { switch (@typeInfo(@TypeOf(val))) {
.@"struct" => { .@"struct" => {
var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } }); var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } });
@ -812,17 +812,36 @@ test checkValueDepth {
try expectValueDepthEquals(3, @as([]const []const u8, &.{&.{ 1, 2, 3 }})); try expectValueDepthEquals(3, @as([]const []const u8, &.{&.{ 1, 2, 3 }}));
} }
inline fn canSerializeType(T: type) bool { fn assertCanSerializeType(T: type) void {
comptime return canSerializeTypeInner(T, &.{}, false); comptime switch (canSerializeType(T)) {
.success => {},
.failure => |FailedType| {
@compileError(std.fmt.comptimePrint("cannot serialize type {}", .{FailedType}));
},
};
} }
const CanSerializeTypeResult = union(enum) {
success,
failure: type,
};
fn canSerializeType(T: type) CanSerializeTypeResult {
comptime return canSerializeTypeInner(T, &.{}).result;
}
const CanSerializeTypeInnerResult = struct {
// Keep track of optionals to reject nested optionals, which cannot be serialized.
is_optional: bool = false,
result: CanSerializeTypeResult,
};
fn canSerializeTypeInner( fn canSerializeTypeInner(
T: type, T: type,
/// Visited structs and unions, to avoid infinite recursion. /// Visited structs and unions, to avoid infinite recursion.
/// Tracking more types is unnecessary, and a little complex due to optional nesting. /// Tracking more types is unnecessary, and a little complex due to optional nesting.
visited: []const type, visited: []const type,
parent_is_optional: bool, ) CanSerializeTypeInnerResult {
) bool {
return switch (@typeInfo(T)) { return switch (@typeInfo(T)) {
.bool, .bool,
.int, .int,
@ -831,7 +850,7 @@ fn canSerializeTypeInner(
.comptime_int, .comptime_int,
.null, .null,
.enum_literal, .enum_literal,
=> true, => .{ .result = .success },
.noreturn, .noreturn,
.void, .void,
@ -843,63 +862,92 @@ fn canSerializeTypeInner(
.frame, .frame,
.@"anyframe", .@"anyframe",
.@"opaque", .@"opaque",
=> false, => .{ .result = .{ .failure = T } },
.@"enum" => |@"enum"| @"enum".is_exhaustive, .@"enum" => |@"enum"| .{ .result = if (@"enum".is_exhaustive) .success else .{ .failure = T } },
.pointer => |pointer| switch (pointer.size) { .pointer => |pointer| switch (pointer.size) {
.one => canSerializeTypeInner(pointer.child, visited, parent_is_optional), .one => canSerializeTypeInner(pointer.child, visited),
.slice => canSerializeTypeInner(pointer.child, visited, false), .slice => .{ .result = canSerializeTypeInner(pointer.child, visited).result },
.many, .c => false, .many, .c => .{ .result = .{ .failure = T } },
}, },
.optional => |optional| if (parent_is_optional) .optional => |optional| {
false const inner = canSerializeTypeInner(optional.child, visited);
else return switch (inner.result) {
canSerializeTypeInner(optional.child, visited, true), .success => .{
.is_optional = true,
.result = if (inner.is_optional) .{ .failure = T } else .success,
},
.failure => inner,
};
},
.array => |array| canSerializeTypeInner(array.child, visited, false), .array => |array| .{ .result = canSerializeTypeInner(array.child, visited).result },
.vector => |vector| canSerializeTypeInner(vector.child, visited, false), .vector => |vector| .{ .result = canSerializeTypeInner(vector.child, visited).result },
.@"struct" => |@"struct"| { .@"struct" => |@"struct"| {
for (visited) |V| if (T == V) return true; for (visited) |V| if (T == V) return .{ .result = .success };
const new_visited = visited ++ .{T}; const new_visited = visited ++ .{T};
for (@"struct".fields) |field| { for (@"struct".fields) |field| {
if (!canSerializeTypeInner(field.type, new_visited, false)) return false; const res = canSerializeTypeInner(field.type, new_visited);
if (res.result != .success)
return res;
} }
return true; return .{ .result = .success };
}, },
.@"union" => |@"union"| { .@"union" => |@"union"| {
for (visited) |V| if (T == V) return true; for (visited) |V| if (T == V) return .{ .result = .success };
const new_visited = visited ++ .{T}; const new_visited = visited ++ .{T};
if (@"union".tag_type == null) return false; if (@"union".tag_type == null) return .{ .result = .{ .failure = T } };
for (@"union".fields) |field| { for (@"union".fields) |field| {
if (field.type != void and !canSerializeTypeInner(field.type, new_visited, false)) { if (field.type == void)
return false; continue;
const res = canSerializeTypeInner(field.type, new_visited);
if (res.result != .success)
return res;
} }
} return .{ .result = .success };
return true;
}, },
}; };
} }
fn canSerializeTypeResultEqual(a: CanSerializeTypeResult, b: CanSerializeTypeResult) bool {
const TagType = @typeInfo(CanSerializeTypeResult).@"union".tag_type.?;
if (@as(TagType, a) != @as(TagType, b))
return false;
return switch (a) {
.success => true,
.failure => |a_failure| a_failure == b.failure,
};
}
fn expectCanSerializeTypeResult(comptime T: type, result: CanSerializeTypeResult) !void {
try std.testing.expect(canSerializeTypeResultEqual(canSerializeType(T), result));
}
test canSerializeType { test canSerializeType {
try std.testing.expect(!comptime canSerializeType(void)); try expectCanSerializeTypeResult(void, .{ .failure = void });
try std.testing.expect(!comptime canSerializeType(struct { f: [*]u8 })); try expectCanSerializeTypeResult(?void, .{ .failure = void });
try std.testing.expect(!comptime canSerializeType(struct { error{foo} })); try expectCanSerializeTypeResult(struct { f: [*]u8 }, .{ .failure = [*]u8 });
try std.testing.expect(!comptime canSerializeType(union(enum) { a: void, f: [*c]u8 })); try expectCanSerializeTypeResult(error{foo}, .{ .failure = error{foo} });
try std.testing.expect(!comptime canSerializeType(@Vector(0, [*c]u8))); try expectCanSerializeTypeResult(union(enum) { a: void, f: [*c]u8 }, .{ .failure = [*c]u8 });
try std.testing.expect(!comptime canSerializeType(*?[*c]u8)); try expectCanSerializeTypeResult(@Vector(0, [*c]u8), .{ .failure = [*c]u8 });
try std.testing.expect(!comptime canSerializeType(enum(u8) { _ })); try expectCanSerializeTypeResult(?*u8, .success);
try std.testing.expect(!comptime canSerializeType(union { foo: void })); try expectCanSerializeTypeResult(*?[*c]u8, .{ .failure = [*c]u8 });
try std.testing.expect(comptime canSerializeType(union(enum) { foo: void })); const NonExhaustiveEnum = enum(u8) { _ };
try std.testing.expect(comptime canSerializeType(comptime_float)); try expectCanSerializeTypeResult(NonExhaustiveEnum, .{ .failure = NonExhaustiveEnum });
try std.testing.expect(comptime canSerializeType(comptime_int)); const NoTagUnion = union { foo: void };
try std.testing.expect(!comptime canSerializeType(struct { comptime foo: ??u8 = null })); try expectCanSerializeTypeResult(NoTagUnion, .{ .failure = NoTagUnion });
try std.testing.expect(comptime canSerializeType(@TypeOf(.foo))); try expectCanSerializeTypeResult(union(enum) { foo: void }, .success);
try std.testing.expect(comptime canSerializeType(?u8)); try expectCanSerializeTypeResult(comptime_float, .success);
try std.testing.expect(comptime canSerializeType(*?*u8)); try expectCanSerializeTypeResult(comptime_int, .success);
try std.testing.expect(comptime canSerializeType(?struct { try expectCanSerializeTypeResult(struct { comptime foo: ??u8 = null }, .{ .failure = ??u8 });
try expectCanSerializeTypeResult(@TypeOf(.foo), .success);
try expectCanSerializeTypeResult(?u8, .success);
try expectCanSerializeTypeResult(*?*u8, .success);
try expectCanSerializeTypeResult(?struct {
foo: ?struct { foo: ?struct {
?union(enum) { ?union(enum) {
a: ?@Vector(0, ?*u8), a: ?@Vector(0, ?*u8),
@ -908,21 +956,21 @@ test canSerializeType {
f: ?[]?u8, f: ?[]?u8,
}, },
}, },
})); }, .success);
try std.testing.expect(!comptime canSerializeType(??u8)); try expectCanSerializeTypeResult(??u8, .{ .failure = ??u8 });
try std.testing.expect(!comptime canSerializeType(?*?u8)); try expectCanSerializeTypeResult(?*?u8, .{ .failure = ?*?u8 });
try std.testing.expect(!comptime canSerializeType(*?*?*u8)); try expectCanSerializeTypeResult(*?*?u8, .{ .failure = ?*?u8 });
try std.testing.expect(comptime canSerializeType(struct { x: comptime_int = 2 })); try expectCanSerializeTypeResult(struct { x: comptime_int = 2 }, .success);
try std.testing.expect(comptime canSerializeType(struct { x: comptime_float = 2 })); try expectCanSerializeTypeResult(struct { x: comptime_float = 2 }, .success);
try std.testing.expect(comptime canSerializeType(struct { comptime_int })); try expectCanSerializeTypeResult(struct { comptime_int }, .success);
try std.testing.expect(comptime canSerializeType(struct { comptime x: @TypeOf(.foo) = .foo })); try expectCanSerializeTypeResult(struct { comptime x: @TypeOf(.foo) = .foo }, .success);
const Recursive = struct { foo: ?*@This() }; const Recursive = struct { foo: ?*@This() };
try std.testing.expect(comptime canSerializeType(Recursive)); try expectCanSerializeTypeResult(Recursive, .success);
// Make sure we validate nested optional before we early out due to already having seen // Make sure we validate nested optional before we early out due to already having seen
// a type recursion! // a type recursion!
try std.testing.expect(!comptime canSerializeType(struct { try expectCanSerializeTypeResult(struct {
add_to_visited: ?u8, add_to_visited: ?u8,
retrieve_from_visited: ??u8, retrieve_from_visited: ??u8,
})); }, .{ .failure = ??u8 });
} }

View file

@ -379,7 +379,7 @@ pub fn fromZoirNodeAlloc(
diag: ?*Diagnostics, diag: ?*Diagnostics,
options: Options, options: Options,
) error{ OutOfMemory, ParseZon }!T { ) error{ OutOfMemory, ParseZon }!T {
comptime assert(canParseType(T)); comptime assertCanParseType(T);
if (diag) |s| { if (diag) |s| {
s.assertEmpty(); s.assertEmpty();
@ -1185,24 +1185,43 @@ fn intFromFloatExact(T: type, value: anytype) ?T {
return @intFromFloat(value); return @intFromFloat(value);
} }
fn canParseType(T: type) bool { fn assertCanParseType(T: type) void {
comptime return canParseTypeInner(T, &.{}, false); comptime switch (canParseType(T)) {
.success => {},
.failure => |FailedType| {
@compileError(std.fmt.comptimePrint("cannot parse type {}", .{FailedType}));
},
};
} }
const CanParseTypeResult = union(enum) {
success,
failure: type,
};
fn canParseType(T: type) CanParseTypeResult {
comptime return canParseTypeInner(T, &.{}).result;
}
const CanParseTypeInnerResult = struct {
// Keep track of optionals to reject nested optionals, which cannot be parsed.
is_optional: bool = false,
result: CanParseTypeResult,
};
fn canParseTypeInner( fn canParseTypeInner(
T: type, T: type,
/// Visited structs and unions, to avoid infinite recursion. /// Visited structs and unions, to avoid infinite recursion.
/// Tracking more types is unnecessary, and a little complex due to optional nesting. /// Tracking more types is unnecessary, and a little complex due to optional nesting.
visited: []const type, visited: []const type,
parent_is_optional: bool, ) CanParseTypeInnerResult {
) bool {
return switch (@typeInfo(T)) { return switch (@typeInfo(T)) {
.bool, .bool,
.int, .int,
.float, .float,
.null, .null,
.@"enum", .@"enum",
=> true, => .{ .result = .success },
.noreturn, .noreturn,
.void, .void,
@ -1217,62 +1236,89 @@ fn canParseTypeInner(
.comptime_int, .comptime_int,
.comptime_float, .comptime_float,
.enum_literal, .enum_literal,
=> false, => .{ .result = .{ .failure = T } },
.pointer => |pointer| switch (pointer.size) { .pointer => |pointer| switch (pointer.size) {
.one => canParseTypeInner(pointer.child, visited, parent_is_optional), .one => canParseTypeInner(pointer.child, visited),
.slice => canParseTypeInner(pointer.child, visited, false), .slice => .{ .result = canParseTypeInner(pointer.child, visited).result },
.many, .c => false, .many, .c => .{ .result = .{ .failure = T } },
}, },
.optional => |optional| if (parent_is_optional) .optional => |optional| {
false const inner = canParseTypeInner(optional.child, visited);
else return switch (inner.result) {
canParseTypeInner(optional.child, visited, true), .success => .{
.is_optional = true,
.result = if (inner.is_optional) .{ .failure = T } else .success,
},
.failure => inner,
};
},
.array => |array| canParseTypeInner(array.child, visited, false), .array => |array| .{ .result = canParseTypeInner(array.child, visited).result },
.vector => |vector| canParseTypeInner(vector.child, visited, false), .vector => |vector| .{ .result = canParseTypeInner(vector.child, visited).result },
.@"struct" => |@"struct"| { .@"struct" => |@"struct"| {
for (visited) |V| if (T == V) return true; for (visited) |V| if (T == V) return .{ .result = .success };
const new_visited = visited ++ .{T}; const new_visited = visited ++ .{T};
for (@"struct".fields) |field| { for (@"struct".fields) |field| {
if (!field.is_comptime and !canParseTypeInner(field.type, new_visited, false)) { if (field.is_comptime)
return false; continue;
const res = canParseTypeInner(field.type, new_visited);
if (res.result == .failure)
return res;
} }
} return .{ .result = .success };
return true;
}, },
.@"union" => |@"union"| { .@"union" => |@"union"| {
for (visited) |V| if (T == V) return true; for (visited) |V| if (T == V) return true;
const new_visited = visited ++ .{T}; const new_visited = visited ++ .{T};
for (@"union".fields) |field| { for (@"union".fields) |field| {
if (field.type != void and !canParseTypeInner(field.type, new_visited, false)) { if (field.type == void)
return false; continue;
const res = canParseTypeInner(field.type, new_visited);
if (res.result == .failure)
return res;
} }
} return .{ .result = .success };
return true;
}, },
}; };
} }
fn canParseTypeResultEqual(a: CanParseTypeResult, b: CanParseTypeResult) bool {
const TagType = @typeInfo(CanParseTypeResult).@"union".tag_type.?;
if (@as(TagType, a) != @as(TagType, b))
return false;
return switch (a) {
.success => true,
.failure => |a_failure| a_failure == b.failure,
};
}
fn expectCanParseTypeResult(comptime T: type, result: CanParseTypeResult) !void {
try std.testing.expect(canParseTypeResultEqual(canParseType(T), result));
}
test "std.zon parse canParseType" { test "std.zon parse canParseType" {
try std.testing.expect(!comptime canParseType(void)); try expectCanParseTypeResult(void, .{ .failure = void });
try std.testing.expect(!comptime canParseType(struct { f: [*]u8 })); try expectCanParseTypeResult(?void, .{ .failure = void });
try std.testing.expect(!comptime canParseType(struct { error{foo} })); try expectCanParseTypeResult(struct { f: [*]u8 }, .{ .failure = [*]u8 });
try std.testing.expect(!comptime canParseType(union(enum) { a: void, b: [*c]u8 })); try expectCanParseTypeResult(struct { error{foo} }, .{ .failure = error{foo} });
try std.testing.expect(!comptime canParseType(@Vector(0, [*c]u8))); try expectCanParseTypeResult(union(enum) { a: void, b: [*c]u8 }, .{ .failure = [*c]u8 });
try std.testing.expect(!comptime canParseType(*?[*c]u8)); try expectCanParseTypeResult(@Vector(0, [*c]u8), .{ .failure = [*c]u8 });
try std.testing.expect(comptime canParseType(enum(u8) { _ })); try expectCanParseTypeResult(*?[*c]u8, .{ .failure = [*c]u8 });
try std.testing.expect(comptime canParseType(union { foo: void })); try expectCanParseTypeResult(*?[*c]u8, .{ .failure = [*c]u8 });
try std.testing.expect(comptime canParseType(union(enum) { foo: void })); try expectCanParseTypeResult(enum(u8) { _ }, .success);
try std.testing.expect(!comptime canParseType(comptime_float)); try expectCanParseTypeResult(union { foo: void }, .success);
try std.testing.expect(!comptime canParseType(comptime_int)); try expectCanParseTypeResult(union(enum) { foo: void }, .success);
try std.testing.expect(comptime canParseType(struct { comptime foo: ??u8 = null })); try expectCanParseTypeResult(comptime_float, .{ .failure = comptime_float });
try std.testing.expect(!comptime canParseType(@TypeOf(.foo))); try expectCanParseTypeResult(comptime_int, .{ .failure = comptime_int });
try std.testing.expect(comptime canParseType(?u8)); try expectCanParseTypeResult(struct { comptime foo: ??u8 = null }, .success);
try std.testing.expect(comptime canParseType(*?*u8)); try expectCanParseTypeResult(@TypeOf(.foo), .{ .failure = @TypeOf(.foo) });
try std.testing.expect(comptime canParseType(?struct { try expectCanParseTypeResult(?u8, .success);
try expectCanParseTypeResult(*?*u8, .success);
try expectCanParseTypeResult(?struct {
foo: ?struct { foo: ?struct {
?union(enum) { ?union(enum) {
a: ?@Vector(0, ?*u8), a: ?@Vector(0, ?*u8),
@ -1281,23 +1327,23 @@ test "std.zon parse canParseType" {
f: ?[]?u8, f: ?[]?u8,
}, },
}, },
})); }, .success);
try std.testing.expect(!comptime canParseType(??u8)); try expectCanParseTypeResult(??u8, .{ .failure = ??u8 });
try std.testing.expect(!comptime canParseType(?*?u8)); try expectCanParseTypeResult(?*?u8, .{ .failure = ?*?u8 });
try std.testing.expect(!comptime canParseType(*?*?*u8)); try expectCanParseTypeResult(*?*?*u8, .{ .failure = ?*?*u8 });
try std.testing.expect(!comptime canParseType(struct { x: comptime_int = 2 })); try expectCanParseTypeResult(struct { x: comptime_int = 2 }, .{ .failure = comptime_int });
try std.testing.expect(!comptime canParseType(struct { x: comptime_float = 2 })); try expectCanParseTypeResult(struct { x: comptime_float = 2 }, .{ .failure = comptime_float });
try std.testing.expect(comptime canParseType(struct { comptime x: @TypeOf(.foo) = .foo })); try expectCanParseTypeResult(struct { comptime x: @TypeOf(.foo) = .foo }, .success);
try std.testing.expect(!comptime canParseType(struct { comptime_int })); try expectCanParseTypeResult(struct { comptime_int }, .{ .failure = comptime_int });
const Recursive = struct { foo: ?*@This() }; const Recursive = struct { foo: ?*@This() };
try std.testing.expect(comptime canParseType(Recursive)); try expectCanParseTypeResult(Recursive, .success);
// Make sure we validate nested optional before we early out due to already having seen // Make sure we validate nested optional before we early out due to already having seen
// a type recursion! // a type recursion!
try std.testing.expect(!comptime canParseType(struct { try expectCanParseTypeResult(struct {
add_to_visited: ?u8, add_to_visited: ?u8,
retrieve_from_visited: ??u8, retrieve_from_visited: ??u8,
})); }, .{ .failure = ??u8 });
} }
test "std.zon requiresAllocator" { test "std.zon requiresAllocator" {