aboutsummaryrefslogtreecommitdiff
path: root/lib/std/zon/parse.zig
diff options
context:
space:
mode:
authorMatthew Lugg <mlugg@mlugg.co.uk>2025-02-03 16:38:35 +0000
committerGitHub <noreply@github.com>2025-02-03 16:38:35 +0000
commit317722b37b201e393aed60045f4bf9649103e63e (patch)
treed0485fccbcfd0d2951ac373bbd5d483cc8539ca2 /lib/std/zon/parse.zig
parente61acd8eb563d3c233202ef3a1a63df384d09943 (diff)
parentdc5c8278474f998360bc48e3dd0fe9a2929b4374 (diff)
downloadzig-317722b37b201e393aed60045f4bf9649103e63e.tar.gz
zig-317722b37b201e393aed60045f4bf9649103e63e.zip
Merge pull request #20271 from MasonRemaley/zon
ZON
Diffstat (limited to 'lib/std/zon/parse.zig')
-rw-r--r--lib/std/zon/parse.zig3449
1 files changed, 3449 insertions, 0 deletions
diff --git a/lib/std/zon/parse.zig b/lib/std/zon/parse.zig
new file mode 100644
index 0000000000..daf83d0bbd
--- /dev/null
+++ b/lib/std/zon/parse.zig
@@ -0,0 +1,3449 @@
+//! The simplest way to parse ZON at runtime is to use `fromSlice`. If you need to parse ZON at
+//! compile time, you may use `@import`.
+//!
+//! Parsing from individual Zoir nodes is also available:
+//! * `fromZoir`
+//! * `fromZoirNode`
+//!
+//! For lower level control, it is possible to operate on `std.zig.Zoir` directly.
+
+const std = @import("std");
+const builtin = @import("builtin");
+const Allocator = std.mem.Allocator;
+const Ast = std.zig.Ast;
+const Zoir = std.zig.Zoir;
+const ZonGen = std.zig.ZonGen;
+const TokenIndex = std.zig.Ast.TokenIndex;
+const Base = std.zig.number_literal.Base;
+const StrLitErr = std.zig.string_literal.Error;
+const NumberLiteralError = std.zig.number_literal.Error;
+const assert = std.debug.assert;
+const ArrayListUnmanaged = std.ArrayListUnmanaged;
+
+/// Rename when adding or removing support for a type.
+const valid_types = {};
+
+/// Configuration for the runtime parser.
+pub const Options = struct {
+ /// If true, unknown fields do not error.
+ ignore_unknown_fields: bool = false,
+ /// If true, the parser cleans up partially parsed values on error. This requires some extra
+ /// bookkeeping, so you may want to turn it off if you don't need this feature (e.g. because
+ /// you're using arena allocation.)
+ free_on_error: bool = true,
+};
+
+pub const Error = union(enum) {
+ zoir: Zoir.CompileError,
+ type_check: Error.TypeCheckFailure,
+
+ pub const Note = union(enum) {
+ zoir: Zoir.CompileError.Note,
+ type_check: TypeCheckFailure.Note,
+
+ pub const Iterator = struct {
+ index: usize = 0,
+ err: Error,
+ status: *const Status,
+
+ pub fn next(self: *@This()) ?Note {
+ switch (self.err) {
+ .zoir => |err| {
+ if (self.index >= err.note_count) return null;
+ const zoir = self.status.zoir.?;
+ const note = err.getNotes(zoir)[self.index];
+ self.index += 1;
+ return .{ .zoir = note };
+ },
+ .type_check => |err| {
+ if (self.index >= err.getNoteCount()) return null;
+ const note = err.getNote(self.index);
+ self.index += 1;
+ return .{ .type_check = note };
+ },
+ }
+ }
+ };
+
+ fn formatMessage(
+ self: []const u8,
+ comptime f: []const u8,
+ options: std.fmt.FormatOptions,
+ writer: anytype,
+ ) !void {
+ _ = f;
+ _ = options;
+
+ // Just writes the string for now, but we're keeping this behind a formatter so we have
+ // the option to extend it in the future to print more advanced messages (like `Error`
+ // does) without breaking the API.
+ try writer.writeAll(self);
+ }
+
+ pub fn fmtMessage(self: Note, status: *const Status) std.fmt.Formatter(Note.formatMessage) {
+ return .{ .data = switch (self) {
+ .zoir => |note| note.msg.get(status.zoir.?),
+ .type_check => |note| note.msg,
+ } };
+ }
+
+ pub fn getLocation(self: Note, status: *const Status) Ast.Location {
+ const ast = status.ast.?;
+ switch (self) {
+ .zoir => |note| return zoirErrorLocation(ast, note.token, note.node_or_offset),
+ .type_check => |note| return ast.tokenLocation(note.offset, note.token),
+ }
+ }
+ };
+
+ pub const Iterator = struct {
+ index: usize = 0,
+ status: *const Status,
+
+ pub fn next(self: *@This()) ?Error {
+ const zoir = self.status.zoir orelse return null;
+
+ if (self.index < zoir.compile_errors.len) {
+ const result: Error = .{ .zoir = zoir.compile_errors[self.index] };
+ self.index += 1;
+ return result;
+ }
+
+ if (self.status.type_check) |err| {
+ if (self.index == zoir.compile_errors.len) {
+ const result: Error = .{ .type_check = err };
+ self.index += 1;
+ return result;
+ }
+ }
+
+ return null;
+ }
+ };
+
+ const TypeCheckFailure = struct {
+ const Note = struct {
+ token: Ast.TokenIndex,
+ offset: u32,
+ msg: []const u8,
+ owned: bool,
+
+ fn deinit(self: @This(), gpa: Allocator) void {
+ if (self.owned) gpa.free(self.msg);
+ }
+ };
+
+ message: []const u8,
+ owned: bool,
+ token: Ast.TokenIndex,
+ offset: u32,
+ note: ?@This().Note,
+
+ fn deinit(self: @This(), gpa: Allocator) void {
+ if (self.note) |note| note.deinit(gpa);
+ if (self.owned) gpa.free(self.message);
+ }
+
+ fn getNoteCount(self: @This()) usize {
+ return @intFromBool(self.note != null);
+ }
+
+ fn getNote(self: @This(), index: usize) @This().Note {
+ assert(index == 0);
+ return self.note.?;
+ }
+ };
+
+ const FormatMessage = struct {
+ err: Error,
+ status: *const Status,
+ };
+
+ fn formatMessage(
+ self: FormatMessage,
+ comptime f: []const u8,
+ options: std.fmt.FormatOptions,
+ writer: anytype,
+ ) !void {
+ _ = f;
+ _ = options;
+ switch (self.err) {
+ .zoir => |err| try writer.writeAll(err.msg.get(self.status.zoir.?)),
+ .type_check => |tc| try writer.writeAll(tc.message),
+ }
+ }
+
+ pub fn fmtMessage(self: @This(), status: *const Status) std.fmt.Formatter(formatMessage) {
+ return .{ .data = .{
+ .err = self,
+ .status = status,
+ } };
+ }
+
+ pub fn getLocation(self: @This(), status: *const Status) Ast.Location {
+ const ast = status.ast.?;
+ return switch (self) {
+ .zoir => |err| return zoirErrorLocation(
+ status.ast.?,
+ err.token,
+ err.node_or_offset,
+ ),
+ .type_check => |err| return ast.tokenLocation(err.offset, err.token),
+ };
+ }
+
+ pub fn iterateNotes(self: @This(), status: *const Status) Note.Iterator {
+ return .{ .err = self, .status = status };
+ }
+
+ fn zoirErrorLocation(ast: Ast, maybe_token: Ast.TokenIndex, node_or_offset: u32) Ast.Location {
+ if (maybe_token == Zoir.CompileError.invalid_token) {
+ const main_tokens = ast.nodes.items(.main_token);
+ const ast_node = node_or_offset;
+ const token = main_tokens[ast_node];
+ return ast.tokenLocation(0, token);
+ } else {
+ var location = ast.tokenLocation(0, maybe_token);
+ location.column += node_or_offset;
+ return location;
+ }
+ }
+};
+
+/// Information about the success or failure of a parse.
+pub const Status = struct {
+ ast: ?Ast = null,
+ zoir: ?Zoir = null,
+ type_check: ?Error.TypeCheckFailure = null,
+
+ fn assertEmpty(self: Status) void {
+ assert(self.ast == null);
+ assert(self.zoir == null);
+ assert(self.type_check == null);
+ }
+
+ pub fn deinit(self: *Status, gpa: Allocator) void {
+ if (self.ast) |*ast| ast.deinit(gpa);
+ if (self.zoir) |*zoir| zoir.deinit(gpa);
+ if (self.type_check) |tc| tc.deinit(gpa);
+ self.* = undefined;
+ }
+
+ pub fn iterateErrors(self: *const Status) Error.Iterator {
+ return .{ .status = self };
+ }
+
+ pub fn format(
+ self: *const @This(),
+ comptime fmt: []const u8,
+ options: std.fmt.FormatOptions,
+ writer: anytype,
+ ) !void {
+ _ = fmt;
+ _ = options;
+ var errors = self.iterateErrors();
+ while (errors.next()) |err| {
+ const loc = err.getLocation(self);
+ const msg = err.fmtMessage(self);
+ try writer.print("{}:{}: error: {}\n", .{ loc.line + 1, loc.column + 1, msg });
+
+ var notes = err.iterateNotes(self);
+ while (notes.next()) |note| {
+ const note_loc = note.getLocation(self);
+ const note_msg = note.fmtMessage(self);
+ try writer.print("{}:{}: note: {s}\n", .{
+ note_loc.line + 1,
+ note_loc.column + 1,
+ note_msg,
+ });
+ }
+ }
+ }
+};
+
+/// Parses the given slice as ZON.
+///
+/// Returns `error.OutOfMemory` on allocation failure, or `error.ParseZon` error if the ZON is
+/// invalid or can not be deserialized into type `T`.
+///
+/// When the parser returns `error.ParseZon`, it will also store a human readable explanation in
+/// `status` if non null. If status is not null, it must be initialized to `.{}`.
+pub fn fromSlice(
+ /// The type to deserialize into. May not be or contain any of the following types:
+ /// * Any comptime-only type, except in a comptime field
+ /// * `type`
+ /// * `void`, except as a union payload
+ /// * `noreturn`
+ /// * An error set/error union
+ /// * A many-pointer or C-pointer
+ /// * An opaque type, including `anyopaque`
+ /// * An async frame type, including `anyframe` and `anyframe->T`
+ /// * A function
+ ///
+ /// All other types are valid. Unsupported types will fail at compile time.
+ T: type,
+ gpa: Allocator,
+ source: [:0]const u8,
+ status: ?*Status,
+ options: Options,
+) error{ OutOfMemory, ParseZon }!T {
+ if (status) |s| s.assertEmpty();
+
+ var ast = try std.zig.Ast.parse(gpa, source, .zon);
+ defer if (status == null) ast.deinit(gpa);
+ if (status) |s| s.ast = ast;
+
+ // If there's no status, Zoir exists for the lifetime of this function. If there is a status,
+ // ownership is transferred to status.
+ var zoir = try ZonGen.generate(gpa, ast, .{ .parse_str_lits = false });
+ defer if (status == null) zoir.deinit(gpa);
+
+ if (status) |s| s.* = .{};
+ return fromZoir(T, gpa, ast, zoir, status, options);
+}
+
+/// Like `fromSlice`, but operates on `Zoir` instead of ZON source.
+pub fn fromZoir(
+ T: type,
+ gpa: Allocator,
+ ast: Ast,
+ zoir: Zoir,
+ status: ?*Status,
+ options: Options,
+) error{ OutOfMemory, ParseZon }!T {
+ return fromZoirNode(T, gpa, ast, zoir, .root, status, options);
+}
+
+/// Like `fromZoir`, but the parse starts on `node` instead of root.
+pub fn fromZoirNode(
+ T: type,
+ gpa: Allocator,
+ ast: Ast,
+ zoir: Zoir,
+ node: Zoir.Node.Index,
+ status: ?*Status,
+ options: Options,
+) error{ OutOfMemory, ParseZon }!T {
+ comptime assert(canParseType(T));
+
+ if (status) |s| {
+ s.assertEmpty();
+ s.ast = ast;
+ s.zoir = zoir;
+ }
+
+ if (zoir.hasCompileErrors()) {
+ return error.ParseZon;
+ }
+
+ var parser: Parser = .{
+ .gpa = gpa,
+ .ast = ast,
+ .zoir = zoir,
+ .options = options,
+ .status = status,
+ };
+
+ return parser.parseExpr(T, node);
+}
+
+/// Frees ZON values.
+///
+/// Provided for convenience, you may also free these values on your own using the same allocator
+/// passed into the parser.
+///
+/// Asserts at comptime that sufficient information is available via the type system to free this
+/// value. Untagged unions, for example, will fail this assert.
+pub fn free(gpa: Allocator, value: anytype) void {
+ const Value = @TypeOf(value);
+
+ _ = valid_types;
+ switch (@typeInfo(Value)) {
+ .bool, .int, .float, .@"enum" => {},
+ .pointer => |pointer| {
+ switch (pointer.size) {
+ .one => {
+ free(gpa, value.*);
+ gpa.destroy(value);
+ },
+ .slice => {
+ for (value) |item| {
+ free(gpa, item);
+ }
+ gpa.free(value);
+ },
+ .many, .c => comptime unreachable,
+ }
+ },
+ .array => for (value) |item| {
+ free(gpa, item);
+ },
+ .@"struct" => |@"struct"| inline for (@"struct".fields) |field| {
+ free(gpa, @field(value, field.name));
+ },
+ .@"union" => |@"union"| if (@"union".tag_type == null) {
+ if (comptime requiresAllocator(Value)) unreachable;
+ } else switch (value) {
+ inline else => |_, tag| {
+ free(gpa, @field(value, @tagName(tag)));
+ },
+ },
+ .optional => if (value) |some| {
+ free(gpa, some);
+ },
+ .vector => |vector| for (0..vector.len) |i| free(gpa, value[i]),
+ .void => {},
+ else => comptime unreachable,
+ }
+}
+
+fn requiresAllocator(T: type) bool {
+ _ = valid_types;
+ return switch (@typeInfo(T)) {
+ .pointer => true,
+ .array => |array| return array.len > 0 and requiresAllocator(array.child),
+ .@"struct" => |@"struct"| inline for (@"struct".fields) |field| {
+ if (requiresAllocator(field.type)) {
+ break true;
+ }
+ } else false,
+ .@"union" => |@"union"| inline for (@"union".fields) |field| {
+ if (requiresAllocator(field.type)) {
+ break true;
+ }
+ } else false,
+ .optional => |optional| requiresAllocator(optional.child),
+ .vector => |vector| return vector.len > 0 and requiresAllocator(vector.child),
+ else => false,
+ };
+}
+
+const Parser = struct {
+ gpa: Allocator,
+ ast: Ast,
+ zoir: Zoir,
+ status: ?*Status,
+ options: Options,
+
+ fn parseExpr(self: *@This(), T: type, node: Zoir.Node.Index) error{ ParseZon, OutOfMemory }!T {
+ return self.parseExprInner(T, node) catch |err| switch (err) {
+ error.WrongType => return self.failExpectedType(T, node),
+ else => |e| return e,
+ };
+ }
+
+ fn parseExprInner(
+ self: *@This(),
+ T: type,
+ node: Zoir.Node.Index,
+ ) error{ ParseZon, OutOfMemory, WrongType }!T {
+ switch (@typeInfo(T)) {
+ .optional => |optional| if (node.get(self.zoir) == .null) {
+ return null;
+ } else {
+ return try self.parseExprInner(optional.child, node);
+ },
+ .bool => return self.parseBool(node),
+ .int => return self.parseInt(T, node),
+ .float => return self.parseFloat(T, node),
+ .@"enum" => return self.parseEnumLiteral(T, node),
+ .pointer => |pointer| switch (pointer.size) {
+ .one => {
+ const result = try self.gpa.create(pointer.child);
+ errdefer self.gpa.destroy(result);
+ result.* = try self.parseExprInner(pointer.child, node);
+ return result;
+ },
+ .slice => return self.parseSlicePointer(T, node),
+ else => comptime unreachable,
+ },
+ .array => return self.parseArray(T, node),
+ .@"struct" => |@"struct"| if (@"struct".is_tuple)
+ return self.parseTuple(T, node)
+ else
+ return self.parseStruct(T, node),
+ .@"union" => return self.parseUnion(T, node),
+ .vector => return self.parseVector(T, node),
+
+ else => comptime unreachable,
+ }
+ }
+
+ /// Prints a message of the form `expected T` where T is first converted to a ZON type. For
+ /// example, `**?**u8` becomes `?u8`, and types that involve user specified type names are just
+ /// referred to by the type of container.
+ fn failExpectedType(
+ self: @This(),
+ T: type,
+ node: Zoir.Node.Index,
+ ) error{ ParseZon, OutOfMemory } {
+ @branchHint(.cold);
+ return self.failExpectedTypeInner(T, false, node);
+ }
+
+ fn failExpectedTypeInner(
+ self: @This(),
+ T: type,
+ opt: bool,
+ node: Zoir.Node.Index,
+ ) error{ ParseZon, OutOfMemory } {
+ _ = valid_types;
+ switch (@typeInfo(T)) {
+ .@"struct" => |@"struct"| if (@"struct".is_tuple) {
+ if (opt) {
+ return self.failNode(node, "expected optional tuple");
+ } else {
+ return self.failNode(node, "expected tuple");
+ }
+ } else {
+ if (opt) {
+ return self.failNode(node, "expected optional struct");
+ } else {
+ return self.failNode(node, "expected struct");
+ }
+ },
+ .@"union" => if (opt) {
+ return self.failNode(node, "expected optional union");
+ } else {
+ return self.failNode(node, "expected union");
+ },
+ .array => if (opt) {
+ return self.failNode(node, "expected optional array");
+ } else {
+ return self.failNode(node, "expected array");
+ },
+ .pointer => |pointer| switch (pointer.size) {
+ .one => return self.failExpectedTypeInner(pointer.child, opt, node),
+ .slice => {
+ if (pointer.child == u8 and
+ pointer.is_const and
+ (pointer.sentinel() == null or pointer.sentinel() == 0) and
+ pointer.alignment == 1)
+ {
+ if (opt) {
+ return self.failNode(node, "expected optional string");
+ } else {
+ return self.failNode(node, "expected string");
+ }
+ } else {
+ if (opt) {
+ return self.failNode(node, "expected optional array");
+ } else {
+ return self.failNode(node, "expected array");
+ }
+ }
+ },
+ else => comptime unreachable,
+ },
+ .vector, .bool, .int, .float => if (opt) {
+ return self.failNodeFmt(node, "expected type '{s}'", .{@typeName(?T)});
+ } else {
+ return self.failNodeFmt(node, "expected type '{s}'", .{@typeName(T)});
+ },
+ .@"enum" => if (opt) {
+ return self.failNode(node, "expected optional enum literal");
+ } else {
+ return self.failNode(node, "expected enum literal");
+ },
+ .optional => |optional| {
+ return self.failExpectedTypeInner(optional.child, true, node);
+ },
+ else => comptime unreachable,
+ }
+ }
+
+ fn parseBool(self: @This(), node: Zoir.Node.Index) !bool {
+ switch (node.get(self.zoir)) {
+ .true => return true,
+ .false => return false,
+ else => return error.WrongType,
+ }
+ }
+
+ fn parseInt(self: @This(), T: type, node: Zoir.Node.Index) !T {
+ switch (node.get(self.zoir)) {
+ .int_literal => |int| switch (int) {
+ .small => |val| return std.math.cast(T, val) orelse
+ self.failCannotRepresent(T, node),
+ .big => |val| return val.toInt(T) catch
+ self.failCannotRepresent(T, node),
+ },
+ .float_literal => |val| return intFromFloatExact(T, val) orelse
+ self.failCannotRepresent(T, node),
+
+ .char_literal => |val| return std.math.cast(T, val) orelse
+ self.failCannotRepresent(T, node),
+ else => return error.WrongType,
+ }
+ }
+
+ fn parseFloat(self: @This(), T: type, node: Zoir.Node.Index) !T {
+ switch (node.get(self.zoir)) {
+ .int_literal => |int| switch (int) {
+ .small => |val| return @floatFromInt(val),
+ .big => |val| return val.toFloat(T),
+ },
+ .float_literal => |val| return @floatCast(val),
+ .pos_inf => return std.math.inf(T),
+ .neg_inf => return -std.math.inf(T),
+ .nan => return std.math.nan(T),
+ .char_literal => |val| return @floatFromInt(val),
+ else => return error.WrongType,
+ }
+ }
+
+ fn parseEnumLiteral(self: @This(), T: type, node: Zoir.Node.Index) !T {
+ switch (node.get(self.zoir)) {
+ .enum_literal => |field_name| {
+ // Create a comptime string map for the enum fields
+ const enum_fields = @typeInfo(T).@"enum".fields;
+ comptime var kvs_list: [enum_fields.len]struct { []const u8, T } = undefined;
+ inline for (enum_fields, 0..) |field, i| {
+ kvs_list[i] = .{ field.name, @enumFromInt(field.value) };
+ }
+ const enum_tags = std.StaticStringMap(T).initComptime(kvs_list);
+
+ // Get the tag if it exists
+ const field_name_str = field_name.get(self.zoir);
+ return enum_tags.get(field_name_str) orelse
+ self.failUnexpected(T, "enum literal", node, null, field_name_str);
+ },
+ else => return error.WrongType,
+ }
+ }
+
+ fn parseSlicePointer(self: *@This(), T: type, node: Zoir.Node.Index) !T {
+ switch (node.get(self.zoir)) {
+ .string_literal => return self.parseString(T, node),
+ .array_literal => |nodes| return self.parseSlice(T, nodes),
+ .empty_literal => return self.parseSlice(T, .{ .start = node, .len = 0 }),
+ else => return error.WrongType,
+ }
+ }
+
+ fn parseString(self: *@This(), T: type, node: Zoir.Node.Index) !T {
+ const ast_node = node.getAstNode(self.zoir);
+ const pointer = @typeInfo(T).pointer;
+ var size_hint = ZonGen.strLitSizeHint(self.ast, ast_node);
+ if (pointer.sentinel() != null) size_hint += 1;
+
+ var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(self.gpa, size_hint);
+ defer buf.deinit(self.gpa);
+ switch (try ZonGen.parseStrLit(self.ast, ast_node, buf.writer(self.gpa))) {
+ .success => {},
+ .failure => |err| {
+ const token = self.ast.nodes.items(.main_token)[ast_node];
+ const raw_string = self.ast.tokenSlice(token);
+ return self.failTokenFmt(token, @intCast(err.offset()), "{s}", .{err.fmt(raw_string)});
+ },
+ }
+
+ if (pointer.child != u8 or
+ pointer.size != .slice or
+ !pointer.is_const or
+ (pointer.sentinel() != null and pointer.sentinel() != 0) or
+ pointer.alignment != 1)
+ {
+ return error.WrongType;
+ }
+
+ if (pointer.sentinel() != null) {
+ return buf.toOwnedSliceSentinel(self.gpa, 0);
+ } else {
+ return buf.toOwnedSlice(self.gpa);
+ }
+ }
+
+ fn parseSlice(self: *@This(), T: type, nodes: Zoir.Node.Index.Range) !T {
+ const pointer = @typeInfo(T).pointer;
+
+ // Make sure we're working with a slice
+ switch (pointer.size) {
+ .slice => {},
+ .one, .many, .c => comptime unreachable,
+ }
+
+ // Allocate the slice
+ const slice = try self.gpa.allocWithOptions(
+ pointer.child,
+ nodes.len,
+ pointer.alignment,
+ pointer.sentinel(),
+ );
+ errdefer self.gpa.free(slice);
+
+ // Parse the elements and return the slice
+ for (slice, 0..) |*elem, i| {
+ errdefer if (self.options.free_on_error) {
+ for (slice[0..i]) |item| {
+ free(self.gpa, item);
+ }
+ };
+ elem.* = try self.parseExpr(pointer.child, nodes.at(@intCast(i)));
+ }
+
+ return slice;
+ }
+
+ fn parseArray(self: *@This(), T: type, node: Zoir.Node.Index) !T {
+ const nodes: Zoir.Node.Index.Range = switch (node.get(self.zoir)) {
+ .array_literal => |nodes| nodes,
+ .empty_literal => .{ .start = node, .len = 0 },
+ else => return error.WrongType,
+ };
+
+ const array_info = @typeInfo(T).array;
+
+ // Check if the size matches
+ if (nodes.len < array_info.len) {
+ return self.failNodeFmt(
+ node,
+ "expected {} array elements; found {}",
+ .{ array_info.len, nodes.len },
+ );
+ } else if (nodes.len > array_info.len) {
+ return self.failNodeFmt(
+ nodes.at(array_info.len),
+ "index {} outside of array of length {}",
+ .{ array_info.len, array_info.len },
+ );
+ }
+
+ // Parse the elements and return the array
+ var result: T = undefined;
+ for (&result, 0..) |*elem, i| {
+ // If we fail to parse this field, free all fields before it
+ errdefer if (self.options.free_on_error) {
+ for (result[0..i]) |item| {
+ free(self.gpa, item);
+ }
+ };
+
+ elem.* = try self.parseExpr(array_info.child, nodes.at(@intCast(i)));
+ }
+ return result;
+ }
+
+ fn parseStruct(self: *@This(), T: type, node: Zoir.Node.Index) !T {
+ const repr = node.get(self.zoir);
+ const fields: @FieldType(Zoir.Node, "struct_literal") = switch (repr) {
+ .struct_literal => |nodes| nodes,
+ .empty_literal => .{ .names = &.{}, .vals = .{ .start = node, .len = 0 } },
+ else => return error.WrongType,
+ };
+
+ const field_infos = @typeInfo(T).@"struct".fields;
+
+ // Build a map from field name to index.
+ // The special value `comptime_field` indicates that this is actually a comptime field.
+ const comptime_field = std.math.maxInt(usize);
+ const field_indices: std.StaticStringMap(usize) = comptime b: {
+ var kvs_list: [field_infos.len]struct { []const u8, usize } = undefined;
+ for (&kvs_list, field_infos, 0..) |*kv, field, i| {
+ kv.* = .{ field.name, if (field.is_comptime) comptime_field else i };
+ }
+ break :b .initComptime(kvs_list);
+ };
+
+ // Parse the struct
+ var result: T = undefined;
+ var field_found: [field_infos.len]bool = @splat(false);
+
+ // If we fail partway through, free all already initialized fields
+ var initialized: usize = 0;
+ errdefer if (self.options.free_on_error and field_infos.len > 0) {
+ for (fields.names[0..initialized]) |name_runtime| {
+ switch (field_indices.get(name_runtime.get(self.zoir)) orelse continue) {
+ inline 0...(field_infos.len - 1) => |name_index| {
+ const name = field_infos[name_index].name;
+ free(self.gpa, @field(result, name));
+ },
+ else => unreachable, // Can't be out of bounds
+ }
+ }
+ };
+
+ // Fill in the fields we found
+ for (0..fields.names.len) |i| {
+ const name = fields.names[i].get(self.zoir);
+ const field_index = field_indices.get(name) orelse {
+ if (self.options.ignore_unknown_fields) continue;
+ return self.failUnexpected(T, "field", node, i, name);
+ };
+ if (field_index == comptime_field) {
+ return self.failComptimeField(node, i);
+ }
+
+ // Mark the field as found. Assert that the found array is not zero length to satisfy
+ // the type checker (it can't be since we made it into an iteration of this loop.)
+ if (field_found.len == 0) unreachable;
+ field_found[field_index] = true;
+
+ switch (field_index) {
+ inline 0...(field_infos.len - 1) => |j| {
+ if (field_infos[j].is_comptime) unreachable;
+
+ @field(result, field_infos[j].name) = try self.parseExpr(
+ field_infos[j].type,
+ fields.vals.at(@intCast(i)),
+ );
+ },
+ else => unreachable, // Can't be out of bounds
+ }
+
+ initialized += 1;
+ }
+
+ // Fill in any missing default fields
+ inline for (field_found, 0..) |found, i| {
+ if (!found) {
+ const field_info = field_infos[i];
+ if (field_info.default_value_ptr) |default| {
+ const typed: *const field_info.type = @ptrCast(@alignCast(default));
+ @field(result, field_info.name) = typed.*;
+ } else {
+ return self.failNodeFmt(
+ node,
+ "missing required field {s}",
+ .{field_infos[i].name},
+ );
+ }
+ }
+ }
+
+ return result;
+ }
+
+ fn parseTuple(self: *@This(), T: type, node: Zoir.Node.Index) !T {
+ const nodes: Zoir.Node.Index.Range = switch (node.get(self.zoir)) {
+ .array_literal => |nodes| nodes,
+ .empty_literal => .{ .start = node, .len = 0 },
+ else => return error.WrongType,
+ };
+
+ var result: T = undefined;
+ const field_infos = @typeInfo(T).@"struct".fields;
+
+ if (nodes.len > field_infos.len) {
+ return self.failNodeFmt(
+ nodes.at(field_infos.len),
+ "index {} outside of tuple length {}",
+ .{ field_infos.len, field_infos.len },
+ );
+ }
+
+ inline for (0..field_infos.len) |i| {
+ // Check if we're out of bounds
+ if (i >= nodes.len) {
+ if (field_infos[i].default_value_ptr) |default| {
+ const typed: *const field_infos[i].type = @ptrCast(@alignCast(default));
+ @field(result, field_infos[i].name) = typed.*;
+ } else {
+ return self.failNodeFmt(node, "missing tuple field with index {}", .{i});
+ }
+ } else {
+ // If we fail to parse this field, free all fields before it
+ errdefer if (self.options.free_on_error) {
+ inline for (0..i) |j| {
+ if (j >= i) break;
+ free(self.gpa, result[j]);
+ }
+ };
+
+ if (field_infos[i].is_comptime) {
+ return self.failComptimeField(node, i);
+ } else {
+ result[i] = try self.parseExpr(field_infos[i].type, nodes.at(i));
+ }
+ }
+ }
+
+ return result;
+ }
+
+ fn parseUnion(self: *@This(), T: type, node: Zoir.Node.Index) !T {
+ const @"union" = @typeInfo(T).@"union";
+ const field_infos = @"union".fields;
+
+ if (field_infos.len == 0) comptime unreachable;
+
+ // Gather info on the fields
+ const field_indices = b: {
+ comptime var kvs_list: [field_infos.len]struct { []const u8, usize } = undefined;
+ inline for (field_infos, 0..) |field, i| {
+ kvs_list[i] = .{ field.name, i };
+ }
+ break :b std.StaticStringMap(usize).initComptime(kvs_list);
+ };
+
+ // Parse the union
+ switch (node.get(self.zoir)) {
+ .enum_literal => |field_name| {
+ // The union must be tagged for an enum literal to coerce to it
+ if (@"union".tag_type == null) {
+ return error.WrongType;
+ }
+
+ // Get the index of the named field. We don't use `parseEnum` here as
+ // the order of the enum and the order of the union might not match!
+ const field_index = b: {
+ const field_name_str = field_name.get(self.zoir);
+ break :b field_indices.get(field_name_str) orelse
+ return self.failUnexpected(T, "field", node, null, field_name_str);
+ };
+
+ // Initialize the union from the given field.
+ switch (field_index) {
+ inline 0...field_infos.len - 1 => |i| {
+ // Fail if the field is not void
+ if (field_infos[i].type != void)
+ return self.failNode(node, "expected union");
+
+ // Instantiate the union
+ return @unionInit(T, field_infos[i].name, {});
+ },
+ else => unreachable, // Can't be out of bounds
+ }
+ },
+ .struct_literal => |struct_fields| {
+ if (struct_fields.names.len != 1) {
+ return error.WrongType;
+ }
+
+ // Fill in the field we found
+ const field_name = struct_fields.names[0];
+ const field_name_str = field_name.get(self.zoir);
+ const field_val = struct_fields.vals.at(0);
+ const field_index = field_indices.get(field_name_str) orelse
+ return self.failUnexpected(T, "field", node, 0, field_name_str);
+
+ switch (field_index) {
+ inline 0...field_infos.len - 1 => |i| {
+ if (field_infos[i].type == void) {
+ return self.failNode(field_val, "expected type 'void'");
+ } else {
+ const value = try self.parseExpr(field_infos[i].type, field_val);
+ return @unionInit(T, field_infos[i].name, value);
+ }
+ },
+ else => unreachable, // Can't be out of bounds
+ }
+ },
+ else => return error.WrongType,
+ }
+ }
+
+ fn parseVector(
+ self: *@This(),
+ T: type,
+ node: Zoir.Node.Index,
+ ) !T {
+ const vector_info = @typeInfo(T).vector;
+
+ const nodes: Zoir.Node.Index.Range = switch (node.get(self.zoir)) {
+ .array_literal => |nodes| nodes,
+ .empty_literal => .{ .start = node, .len = 0 },
+ else => return error.WrongType,
+ };
+
+ var result: T = undefined;
+
+ if (nodes.len != vector_info.len) {
+ return self.failNodeFmt(
+ node,
+ "expected {} vector elements; found {}",
+ .{ vector_info.len, nodes.len },
+ );
+ }
+
+ for (0..vector_info.len) |i| {
+ errdefer for (0..i) |j| free(self.gpa, result[j]);
+ result[i] = try self.parseExpr(vector_info.child, nodes.at(@intCast(i)));
+ }
+
+ return result;
+ }
+
+ fn failTokenFmt(
+ self: @This(),
+ token: Ast.TokenIndex,
+ offset: u32,
+ comptime fmt: []const u8,
+ args: anytype,
+ ) error{ OutOfMemory, ParseZon } {
+ @branchHint(.cold);
+ return self.failTokenFmtNote(token, offset, fmt, args, null);
+ }
+
+ fn failTokenFmtNote(
+ self: @This(),
+ token: Ast.TokenIndex,
+ offset: u32,
+ comptime fmt: []const u8,
+ args: anytype,
+ note: ?Error.TypeCheckFailure.Note,
+ ) error{ OutOfMemory, ParseZon } {
+ @branchHint(.cold);
+ comptime assert(args.len > 0);
+ if (self.status) |s| s.type_check = .{
+ .token = token,
+ .offset = offset,
+ .message = std.fmt.allocPrint(self.gpa, fmt, args) catch |err| {
+ if (note) |n| n.deinit(self.gpa);
+ return err;
+ },
+ .owned = true,
+ .note = note,
+ };
+ return error.ParseZon;
+ }
+
+ fn failNodeFmt(
+ self: @This(),
+ node: Zoir.Node.Index,
+ comptime fmt: []const u8,
+ args: anytype,
+ ) error{ OutOfMemory, ParseZon } {
+ @branchHint(.cold);
+ const main_tokens = self.ast.nodes.items(.main_token);
+ const token = main_tokens[node.getAstNode(self.zoir)];
+ return self.failTokenFmt(token, 0, fmt, args);
+ }
+
+ fn failToken(
+ self: @This(),
+ failure: Error.TypeCheckFailure,
+ ) error{ParseZon} {
+ @branchHint(.cold);
+ if (self.status) |s| s.type_check = failure;
+ return error.ParseZon;
+ }
+
+ fn failNode(
+ self: @This(),
+ node: Zoir.Node.Index,
+ message: []const u8,
+ ) error{ParseZon} {
+ @branchHint(.cold);
+ const main_tokens = self.ast.nodes.items(.main_token);
+ const token = main_tokens[node.getAstNode(self.zoir)];
+ return self.failToken(.{
+ .token = token,
+ .offset = 0,
+ .message = message,
+ .owned = false,
+ .note = null,
+ });
+ }
+
+ fn failCannotRepresent(
+ self: @This(),
+ T: type,
+ node: Zoir.Node.Index,
+ ) error{ OutOfMemory, ParseZon } {
+ @branchHint(.cold);
+ return self.failNodeFmt(node, "type '{s}' cannot represent value", .{@typeName(T)});
+ }
+
+ fn failUnexpected(
+ self: @This(),
+ T: type,
+ item_kind: []const u8,
+ node: Zoir.Node.Index,
+ field: ?usize,
+ name: []const u8,
+ ) error{ OutOfMemory, ParseZon } {
+ @branchHint(.cold);
+ const token = if (field) |f| b: {
+ var buf: [2]Ast.Node.Index = undefined;
+ const struct_init = self.ast.fullStructInit(&buf, node.getAstNode(self.zoir)).?;
+ const field_node = struct_init.ast.fields[f];
+ break :b self.ast.firstToken(field_node) - 2;
+ } else b: {
+ const main_tokens = self.ast.nodes.items(.main_token);
+ break :b main_tokens[node.getAstNode(self.zoir)];
+ };
+ switch (@typeInfo(T)) {
+ inline .@"struct", .@"union", .@"enum" => |info| {
+ const note: Error.TypeCheckFailure.Note = if (info.fields.len == 0) b: {
+ break :b .{
+ .token = token,
+ .offset = 0,
+ .msg = "none expected",
+ .owned = false,
+ };
+ } else b: {
+ const msg = "supported: ";
+ var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(self.gpa, 64);
+ defer buf.deinit(self.gpa);
+ const writer = buf.writer(self.gpa);
+ try writer.writeAll(msg);
+ inline for (info.fields, 0..) |field_info, i| {
+ if (i != 0) try writer.writeAll(", ");
+ try writer.print("'{p_}'", .{std.zig.fmtId(field_info.name)});
+ }
+ break :b .{
+ .token = token,
+ .offset = 0,
+ .msg = try buf.toOwnedSlice(self.gpa),
+ .owned = true,
+ };
+ };
+ return self.failTokenFmtNote(
+ token,
+ 0,
+ "unexpected {s} '{s}'",
+ .{ item_kind, name },
+ note,
+ );
+ },
+ else => comptime unreachable,
+ }
+ }
+
+ // Technically we could do this if we were willing to do a deep equal to verify
+ // the value matched, but doing so doesn't seem to support any real use cases
+ // so isn't worth the complexity at the moment.
+ fn failComptimeField(
+ self: @This(),
+ node: Zoir.Node.Index,
+ field: usize,
+ ) error{ OutOfMemory, ParseZon } {
+ @branchHint(.cold);
+ const ast_node = node.getAstNode(self.zoir);
+ var buf: [2]Ast.Node.Index = undefined;
+ const token = if (self.ast.fullStructInit(&buf, ast_node)) |struct_init| b: {
+ const field_node = struct_init.ast.fields[field];
+ break :b self.ast.firstToken(field_node);
+ } else b: {
+ const array_init = self.ast.fullArrayInit(&buf, ast_node).?;
+ const value_node = array_init.ast.elements[field];
+ break :b self.ast.firstToken(value_node);
+ };
+ return self.failToken(.{
+ .token = token,
+ .offset = 0,
+ .message = "cannot initialize comptime field",
+ .owned = false,
+ .note = null,
+ });
+ }
+};
+
+fn intFromFloatExact(T: type, value: anytype) ?T {
+ if (value > std.math.maxInt(T) or value < std.math.minInt(T)) {
+ return null;
+ }
+
+ if (std.math.isNan(value) or std.math.trunc(value) != value) {
+ return null;
+ }
+
+ return @intFromFloat(value);
+}
+
+fn canParseType(T: type) bool {
+ comptime return canParseTypeInner(T, &.{}, false);
+}
+
+fn canParseTypeInner(
+ 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,
+ .null,
+ .@"enum",
+ => true,
+
+ .noreturn,
+ .void,
+ .type,
+ .undefined,
+ .error_union,
+ .error_set,
+ .@"fn",
+ .frame,
+ .@"anyframe",
+ .@"opaque",
+ .comptime_int,
+ .comptime_float,
+ .enum_literal,
+ => false,
+
+ .pointer => |pointer| switch (pointer.size) {
+ .one => canParseTypeInner(pointer.child, visited, parent_is_optional),
+ .slice => canParseTypeInner(pointer.child, visited, false),
+ .many, .c => false,
+ },
+
+ .optional => |optional| if (parent_is_optional)
+ false
+ else
+ canParseTypeInner(optional.child, visited, true),
+
+ .array => |array| canParseTypeInner(array.child, visited, false),
+ .vector => |vector| canParseTypeInner(vector.child, visited, false),
+
+ .@"struct" => |@"struct"| {
+ for (visited) |V| if (T == V) return true;
+ const new_visited = visited ++ .{T};
+ for (@"struct".fields) |field| {
+ if (!field.is_comptime and !canParseTypeInner(field.type, new_visited, false)) {
+ return false;
+ }
+ }
+ return true;
+ },
+ .@"union" => |@"union"| {
+ for (visited) |V| if (T == V) return true;
+ const new_visited = visited ++ .{T};
+ for (@"union".fields) |field| {
+ if (field.type != void and !canParseTypeInner(field.type, new_visited, false)) {
+ return false;
+ }
+ }
+ return true;
+ },
+ };
+}
+
+test "std.zon parse canParseType" {
+ try std.testing.expect(!comptime canParseType(void));
+ try std.testing.expect(!comptime canParseType(struct { f: [*]u8 }));
+ try std.testing.expect(!comptime canParseType(struct { error{foo} }));
+ try std.testing.expect(!comptime canParseType(union(enum) { a: void, b: [*c]u8 }));
+ try std.testing.expect(!comptime canParseType(@Vector(0, [*c]u8)));
+ try std.testing.expect(!comptime canParseType(*?[*c]u8));
+ try std.testing.expect(comptime canParseType(enum(u8) { _ }));
+ try std.testing.expect(comptime canParseType(union { foo: void }));
+ try std.testing.expect(comptime canParseType(union(enum) { foo: void }));
+ try std.testing.expect(!comptime canParseType(comptime_float));
+ try std.testing.expect(!comptime canParseType(comptime_int));
+ try std.testing.expect(comptime canParseType(struct { comptime foo: ??u8 = null }));
+ try std.testing.expect(!comptime canParseType(@TypeOf(.foo)));
+ try std.testing.expect(comptime canParseType(?u8));
+ try std.testing.expect(comptime canParseType(*?*u8));
+ try std.testing.expect(comptime canParseType(?struct {
+ foo: ?struct {
+ ?union(enum) {
+ a: ?@Vector(0, ?*u8),
+ },
+ ?struct {
+ f: ?[]?u8,
+ },
+ },
+ }));
+ try std.testing.expect(!comptime canParseType(??u8));
+ try std.testing.expect(!comptime canParseType(?*?u8));
+ try std.testing.expect(!comptime canParseType(*?*?*u8));
+ try std.testing.expect(!comptime canParseType(struct { x: comptime_int = 2 }));
+ try std.testing.expect(!comptime canParseType(struct { x: comptime_float = 2 }));
+ try std.testing.expect(comptime canParseType(struct { comptime x: @TypeOf(.foo) = .foo }));
+ try std.testing.expect(!comptime canParseType(struct { comptime_int }));
+ const Recursive = struct { foo: ?*@This() };
+ try std.testing.expect(comptime canParseType(Recursive));
+
+ // Make sure we validate nested optional before we early out due to already having seen
+ // a type recursion!
+ try std.testing.expect(!comptime canParseType(struct {
+ add_to_visited: ?u8,
+ retrieve_from_visited: ??u8,
+ }));
+}
+
+test "std.zon requiresAllocator" {
+ try std.testing.expect(!requiresAllocator(u8));
+ try std.testing.expect(!requiresAllocator(f32));
+ try std.testing.expect(!requiresAllocator(enum { foo }));
+ try std.testing.expect(!requiresAllocator(struct { f32 }));
+ try std.testing.expect(!requiresAllocator(struct { x: f32 }));
+ try std.testing.expect(!requiresAllocator([0][]const u8));
+ try std.testing.expect(!requiresAllocator([2]u8));
+ try std.testing.expect(!requiresAllocator(union { x: f32, y: f32 }));
+ try std.testing.expect(!requiresAllocator(union(enum) { x: f32, y: f32 }));
+ try std.testing.expect(!requiresAllocator(?f32));
+ try std.testing.expect(!requiresAllocator(void));
+ try std.testing.expect(!requiresAllocator(@TypeOf(null)));
+ try std.testing.expect(!requiresAllocator(@Vector(3, u8)));
+ try std.testing.expect(!requiresAllocator(@Vector(0, *const u8)));
+
+ try std.testing.expect(requiresAllocator([]u8));
+ try std.testing.expect(requiresAllocator(*struct { u8, u8 }));
+ try std.testing.expect(requiresAllocator([1][]const u8));
+ try std.testing.expect(requiresAllocator(struct { x: i32, y: []u8 }));
+ try std.testing.expect(requiresAllocator(union { x: i32, y: []u8 }));
+ try std.testing.expect(requiresAllocator(union(enum) { x: i32, y: []u8 }));
+ try std.testing.expect(requiresAllocator(?[]u8));
+ try std.testing.expect(requiresAllocator(@Vector(3, *const u8)));
+}
+
+test "std.zon ast errors" {
+ const gpa = std.testing.allocator;
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(struct {}, gpa, ".{.x = 1 .y = 2}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:13: error: expected ',' after initializer\n", "{}", .{status});
+}
+
+test "std.zon comments" {
+ const gpa = std.testing.allocator;
+
+ try std.testing.expectEqual(@as(u8, 10), fromSlice(u8, gpa,
+ \\// comment
+ \\10 // comment
+ \\// comment
+ , null, .{}));
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(u8, gpa,
+ \\//! comment
+ \\10 // comment
+ \\// comment
+ , &status, .{}));
+ try std.testing.expectFmt(
+ "1:1: error: expected expression, found 'a document comment'\n",
+ "{}",
+ .{status},
+ );
+ }
+}
+
+test "std.zon failure/oom formatting" {
+ const gpa = std.testing.allocator;
+ var failing_allocator = std.testing.FailingAllocator.init(gpa, .{
+ .fail_index = 0,
+ .resize_fail_index = 0,
+ });
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.OutOfMemory, fromSlice(
+ []const u8,
+ failing_allocator.allocator(),
+ "\"foo\"",
+ &status,
+ .{},
+ ));
+ try std.testing.expectFmt("", "{}", .{status});
+}
+
+test "std.zon fromSlice syntax error" {
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(u8, std.testing.allocator, ".{", null, .{}),
+ );
+}
+
+test "std.zon optional" {
+ const gpa = std.testing.allocator;
+
+ // Basic usage
+ {
+ const none = try fromSlice(?u32, gpa, "null", null, .{});
+ try std.testing.expect(none == null);
+ const some = try fromSlice(?u32, gpa, "1", null, .{});
+ try std.testing.expect(some.? == 1);
+ }
+
+ // Deep free
+ {
+ const none = try fromSlice(?[]const u8, gpa, "null", null, .{});
+ try std.testing.expect(none == null);
+ const some = try fromSlice(?[]const u8, gpa, "\"foo\"", null, .{});
+ defer free(gpa, some);
+ try std.testing.expectEqualStrings("foo", some.?);
+ }
+}
+
+test "std.zon unions" {
+ const gpa = std.testing.allocator;
+
+ // Unions
+ {
+ const Tagged = union(enum) { x: f32, @"y y": bool, z, @"z z" };
+ const Untagged = union { x: f32, @"y y": bool, z: void, @"z z": void };
+
+ const tagged_x = try fromSlice(Tagged, gpa, ".{.x = 1.5}", null, .{});
+ try std.testing.expectEqual(Tagged{ .x = 1.5 }, tagged_x);
+ const tagged_y = try fromSlice(Tagged, gpa, ".{.@\"y y\" = true}", null, .{});
+ try std.testing.expectEqual(Tagged{ .@"y y" = true }, tagged_y);
+ const tagged_z_shorthand = try fromSlice(Tagged, gpa, ".z", null, .{});
+ try std.testing.expectEqual(@as(Tagged, .z), tagged_z_shorthand);
+ const tagged_zz_shorthand = try fromSlice(Tagged, gpa, ".@\"z z\"", null, .{});
+ try std.testing.expectEqual(@as(Tagged, .@"z z"), tagged_zz_shorthand);
+
+ const untagged_x = try fromSlice(Untagged, gpa, ".{.x = 1.5}", null, .{});
+ try std.testing.expect(untagged_x.x == 1.5);
+ const untagged_y = try fromSlice(Untagged, gpa, ".{.@\"y y\" = true}", null, .{});
+ try std.testing.expect(untagged_y.@"y y");
+ }
+
+ // Deep free
+ {
+ const Union = union(enum) { bar: []const u8, baz: bool };
+
+ const noalloc = try fromSlice(Union, gpa, ".{.baz = false}", null, .{});
+ try std.testing.expectEqual(Union{ .baz = false }, noalloc);
+
+ const alloc = try fromSlice(Union, gpa, ".{.bar = \"qux\"}", null, .{});
+ defer free(gpa, alloc);
+ try std.testing.expectEqualDeep(Union{ .bar = "qux" }, alloc);
+ }
+
+ // Unknown field
+ {
+ const Union = union { x: f32, y: f32 };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Union, gpa, ".{.z=2.5}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ \\1:4: error: unexpected field 'z'
+ \\1:4: note: supported: 'x', 'y'
+ \\
+ ,
+ "{}",
+ .{status},
+ );
+ }
+
+ // Explicit void field
+ {
+ const Union = union(enum) { x: void };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Union, gpa, ".{.x=1}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:6: error: expected type 'void'\n", "{}", .{status});
+ }
+
+ // Extra field
+ {
+ const Union = union { x: f32, y: bool };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Union, gpa, ".{.x = 1.5, .y = true}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:2: error: expected union\n", "{}", .{status});
+ }
+
+ // No fields
+ {
+ const Union = union { x: f32, y: bool };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Union, gpa, ".{}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:2: error: expected union\n", "{}", .{status});
+ }
+
+ // Enum literals cannot coerce into untagged unions
+ {
+ const Union = union { x: void };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(Union, gpa, ".x", &status, .{}));
+ try std.testing.expectFmt("1:2: error: expected union\n", "{}", .{status});
+ }
+
+ // Unknown field for enum literal coercion
+ {
+ const Union = union(enum) { x: void };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(Union, gpa, ".y", &status, .{}));
+ try std.testing.expectFmt(
+ \\1:2: error: unexpected field 'y'
+ \\1:2: note: supported: 'x'
+ \\
+ ,
+ "{}",
+ .{status},
+ );
+ }
+
+ // Non void field for enum literal coercion
+ {
+ const Union = union(enum) { x: f32 };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(Union, gpa, ".x", &status, .{}));
+ try std.testing.expectFmt("1:2: error: expected union\n", "{}", .{status});
+ }
+}
+
+test "std.zon structs" {
+ const gpa = std.testing.allocator;
+
+ // Structs (various sizes tested since they're parsed differently)
+ {
+ const Vec0 = struct {};
+ const Vec1 = struct { x: f32 };
+ const Vec2 = struct { x: f32, y: f32 };
+ const Vec3 = struct { x: f32, y: f32, z: f32 };
+
+ const zero = try fromSlice(Vec0, gpa, ".{}", null, .{});
+ try std.testing.expectEqual(Vec0{}, zero);
+
+ const one = try fromSlice(Vec1, gpa, ".{.x = 1.2}", null, .{});
+ try std.testing.expectEqual(Vec1{ .x = 1.2 }, one);
+
+ const two = try fromSlice(Vec2, gpa, ".{.x = 1.2, .y = 3.4}", null, .{});
+ try std.testing.expectEqual(Vec2{ .x = 1.2, .y = 3.4 }, two);
+
+ const three = try fromSlice(Vec3, gpa, ".{.x = 1.2, .y = 3.4, .z = 5.6}", null, .{});
+ try std.testing.expectEqual(Vec3{ .x = 1.2, .y = 3.4, .z = 5.6 }, three);
+ }
+
+ // Deep free (structs and arrays)
+ {
+ const Foo = struct { bar: []const u8, baz: []const []const u8 };
+
+ const parsed = try fromSlice(
+ Foo,
+ gpa,
+ ".{.bar = \"qux\", .baz = .{\"a\", \"b\"}}",
+ null,
+ .{},
+ );
+ defer free(gpa, parsed);
+ try std.testing.expectEqualDeep(Foo{ .bar = "qux", .baz = &.{ "a", "b" } }, parsed);
+ }
+
+ // Unknown field
+ {
+ const Vec2 = struct { x: f32, y: f32 };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Vec2, gpa, ".{.x=1.5, .z=2.5}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ \\1:12: error: unexpected field 'z'
+ \\1:12: note: supported: 'x', 'y'
+ \\
+ ,
+ "{}",
+ .{status},
+ );
+ }
+
+ // Duplicate field
+ {
+ const Vec2 = struct { x: f32, y: f32 };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Vec2, gpa, ".{.x=1.5, .x=2.5, .x=3.5}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ \\1:4: error: duplicate struct field name
+ \\1:12: note: duplicate name here
+ \\
+ , "{}", .{status});
+ }
+
+ // Ignore unknown fields
+ {
+ const Vec2 = struct { x: f32, y: f32 = 2.0 };
+ const parsed = try fromSlice(Vec2, gpa, ".{ .x = 1.0, .z = 3.0 }", null, .{
+ .ignore_unknown_fields = true,
+ });
+ try std.testing.expectEqual(Vec2{ .x = 1.0, .y = 2.0 }, parsed);
+ }
+
+ // Unknown field when struct has no fields (regression test)
+ {
+ const Vec2 = struct {};
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Vec2, gpa, ".{.x=1.5, .z=2.5}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ \\1:4: error: unexpected field 'x'
+ \\1:4: note: none expected
+ \\
+ , "{}", .{status});
+ }
+
+ // Missing field
+ {
+ const Vec2 = struct { x: f32, y: f32 };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Vec2, gpa, ".{.x=1.5}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:2: error: missing required field y\n", "{}", .{status});
+ }
+
+ // Default field
+ {
+ const Vec2 = struct { x: f32, y: f32 = 1.5 };
+ const parsed = try fromSlice(Vec2, gpa, ".{.x = 1.2}", null, .{});
+ try std.testing.expectEqual(Vec2{ .x = 1.2, .y = 1.5 }, parsed);
+ }
+
+ // Comptime field
+ {
+ const Vec2 = struct { x: f32, comptime y: f32 = 1.5 };
+ const parsed = try fromSlice(Vec2, gpa, ".{.x = 1.2}", null, .{});
+ try std.testing.expectEqual(Vec2{ .x = 1.2, .y = 1.5 }, parsed);
+ }
+
+ // Comptime field assignment
+ {
+ const Vec2 = struct { x: f32, comptime y: f32 = 1.5 };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ const parsed = fromSlice(Vec2, gpa, ".{.x = 1.2, .y = 1.5}", &status, .{});
+ try std.testing.expectError(error.ParseZon, parsed);
+ try std.testing.expectFmt(
+ \\1:18: error: cannot initialize comptime field
+ \\
+ , "{}", .{status});
+ }
+
+ // Enum field (regression test, we were previously getting the field name in an
+ // incorrect way that broke for enum values)
+ {
+ const Vec0 = struct { x: enum { x } };
+ const parsed = try fromSlice(Vec0, gpa, ".{ .x = .x }", null, .{});
+ try std.testing.expectEqual(Vec0{ .x = .x }, parsed);
+ }
+
+ // Enum field and struct field with @
+ {
+ const Vec0 = struct { @"x x": enum { @"x x" } };
+ const parsed = try fromSlice(Vec0, gpa, ".{ .@\"x x\" = .@\"x x\" }", null, .{});
+ try std.testing.expectEqual(Vec0{ .@"x x" = .@"x x" }, parsed);
+ }
+
+ // Type expressions are not allowed
+ {
+ // Structs
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ const parsed = fromSlice(struct {}, gpa, "Empty{}", &status, .{});
+ try std.testing.expectError(error.ParseZon, parsed);
+ try std.testing.expectFmt(
+ \\1:1: error: types are not available in ZON
+ \\1:1: note: replace the type with '.'
+ \\
+ , "{}", .{status});
+ }
+
+ // Arrays
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ const parsed = fromSlice([3]u8, gpa, "[3]u8{1, 2, 3}", &status, .{});
+ try std.testing.expectError(error.ParseZon, parsed);
+ try std.testing.expectFmt(
+ \\1:1: error: types are not available in ZON
+ \\1:1: note: replace the type with '.'
+ \\
+ , "{}", .{status});
+ }
+
+ // Slices
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ const parsed = fromSlice([]u8, gpa, "[]u8{1, 2, 3}", &status, .{});
+ try std.testing.expectError(error.ParseZon, parsed);
+ try std.testing.expectFmt(
+ \\1:1: error: types are not available in ZON
+ \\1:1: note: replace the type with '.'
+ \\
+ , "{}", .{status});
+ }
+
+ // Tuples
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ const parsed = fromSlice(
+ struct { u8, u8, u8 },
+ gpa,
+ "Tuple{1, 2, 3}",
+ &status,
+ .{},
+ );
+ try std.testing.expectError(error.ParseZon, parsed);
+ try std.testing.expectFmt(
+ \\1:1: error: types are not available in ZON
+ \\1:1: note: replace the type with '.'
+ \\
+ , "{}", .{status});
+ }
+
+ // Nested
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ const parsed = fromSlice(struct {}, gpa, ".{ .x = Tuple{1, 2, 3} }", &status, .{});
+ try std.testing.expectError(error.ParseZon, parsed);
+ try std.testing.expectFmt(
+ \\1:9: error: types are not available in ZON
+ \\1:9: note: replace the type with '.'
+ \\
+ , "{}", .{status});
+ }
+ }
+}
+
+test "std.zon tuples" {
+ const gpa = std.testing.allocator;
+
+ // Structs (various sizes tested since they're parsed differently)
+ {
+ const Tuple0 = struct {};
+ const Tuple1 = struct { f32 };
+ const Tuple2 = struct { f32, bool };
+ const Tuple3 = struct { f32, bool, u8 };
+
+ const zero = try fromSlice(Tuple0, gpa, ".{}", null, .{});
+ try std.testing.expectEqual(Tuple0{}, zero);
+
+ const one = try fromSlice(Tuple1, gpa, ".{1.2}", null, .{});
+ try std.testing.expectEqual(Tuple1{1.2}, one);
+
+ const two = try fromSlice(Tuple2, gpa, ".{1.2, true}", null, .{});
+ try std.testing.expectEqual(Tuple2{ 1.2, true }, two);
+
+ const three = try fromSlice(Tuple3, gpa, ".{1.2, false, 3}", null, .{});
+ try std.testing.expectEqual(Tuple3{ 1.2, false, 3 }, three);
+ }
+
+ // Deep free
+ {
+ const Tuple = struct { []const u8, []const u8 };
+ const parsed = try fromSlice(Tuple, gpa, ".{\"hello\", \"world\"}", null, .{});
+ defer free(gpa, parsed);
+ try std.testing.expectEqualDeep(Tuple{ "hello", "world" }, parsed);
+ }
+
+ // Extra field
+ {
+ const Tuple = struct { f32, bool };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Tuple, gpa, ".{0.5, true, 123}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:14: error: index 2 outside of tuple length 2\n", "{}", .{status});
+ }
+
+ // Extra field
+ {
+ const Tuple = struct { f32, bool };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Tuple, gpa, ".{0.5}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ "1:2: error: missing tuple field with index 1\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Tuple with unexpected field names
+ {
+ const Tuple = struct { f32 };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Tuple, gpa, ".{.foo = 10.0}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:2: error: expected tuple\n", "{}", .{status});
+ }
+
+ // Struct with missing field names
+ {
+ const Struct = struct { foo: f32 };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Struct, gpa, ".{10.0}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:2: error: expected struct\n", "{}", .{status});
+ }
+
+ // Comptime field
+ {
+ const Vec2 = struct { f32, comptime f32 = 1.5 };
+ const parsed = try fromSlice(Vec2, gpa, ".{ 1.2 }", null, .{});
+ try std.testing.expectEqual(Vec2{ 1.2, 1.5 }, parsed);
+ }
+
+ // Comptime field assignment
+ {
+ const Vec2 = struct { f32, comptime f32 = 1.5 };
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ const parsed = fromSlice(Vec2, gpa, ".{ 1.2, 1.5}", &status, .{});
+ try std.testing.expectError(error.ParseZon, parsed);
+ try std.testing.expectFmt(
+ \\1:9: error: cannot initialize comptime field
+ \\
+ , "{}", .{status});
+ }
+}
+
+// Test sizes 0 to 3 since small sizes get parsed differently
+test "std.zon arrays and slices" {
+ if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/20881
+
+ const gpa = std.testing.allocator;
+
+ // Literals
+ {
+ // Arrays
+ {
+ const zero = try fromSlice([0]u8, gpa, ".{}", null, .{});
+ try std.testing.expectEqualSlices(u8, &@as([0]u8, .{}), &zero);
+
+ const one = try fromSlice([1]u8, gpa, ".{'a'}", null, .{});
+ try std.testing.expectEqualSlices(u8, &@as([1]u8, .{'a'}), &one);
+
+ const two = try fromSlice([2]u8, gpa, ".{'a', 'b'}", null, .{});
+ try std.testing.expectEqualSlices(u8, &@as([2]u8, .{ 'a', 'b' }), &two);
+
+ const two_comma = try fromSlice([2]u8, gpa, ".{'a', 'b',}", null, .{});
+ try std.testing.expectEqualSlices(u8, &@as([2]u8, .{ 'a', 'b' }), &two_comma);
+
+ const three = try fromSlice([3]u8, gpa, ".{'a', 'b', 'c'}", null, .{});
+ try std.testing.expectEqualSlices(u8, &.{ 'a', 'b', 'c' }, &three);
+
+ const sentinel = try fromSlice([3:'z']u8, gpa, ".{'a', 'b', 'c'}", null, .{});
+ const expected_sentinel: [3:'z']u8 = .{ 'a', 'b', 'c' };
+ try std.testing.expectEqualSlices(u8, &expected_sentinel, &sentinel);
+ }
+
+ // Slice literals
+ {
+ const zero = try fromSlice([]const u8, gpa, ".{}", null, .{});
+ defer free(gpa, zero);
+ try std.testing.expectEqualSlices(u8, @as([]const u8, &.{}), zero);
+
+ const one = try fromSlice([]u8, gpa, ".{'a'}", null, .{});
+ defer free(gpa, one);
+ try std.testing.expectEqualSlices(u8, &.{'a'}, one);
+
+ const two = try fromSlice([]const u8, gpa, ".{'a', 'b'}", null, .{});
+ defer free(gpa, two);
+ try std.testing.expectEqualSlices(u8, &.{ 'a', 'b' }, two);
+
+ const two_comma = try fromSlice([]const u8, gpa, ".{'a', 'b',}", null, .{});
+ defer free(gpa, two_comma);
+ try std.testing.expectEqualSlices(u8, &.{ 'a', 'b' }, two_comma);
+
+ const three = try fromSlice([]u8, gpa, ".{'a', 'b', 'c'}", null, .{});
+ defer free(gpa, three);
+ try std.testing.expectEqualSlices(u8, &.{ 'a', 'b', 'c' }, three);
+
+ const sentinel = try fromSlice([:'z']const u8, gpa, ".{'a', 'b', 'c'}", null, .{});
+ defer free(gpa, sentinel);
+ const expected_sentinel: [:'z']const u8 = &.{ 'a', 'b', 'c' };
+ try std.testing.expectEqualSlices(u8, expected_sentinel, sentinel);
+ }
+ }
+
+ // Deep free
+ {
+ // Arrays
+ {
+ const parsed = try fromSlice([1][]const u8, gpa, ".{\"abc\"}", null, .{});
+ defer free(gpa, parsed);
+ const expected: [1][]const u8 = .{"abc"};
+ try std.testing.expectEqualDeep(expected, parsed);
+ }
+
+ // Slice literals
+ {
+ const parsed = try fromSlice([]const []const u8, gpa, ".{\"abc\"}", null, .{});
+ defer free(gpa, parsed);
+ const expected: []const []const u8 = &.{"abc"};
+ try std.testing.expectEqualDeep(expected, parsed);
+ }
+ }
+
+ // Sentinels and alignment
+ {
+ // Arrays
+ {
+ const sentinel = try fromSlice([1:2]u8, gpa, ".{1}", null, .{});
+ try std.testing.expectEqual(@as(usize, 1), sentinel.len);
+ try std.testing.expectEqual(@as(u8, 1), sentinel[0]);
+ try std.testing.expectEqual(@as(u8, 2), sentinel[1]);
+ }
+
+ // Slice literals
+ {
+ const sentinel = try fromSlice([:2]align(4) u8, gpa, ".{1}", null, .{});
+ defer free(gpa, sentinel);
+ try std.testing.expectEqual(@as(usize, 1), sentinel.len);
+ try std.testing.expectEqual(@as(u8, 1), sentinel[0]);
+ try std.testing.expectEqual(@as(u8, 2), sentinel[1]);
+ }
+ }
+
+ // Expect 0 find 3
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([0]u8, gpa, ".{'a', 'b', 'c'}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ "1:3: error: index 0 outside of array of length 0\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Expect 1 find 2
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([1]u8, gpa, ".{'a', 'b'}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ "1:8: error: index 1 outside of array of length 1\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Expect 2 find 1
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([2]u8, gpa, ".{'a'}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ "1:2: error: expected 2 array elements; found 1\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Expect 3 find 0
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([3]u8, gpa, ".{}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ "1:2: error: expected 3 array elements; found 0\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Wrong inner type
+ {
+ // Array
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([3]bool, gpa, ".{'a', 'b', 'c'}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:3: error: expected type 'bool'\n", "{}", .{status});
+ }
+
+ // Slice
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]bool, gpa, ".{'a', 'b', 'c'}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:3: error: expected type 'bool'\n", "{}", .{status});
+ }
+ }
+
+ // Complete wrong type
+ {
+ // Array
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([3]u8, gpa, "'a'", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+
+ // Slice
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]u8, gpa, "'a'", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+ }
+
+ // Address of is not allowed (indirection for slices in ZON is implicit)
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]u8, gpa, " &.{'a', 'b', 'c'}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ "1:3: error: pointers are not available in ZON\n",
+ "{}",
+ .{status},
+ );
+ }
+}
+
+test "std.zon string literal" {
+ const gpa = std.testing.allocator;
+
+ // Basic string literal
+ {
+ const parsed = try fromSlice([]const u8, gpa, "\"abc\"", null, .{});
+ defer free(gpa, parsed);
+ try std.testing.expectEqualStrings(@as([]const u8, "abc"), parsed);
+ }
+
+ // String literal with escape characters
+ {
+ const parsed = try fromSlice([]const u8, gpa, "\"ab\\nc\"", null, .{});
+ defer free(gpa, parsed);
+ try std.testing.expectEqualStrings(@as([]const u8, "ab\nc"), parsed);
+ }
+
+ // String literal with embedded null
+ {
+ const parsed = try fromSlice([]const u8, gpa, "\"ab\\x00c\"", null, .{});
+ defer free(gpa, parsed);
+ try std.testing.expectEqualStrings(@as([]const u8, "ab\x00c"), parsed);
+ }
+
+ // Passing string literal to a mutable slice
+ {
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]u8, gpa, "\"abcd\"", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]u8, gpa, "\\\\abcd", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+ }
+
+ // Passing string literal to a array
+ {
+ {
+ var ast = try std.zig.Ast.parse(gpa, "\"abcd\"", .zon);
+ defer ast.deinit(gpa);
+ var zoir = try ZonGen.generate(gpa, ast, .{ .parse_str_lits = false });
+ defer zoir.deinit(gpa);
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([4:0]u8, gpa, "\"abcd\"", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([4:0]u8, gpa, "\\\\abcd", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+ }
+
+ // Zero terminated slices
+ {
+ {
+ const parsed: [:0]const u8 = try fromSlice(
+ [:0]const u8,
+ gpa,
+ "\"abc\"",
+ null,
+ .{},
+ );
+ defer free(gpa, parsed);
+ try std.testing.expectEqualStrings("abc", parsed);
+ try std.testing.expectEqual(@as(u8, 0), parsed[3]);
+ }
+
+ {
+ const parsed: [:0]const u8 = try fromSlice(
+ [:0]const u8,
+ gpa,
+ "\\\\abc",
+ null,
+ .{},
+ );
+ defer free(gpa, parsed);
+ try std.testing.expectEqualStrings("abc", parsed);
+ try std.testing.expectEqual(@as(u8, 0), parsed[3]);
+ }
+ }
+
+ // Other value terminated slices
+ {
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([:1]const u8, gpa, "\"foo\"", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([:1]const u8, gpa, "\\\\foo", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+ }
+
+ // Expecting string literal, getting something else
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]const u8, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected string\n", "{}", .{status});
+ }
+
+ // Expecting string literal, getting an incompatible tuple
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]const u8, gpa, ".{false}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:3: error: expected type 'u8'\n", "{}", .{status});
+ }
+
+ // Invalid string literal
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]const i8, gpa, "\"\\a\"", &status, .{}),
+ );
+ try std.testing.expectFmt("1:3: error: invalid escape character: 'a'\n", "{}", .{status});
+ }
+
+ // Slice wrong child type
+ {
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]const i8, gpa, "\"a\"", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]const i8, gpa, "\\\\a", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+ }
+
+ // Bad alignment
+ {
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]align(2) const u8, gpa, "\"abc\"", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice([]align(2) const u8, gpa, "\\\\abc", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected array\n", "{}", .{status});
+ }
+ }
+
+ // Multi line strings
+ inline for (.{ []const u8, [:0]const u8 }) |String| {
+ // Nested
+ {
+ const S = struct {
+ message: String,
+ message2: String,
+ message3: String,
+ };
+ const parsed = try fromSlice(S, gpa,
+ \\.{
+ \\ .message =
+ \\ \\hello, world!
+ \\
+ \\ \\this is a multiline string!
+ \\ \\
+ \\ \\...
+ \\
+ \\ ,
+ \\ .message2 =
+ \\ \\this too...sort of.
+ \\ ,
+ \\ .message3 =
+ \\ \\
+ \\ \\and this.
+ \\}
+ , null, .{});
+ defer free(gpa, parsed);
+ try std.testing.expectEqualStrings(
+ "hello, world!\nthis is a multiline string!\n\n...",
+ parsed.message,
+ );
+ try std.testing.expectEqualStrings("this too...sort of.", parsed.message2);
+ try std.testing.expectEqualStrings("\nand this.", parsed.message3);
+ }
+ }
+}
+
+test "std.zon enum literals" {
+ const gpa = std.testing.allocator;
+
+ const Enum = enum {
+ foo,
+ bar,
+ baz,
+ @"ab\nc",
+ };
+
+ // Tags that exist
+ try std.testing.expectEqual(Enum.foo, try fromSlice(Enum, gpa, ".foo", null, .{}));
+ try std.testing.expectEqual(Enum.bar, try fromSlice(Enum, gpa, ".bar", null, .{}));
+ try std.testing.expectEqual(Enum.baz, try fromSlice(Enum, gpa, ".baz", null, .{}));
+ try std.testing.expectEqual(
+ Enum.@"ab\nc",
+ try fromSlice(Enum, gpa, ".@\"ab\\nc\"", null, .{}),
+ );
+
+ // Bad tag
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Enum, gpa, ".qux", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ \\1:2: error: unexpected enum literal 'qux'
+ \\1:2: note: supported: 'foo', 'bar', 'baz', '@"ab\nc"'
+ \\
+ ,
+ "{}",
+ .{status},
+ );
+ }
+
+ // Bad tag that's too long for parser
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Enum, gpa, ".@\"foobarbaz\"", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ \\1:2: error: unexpected enum literal 'foobarbaz'
+ \\1:2: note: supported: 'foo', 'bar', 'baz', '@"ab\nc"'
+ \\
+ ,
+ "{}",
+ .{status},
+ );
+ }
+
+ // Bad type
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Enum, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected enum literal\n", "{}", .{status});
+ }
+
+ // Test embedded nulls in an identifier
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(Enum, gpa, ".@\"\\x00\"", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ "1:2: error: identifier cannot contain null bytes\n",
+ "{}",
+ .{status},
+ );
+ }
+}
+
+test "std.zon parse bool" {
+ const gpa = std.testing.allocator;
+
+ // Correct floats
+ try std.testing.expectEqual(true, try fromSlice(bool, gpa, "true", null, .{}));
+ try std.testing.expectEqual(false, try fromSlice(bool, gpa, "false", null, .{}));
+
+ // Errors
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(bool, gpa, " foo", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ \\1:2: error: invalid expression
+ \\1:2: note: ZON allows identifiers 'true', 'false', 'null', 'inf', and 'nan'
+ \\1:2: note: precede identifier with '.' for an enum literal
+ \\
+ , "{}", .{status});
+ }
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(bool, gpa, "123", &status, .{}));
+ try std.testing.expectFmt("1:1: error: expected type 'bool'\n", "{}", .{status});
+ }
+}
+
+test "std.zon intFromFloatExact" {
+ // Valid conversions
+ try std.testing.expectEqual(@as(u8, 10), intFromFloatExact(u8, @as(f32, 10.0)).?);
+ try std.testing.expectEqual(@as(i8, -123), intFromFloatExact(i8, @as(f64, @as(f64, -123.0))).?);
+ try std.testing.expectEqual(@as(i16, 45), intFromFloatExact(i16, @as(f128, @as(f128, 45.0))).?);
+
+ // Out of range
+ try std.testing.expectEqual(@as(?u4, null), intFromFloatExact(u4, @as(f32, 16.0)));
+ try std.testing.expectEqual(@as(?i4, null), intFromFloatExact(i4, @as(f64, -17.0)));
+ try std.testing.expectEqual(@as(?u8, null), intFromFloatExact(u8, @as(f128, -2.0)));
+
+ // Not a whole number
+ try std.testing.expectEqual(@as(?u8, null), intFromFloatExact(u8, @as(f32, 0.5)));
+ try std.testing.expectEqual(@as(?i8, null), intFromFloatExact(i8, @as(f64, 0.01)));
+
+ // Infinity and NaN
+ try std.testing.expectEqual(@as(?u8, null), intFromFloatExact(u8, std.math.inf(f32)));
+ try std.testing.expectEqual(@as(?u8, null), intFromFloatExact(u8, -std.math.inf(f32)));
+ try std.testing.expectEqual(@as(?u8, null), intFromFloatExact(u8, std.math.nan(f32)));
+}
+
+test "std.zon parse int" {
+ const gpa = std.testing.allocator;
+
+ // Test various numbers and types
+ try std.testing.expectEqual(@as(u8, 10), try fromSlice(u8, gpa, "10", null, .{}));
+ try std.testing.expectEqual(@as(i16, 24), try fromSlice(i16, gpa, "24", null, .{}));
+ try std.testing.expectEqual(@as(i14, -4), try fromSlice(i14, gpa, "-4", null, .{}));
+ try std.testing.expectEqual(@as(i32, -123), try fromSlice(i32, gpa, "-123", null, .{}));
+
+ // Test limits
+ try std.testing.expectEqual(@as(i8, 127), try fromSlice(i8, gpa, "127", null, .{}));
+ try std.testing.expectEqual(@as(i8, -128), try fromSlice(i8, gpa, "-128", null, .{}));
+
+ // Test characters
+ try std.testing.expectEqual(@as(u8, 'a'), try fromSlice(u8, gpa, "'a'", null, .{}));
+ try std.testing.expectEqual(@as(u8, 'z'), try fromSlice(u8, gpa, "'z'", null, .{}));
+
+ // Test big integers
+ try std.testing.expectEqual(
+ @as(u65, 36893488147419103231),
+ try fromSlice(u65, gpa, "36893488147419103231", null, .{}),
+ );
+ try std.testing.expectEqual(
+ @as(u65, 36893488147419103231),
+ try fromSlice(u65, gpa, "368934_881_474191032_31", null, .{}),
+ );
+
+ // Test big integer limits
+ try std.testing.expectEqual(
+ @as(i66, 36893488147419103231),
+ try fromSlice(i66, gpa, "36893488147419103231", null, .{}),
+ );
+ try std.testing.expectEqual(
+ @as(i66, -36893488147419103232),
+ try fromSlice(i66, gpa, "-36893488147419103232", null, .{}),
+ );
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(
+ i66,
+ gpa,
+ "36893488147419103232",
+ &status,
+ .{},
+ ));
+ try std.testing.expectFmt(
+ "1:1: error: type 'i66' cannot represent value\n",
+ "{}",
+ .{status},
+ );
+ }
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(
+ i66,
+ gpa,
+ "-36893488147419103233",
+ &status,
+ .{},
+ ));
+ try std.testing.expectFmt(
+ "1:1: error: type 'i66' cannot represent value\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Test parsing whole number floats as integers
+ try std.testing.expectEqual(@as(i8, -1), try fromSlice(i8, gpa, "-1.0", null, .{}));
+ try std.testing.expectEqual(@as(i8, 123), try fromSlice(i8, gpa, "123.0", null, .{}));
+
+ // Test non-decimal integers
+ try std.testing.expectEqual(@as(i16, 0xff), try fromSlice(i16, gpa, "0xff", null, .{}));
+ try std.testing.expectEqual(@as(i16, -0xff), try fromSlice(i16, gpa, "-0xff", null, .{}));
+ try std.testing.expectEqual(@as(i16, 0o77), try fromSlice(i16, gpa, "0o77", null, .{}));
+ try std.testing.expectEqual(@as(i16, -0o77), try fromSlice(i16, gpa, "-0o77", null, .{}));
+ try std.testing.expectEqual(@as(i16, 0b11), try fromSlice(i16, gpa, "0b11", null, .{}));
+ try std.testing.expectEqual(@as(i16, -0b11), try fromSlice(i16, gpa, "-0b11", null, .{}));
+
+ // Test non-decimal big integers
+ try std.testing.expectEqual(@as(u65, 0x1ffffffffffffffff), try fromSlice(
+ u65,
+ gpa,
+ "0x1ffffffffffffffff",
+ null,
+ .{},
+ ));
+ try std.testing.expectEqual(@as(i66, 0x1ffffffffffffffff), try fromSlice(
+ i66,
+ gpa,
+ "0x1ffffffffffffffff",
+ null,
+ .{},
+ ));
+ try std.testing.expectEqual(@as(i66, -0x1ffffffffffffffff), try fromSlice(
+ i66,
+ gpa,
+ "-0x1ffffffffffffffff",
+ null,
+ .{},
+ ));
+ try std.testing.expectEqual(@as(u65, 0x1ffffffffffffffff), try fromSlice(
+ u65,
+ gpa,
+ "0o3777777777777777777777",
+ null,
+ .{},
+ ));
+ try std.testing.expectEqual(@as(i66, 0x1ffffffffffffffff), try fromSlice(
+ i66,
+ gpa,
+ "0o3777777777777777777777",
+ null,
+ .{},
+ ));
+ try std.testing.expectEqual(@as(i66, -0x1ffffffffffffffff), try fromSlice(
+ i66,
+ gpa,
+ "-0o3777777777777777777777",
+ null,
+ .{},
+ ));
+ try std.testing.expectEqual(@as(u65, 0x1ffffffffffffffff), try fromSlice(
+ u65,
+ gpa,
+ "0b11111111111111111111111111111111111111111111111111111111111111111",
+ null,
+ .{},
+ ));
+ try std.testing.expectEqual(@as(i66, 0x1ffffffffffffffff), try fromSlice(
+ i66,
+ gpa,
+ "0b11111111111111111111111111111111111111111111111111111111111111111",
+ null,
+ .{},
+ ));
+ try std.testing.expectEqual(@as(i66, -0x1ffffffffffffffff), try fromSlice(
+ i66,
+ gpa,
+ "-0b11111111111111111111111111111111111111111111111111111111111111111",
+ null,
+ .{},
+ ));
+
+ // Number with invalid character in the middle
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(u8, gpa, "32a32", &status, .{}));
+ try std.testing.expectFmt(
+ "1:3: error: invalid digit 'a' for decimal base\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Failing to parse as int
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(u8, gpa, "true", &status, .{}));
+ try std.testing.expectFmt("1:1: error: expected type 'u8'\n", "{}", .{status});
+ }
+
+ // Failing because an int is out of range
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(u8, gpa, "256", &status, .{}));
+ try std.testing.expectFmt(
+ "1:1: error: type 'u8' cannot represent value\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Failing because a negative int is out of range
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(i8, gpa, "-129", &status, .{}));
+ try std.testing.expectFmt(
+ "1:1: error: type 'i8' cannot represent value\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Failing because an unsigned int is negative
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(u8, gpa, "-1", &status, .{}));
+ try std.testing.expectFmt(
+ "1:1: error: type 'u8' cannot represent value\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Failing because a float is non-whole
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(u8, gpa, "1.5", &status, .{}));
+ try std.testing.expectFmt(
+ "1:1: error: type 'u8' cannot represent value\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Failing because a float is negative
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(u8, gpa, "-1.0", &status, .{}));
+ try std.testing.expectFmt(
+ "1:1: error: type 'u8' cannot represent value\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Negative integer zero
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(i8, gpa, "-0", &status, .{}));
+ try std.testing.expectFmt(
+ \\1:2: error: integer literal '-0' is ambiguous
+ \\1:2: note: use '0' for an integer zero
+ \\1:2: note: use '-0.0' for a floating-point signed zero
+ \\
+ , "{}", .{status});
+ }
+
+ // Negative integer zero casted to float
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(f32, gpa, "-0", &status, .{}));
+ try std.testing.expectFmt(
+ \\1:2: error: integer literal '-0' is ambiguous
+ \\1:2: note: use '0' for an integer zero
+ \\1:2: note: use '-0.0' for a floating-point signed zero
+ \\
+ , "{}", .{status});
+ }
+
+ // Negative float 0 is allowed
+ try std.testing.expect(
+ std.math.isNegativeZero(try fromSlice(f32, gpa, "-0.0", null, .{})),
+ );
+ try std.testing.expect(std.math.isPositiveZero(try fromSlice(f32, gpa, "0.0", null, .{})));
+
+ // Double negation is not allowed
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(i8, gpa, "--2", &status, .{}));
+ try std.testing.expectFmt(
+ "1:1: error: expected number or 'inf' after '-'\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(f32, gpa, "--2.0", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ "1:1: error: expected number or 'inf' after '-'\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Invalid int literal
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(u8, gpa, "0xg", &status, .{}));
+ try std.testing.expectFmt("1:3: error: invalid digit 'g' for hex base\n", "{}", .{status});
+ }
+
+ // Notes on invalid int literal
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(u8, gpa, "0123", &status, .{}));
+ try std.testing.expectFmt(
+ \\1:1: error: number '0123' has leading zero
+ \\1:1: note: use '0o' prefix for octal literals
+ \\
+ , "{}", .{status});
+ }
+}
+
+test "std.zon negative char" {
+ const gpa = std.testing.allocator;
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(f32, gpa, "-'a'", &status, .{}));
+ try std.testing.expectFmt(
+ "1:1: error: expected number or 'inf' after '-'\n",
+ "{}",
+ .{status},
+ );
+ }
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(i16, gpa, "-'a'", &status, .{}));
+ try std.testing.expectFmt(
+ "1:1: error: expected number or 'inf' after '-'\n",
+ "{}",
+ .{status},
+ );
+ }
+}
+
+test "std.zon parse float" {
+ const gpa = std.testing.allocator;
+
+ // Test decimals
+ try std.testing.expectEqual(@as(f16, 0.5), try fromSlice(f16, gpa, "0.5", null, .{}));
+ try std.testing.expectEqual(
+ @as(f32, 123.456),
+ try fromSlice(f32, gpa, "123.456", null, .{}),
+ );
+ try std.testing.expectEqual(
+ @as(f64, -123.456),
+ try fromSlice(f64, gpa, "-123.456", null, .{}),
+ );
+ try std.testing.expectEqual(@as(f128, 42.5), try fromSlice(f128, gpa, "42.5", null, .{}));
+
+ // Test whole numbers with and without decimals
+ try std.testing.expectEqual(@as(f16, 5.0), try fromSlice(f16, gpa, "5.0", null, .{}));
+ try std.testing.expectEqual(@as(f16, 5.0), try fromSlice(f16, gpa, "5", null, .{}));
+ try std.testing.expectEqual(@as(f32, -102), try fromSlice(f32, gpa, "-102.0", null, .{}));
+ try std.testing.expectEqual(@as(f32, -102), try fromSlice(f32, gpa, "-102", null, .{}));
+
+ // Test characters and negated characters
+ try std.testing.expectEqual(@as(f32, 'a'), try fromSlice(f32, gpa, "'a'", null, .{}));
+ try std.testing.expectEqual(@as(f32, 'z'), try fromSlice(f32, gpa, "'z'", null, .{}));
+
+ // Test big integers
+ try std.testing.expectEqual(
+ @as(f32, 36893488147419103231),
+ try fromSlice(f32, gpa, "36893488147419103231", null, .{}),
+ );
+ try std.testing.expectEqual(
+ @as(f32, -36893488147419103231),
+ try fromSlice(f32, gpa, "-36893488147419103231", null, .{}),
+ );
+ try std.testing.expectEqual(@as(f128, 0x1ffffffffffffffff), try fromSlice(
+ f128,
+ gpa,
+ "0x1ffffffffffffffff",
+ null,
+ .{},
+ ));
+ try std.testing.expectEqual(@as(f32, 0x1ffffffffffffffff), try fromSlice(
+ f32,
+ gpa,
+ "0x1ffffffffffffffff",
+ null,
+ .{},
+ ));
+
+ // Exponents, underscores
+ try std.testing.expectEqual(
+ @as(f32, 123.0E+77),
+ try fromSlice(f32, gpa, "12_3.0E+77", null, .{}),
+ );
+
+ // Hexadecimal
+ try std.testing.expectEqual(
+ @as(f32, 0x103.70p-5),
+ try fromSlice(f32, gpa, "0x103.70p-5", null, .{}),
+ );
+ try std.testing.expectEqual(
+ @as(f32, -0x103.70),
+ try fromSlice(f32, gpa, "-0x103.70", null, .{}),
+ );
+ try std.testing.expectEqual(
+ @as(f32, 0x1234_5678.9ABC_CDEFp-10),
+ try fromSlice(f32, gpa, "0x1234_5678.9ABC_CDEFp-10", null, .{}),
+ );
+
+ // inf, nan
+ try std.testing.expect(std.math.isPositiveInf(try fromSlice(f32, gpa, "inf", null, .{})));
+ try std.testing.expect(std.math.isNegativeInf(try fromSlice(f32, gpa, "-inf", null, .{})));
+ try std.testing.expect(std.math.isNan(try fromSlice(f32, gpa, "nan", null, .{})));
+
+ // Negative nan not allowed
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(f32, gpa, "-nan", &status, .{}));
+ try std.testing.expectFmt(
+ "1:1: error: expected number or 'inf' after '-'\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // nan as int not allowed
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(i8, gpa, "nan", &status, .{}));
+ try std.testing.expectFmt("1:1: error: expected type 'i8'\n", "{}", .{status});
+ }
+
+ // nan as int not allowed
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(i8, gpa, "nan", &status, .{}));
+ try std.testing.expectFmt("1:1: error: expected type 'i8'\n", "{}", .{status});
+ }
+
+ // inf as int not allowed
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(i8, gpa, "inf", &status, .{}));
+ try std.testing.expectFmt("1:1: error: expected type 'i8'\n", "{}", .{status});
+ }
+
+ // -inf as int not allowed
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(i8, gpa, "-inf", &status, .{}));
+ try std.testing.expectFmt("1:1: error: expected type 'i8'\n", "{}", .{status});
+ }
+
+ // Bad identifier as float
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(f32, gpa, "foo", &status, .{}));
+ try std.testing.expectFmt(
+ \\1:1: error: invalid expression
+ \\1:1: note: ZON allows identifiers 'true', 'false', 'null', 'inf', and 'nan'
+ \\1:1: note: precede identifier with '.' for an enum literal
+ \\
+ , "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(error.ParseZon, fromSlice(f32, gpa, "-foo", &status, .{}));
+ try std.testing.expectFmt(
+ "1:1: error: expected number or 'inf' after '-'\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Non float as float
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(f32, gpa, "\"foo\"", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected type 'f32'\n", "{}", .{status});
+ }
+}
+
+test "std.zon free on error" {
+ // Test freeing partially allocated structs
+ {
+ const Struct = struct {
+ x: []const u8,
+ y: []const u8,
+ z: bool,
+ };
+ try std.testing.expectError(error.ParseZon, fromSlice(Struct, std.testing.allocator,
+ \\.{
+ \\ .x = "hello",
+ \\ .y = "world",
+ \\ .z = "fail",
+ \\}
+ , null, .{}));
+ }
+
+ // Test freeing partially allocated tuples
+ {
+ const Struct = struct {
+ []const u8,
+ []const u8,
+ bool,
+ };
+ try std.testing.expectError(error.ParseZon, fromSlice(Struct, std.testing.allocator,
+ \\.{
+ \\ "hello",
+ \\ "world",
+ \\ "fail",
+ \\}
+ , null, .{}));
+ }
+
+ // Test freeing structs with missing fields
+ {
+ const Struct = struct {
+ x: []const u8,
+ y: bool,
+ };
+ try std.testing.expectError(error.ParseZon, fromSlice(Struct, std.testing.allocator,
+ \\.{
+ \\ .x = "hello",
+ \\}
+ , null, .{}));
+ }
+
+ // Test freeing partially allocated arrays
+ {
+ try std.testing.expectError(error.ParseZon, fromSlice(
+ [3][]const u8,
+ std.testing.allocator,
+ \\.{
+ \\ "hello",
+ \\ false,
+ \\ false,
+ \\}
+ ,
+ null,
+ .{},
+ ));
+ }
+
+ // Test freeing partially allocated slices
+ {
+ try std.testing.expectError(error.ParseZon, fromSlice(
+ [][]const u8,
+ std.testing.allocator,
+ \\.{
+ \\ "hello",
+ \\ "world",
+ \\ false,
+ \\}
+ ,
+ null,
+ .{},
+ ));
+ }
+
+ // We can parse types that can't be freed, as long as they contain no allocations, e.g. untagged
+ // unions.
+ try std.testing.expectEqual(
+ @as(f32, 1.5),
+ (try fromSlice(union { x: f32 }, std.testing.allocator, ".{ .x = 1.5 }", null, .{})).x,
+ );
+
+ // We can also parse types that can't be freed if it's impossible for an error to occur after
+ // the allocation, as is the case here.
+ {
+ const result = try fromSlice(
+ union { x: []const u8 },
+ std.testing.allocator,
+ ".{ .x = \"foo\" }",
+ null,
+ .{},
+ );
+ defer free(std.testing.allocator, result.x);
+ try std.testing.expectEqualStrings("foo", result.x);
+ }
+
+ // However, if it's possible we could get an error requiring we free the value, but the value
+ // cannot be freed (e.g. untagged unions) then we need to turn off `free_on_error` for it to
+ // compile.
+ {
+ const S = struct {
+ union { x: []const u8 },
+ bool,
+ };
+ const result = try fromSlice(
+ S,
+ std.testing.allocator,
+ ".{ .{ .x = \"foo\" }, true }",
+ null,
+ .{ .free_on_error = false },
+ );
+ defer free(std.testing.allocator, result[0].x);
+ try std.testing.expectEqualStrings("foo", result[0].x);
+ try std.testing.expect(result[1]);
+ }
+
+ // Again but for structs.
+ {
+ const S = struct {
+ a: union { x: []const u8 },
+ b: bool,
+ };
+ const result = try fromSlice(
+ S,
+ std.testing.allocator,
+ ".{ .a = .{ .x = \"foo\" }, .b = true }",
+ null,
+ .{
+ .free_on_error = false,
+ },
+ );
+ defer free(std.testing.allocator, result.a.x);
+ try std.testing.expectEqualStrings("foo", result.a.x);
+ try std.testing.expect(result.b);
+ }
+
+ // Again but for arrays.
+ {
+ const S = [2]union { x: []const u8 };
+ const result = try fromSlice(
+ S,
+ std.testing.allocator,
+ ".{ .{ .x = \"foo\" }, .{ .x = \"bar\" } }",
+ null,
+ .{
+ .free_on_error = false,
+ },
+ );
+ defer free(std.testing.allocator, result[0].x);
+ defer free(std.testing.allocator, result[1].x);
+ try std.testing.expectEqualStrings("foo", result[0].x);
+ try std.testing.expectEqualStrings("bar", result[1].x);
+ }
+
+ // Again but for slices.
+ {
+ const S = []union { x: []const u8 };
+ const result = try fromSlice(
+ S,
+ std.testing.allocator,
+ ".{ .{ .x = \"foo\" }, .{ .x = \"bar\" } }",
+ null,
+ .{
+ .free_on_error = false,
+ },
+ );
+ defer std.testing.allocator.free(result);
+ defer free(std.testing.allocator, result[0].x);
+ defer free(std.testing.allocator, result[1].x);
+ try std.testing.expectEqualStrings("foo", result[0].x);
+ try std.testing.expectEqualStrings("bar", result[1].x);
+ }
+}
+
+test "std.zon vector" {
+ if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/15330
+ if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/15329
+
+ const gpa = std.testing.allocator;
+
+ // Passing cases
+ try std.testing.expectEqual(
+ @Vector(0, bool){},
+ try fromSlice(@Vector(0, bool), gpa, ".{}", null, .{}),
+ );
+ try std.testing.expectEqual(
+ @Vector(3, bool){ true, false, true },
+ try fromSlice(@Vector(3, bool), gpa, ".{true, false, true}", null, .{}),
+ );
+
+ try std.testing.expectEqual(
+ @Vector(0, f32){},
+ try fromSlice(@Vector(0, f32), gpa, ".{}", null, .{}),
+ );
+ try std.testing.expectEqual(
+ @Vector(3, f32){ 1.5, 2.5, 3.5 },
+ try fromSlice(@Vector(3, f32), gpa, ".{1.5, 2.5, 3.5}", null, .{}),
+ );
+
+ try std.testing.expectEqual(
+ @Vector(0, u8){},
+ try fromSlice(@Vector(0, u8), gpa, ".{}", null, .{}),
+ );
+ try std.testing.expectEqual(
+ @Vector(3, u8){ 2, 4, 6 },
+ try fromSlice(@Vector(3, u8), gpa, ".{2, 4, 6}", null, .{}),
+ );
+
+ {
+ try std.testing.expectEqual(
+ @Vector(0, *const u8){},
+ try fromSlice(@Vector(0, *const u8), gpa, ".{}", null, .{}),
+ );
+ const pointers = try fromSlice(@Vector(3, *const u8), gpa, ".{2, 4, 6}", null, .{});
+ defer free(gpa, pointers);
+ try std.testing.expectEqualDeep(@Vector(3, *const u8){ &2, &4, &6 }, pointers);
+ }
+
+ {
+ try std.testing.expectEqual(
+ @Vector(0, ?*const u8){},
+ try fromSlice(@Vector(0, ?*const u8), gpa, ".{}", null, .{}),
+ );
+ const pointers = try fromSlice(@Vector(3, ?*const u8), gpa, ".{2, null, 6}", null, .{});
+ defer free(gpa, pointers);
+ try std.testing.expectEqualDeep(@Vector(3, ?*const u8){ &2, null, &6 }, pointers);
+ }
+
+ // Too few fields
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(@Vector(2, f32), gpa, ".{0.5}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ "1:2: error: expected 2 vector elements; found 1\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Too many fields
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(@Vector(2, f32), gpa, ".{0.5, 1.5, 2.5}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ "1:2: error: expected 2 vector elements; found 3\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Wrong type fields
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(@Vector(3, f32), gpa, ".{0.5, true, 2.5}", &status, .{}),
+ );
+ try std.testing.expectFmt(
+ "1:8: error: expected type 'f32'\n",
+ "{}",
+ .{status},
+ );
+ }
+
+ // Wrong type
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(@Vector(3, u8), gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected type '@Vector(3, u8)'\n", "{}", .{status});
+ }
+
+ // Elements should get freed on error
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(@Vector(3, *u8), gpa, ".{1, true, 3}", &status, .{}),
+ );
+ try std.testing.expectFmt("1:6: error: expected type 'u8'\n", "{}", .{status});
+ }
+}
+
+test "std.zon add pointers" {
+ const gpa = std.testing.allocator;
+
+ // Primitive with varying levels of pointers
+ {
+ const result = try fromSlice(*u32, gpa, "10", null, .{});
+ defer free(gpa, result);
+ try std.testing.expectEqual(@as(u32, 10), result.*);
+ }
+
+ {
+ const result = try fromSlice(**u32, gpa, "10", null, .{});
+ defer free(gpa, result);
+ try std.testing.expectEqual(@as(u32, 10), result.*.*);
+ }
+
+ {
+ const result = try fromSlice(***u32, gpa, "10", null, .{});
+ defer free(gpa, result);
+ try std.testing.expectEqual(@as(u32, 10), result.*.*.*);
+ }
+
+ // Primitive optional with varying levels of pointers
+ {
+ const some = try fromSlice(?*u32, gpa, "10", null, .{});
+ defer free(gpa, some);
+ try std.testing.expectEqual(@as(u32, 10), some.?.*);
+
+ const none = try fromSlice(?*u32, gpa, "null", null, .{});
+ defer free(gpa, none);
+ try std.testing.expectEqual(null, none);
+ }
+
+ {
+ const some = try fromSlice(*?u32, gpa, "10", null, .{});
+ defer free(gpa, some);
+ try std.testing.expectEqual(@as(u32, 10), some.*.?);
+
+ const none = try fromSlice(*?u32, gpa, "null", null, .{});
+ defer free(gpa, none);
+ try std.testing.expectEqual(null, none.*);
+ }
+
+ {
+ const some = try fromSlice(?**u32, gpa, "10", null, .{});
+ defer free(gpa, some);
+ try std.testing.expectEqual(@as(u32, 10), some.?.*.*);
+
+ const none = try fromSlice(?**u32, gpa, "null", null, .{});
+ defer free(gpa, none);
+ try std.testing.expectEqual(null, none);
+ }
+
+ {
+ const some = try fromSlice(*?*u32, gpa, "10", null, .{});
+ defer free(gpa, some);
+ try std.testing.expectEqual(@as(u32, 10), some.*.?.*);
+
+ const none = try fromSlice(*?*u32, gpa, "null", null, .{});
+ defer free(gpa, none);
+ try std.testing.expectEqual(null, none.*);
+ }
+
+ {
+ const some = try fromSlice(**?u32, gpa, "10", null, .{});
+ defer free(gpa, some);
+ try std.testing.expectEqual(@as(u32, 10), some.*.*.?);
+
+ const none = try fromSlice(**?u32, gpa, "null", null, .{});
+ defer free(gpa, none);
+ try std.testing.expectEqual(null, none.*.*);
+ }
+
+ // Pointer to an array
+ {
+ const result = try fromSlice(*[3]u8, gpa, ".{ 1, 2, 3 }", null, .{});
+ defer free(gpa, result);
+ try std.testing.expectEqual([3]u8{ 1, 2, 3 }, result.*);
+ }
+
+ // A complicated type with nested internal pointers and string allocations
+ {
+ const Inner = struct {
+ f1: *const ?*const []const u8,
+ f2: *const ?*const []const u8,
+ };
+ const Outer = struct {
+ f1: *const ?*const Inner,
+ f2: *const ?*const Inner,
+ };
+ const expected: Outer = .{
+ .f1 = &&.{
+ .f1 = &null,
+ .f2 = &&"foo",
+ },
+ .f2 = &null,
+ };
+
+ const found = try fromSlice(?*Outer, gpa,
+ \\.{
+ \\ .f1 = .{
+ \\ .f1 = null,
+ \\ .f2 = "foo",
+ \\ },
+ \\ .f2 = null,
+ \\}
+ , null, .{});
+ defer free(gpa, found);
+
+ try std.testing.expectEqualDeep(expected, found.?.*);
+ }
+
+ // Test that optional types are flattened correctly in errors
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(*const ?*const u8, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected type '?u8'\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(*const ?*const f32, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected type '?f32'\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(*const ?*const @Vector(3, u8), gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected type '?@Vector(3, u8)'\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(*const ?*const bool, gpa, "10", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected type '?bool'\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(*const ?*const struct { a: i32 }, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected optional struct\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(*const ?*const struct { i32 }, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected optional tuple\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(*const ?*const union { x: void }, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected optional union\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(*const ?*const [3]u8, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected optional array\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(?[3]u8, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected optional array\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(*const ?*const []u8, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected optional array\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(?[]u8, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected optional array\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(*const ?*const []const u8, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected optional string\n", "{}", .{status});
+ }
+
+ {
+ var status: Status = .{};
+ defer status.deinit(gpa);
+ try std.testing.expectError(
+ error.ParseZon,
+ fromSlice(*const ?*const enum { foo }, gpa, "true", &status, .{}),
+ );
+ try std.testing.expectFmt("1:1: error: expected optional enum literal\n", "{}", .{status});
+ }
+}