Something went wrong. Try again.
A small configuration language for Zig: ZON with let, if and templates over a context, evaluated to a struct, always finishing.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! What an expression evaluates to, before it is decoded into a Zig type.//!//! Every value is immutable, and composite ones share their parts freely: a//! `let` bound to a struct and used twice is one struct, not two copies.//! Values read out of the caller's context are not copied at all -- a//! `.host` value is a pointer into the context and a table of functions that//! know its type -- so a program that reads one field of a large struct//! reads one field.
const std = @import("std");const Allocator = std.mem.Allocator;
pub const Value = union(enum) { null, bool: bool, int: i64, float: f64, string: []const u8, /// `.name`: a Zig enum literal, which decodes into any enum with that /// tag, or into a union variant with no payload. enum_literal: []const u8, list: []const Value, /// A struct literal, `.{ .a = 1 }`. Field names are unique, and in the /// order they were written. record: []const Field, /// Something in the caller's context too big to convert eagerly: a /// struct, a tagged union, or a slice or array of anything but bytes. host: Host,
pub const Field = struct { name: []const u8, value: Value, /// Where the field was written in the program, as a byte offset, so /// that a value that fails to decode can say where it came from. /// `no_pos` for a field that came from the context. pos: u32 = no_pos, };
pub const no_pos = std.math.maxInt(u32);
/// A word for what kind of value this is, for messages. pub fn kindName(v: Value) []const u8 { return switch (v) { .null => "null", .bool => "bool", .int => "int", .float => "float", .string => "string", .enum_literal => "enum literal", .list => "list", .record => "struct", .host => |h| h.vt.type_name, }; }
// -- struct-like views --------------------------------------------------- // // A record, a struct from the context, and a tagged union from the // context (seen as a struct with one field, the active one, which is how // a program writes a union) all answer these the same way.
/// How many fields, or null if this is not struct-like. pub fn fieldCount(v: Value) ?usize { return switch (v) { .record => |r| r.len, .host => |h| switch (h.vt.kind) { .@"struct" => h.vt.field_names.len, .@"union" => 1, .list => null, }, else => null, }; }
/// The `i`th field of a struct-like value. pub fn fieldAt(v: Value, i: usize, why: *[]const u8) Host.Error!Field { switch (v) { .record => |r| return r[i], .host => |h| { const name = switch (h.vt.kind) { .@"struct" => h.vt.field_names[i], .@"union" => h.vt.tag(h.ptr), .list => unreachable, }; return .{ .name = name, .value = (try h.vt.field(h.ptr, name, why)).? }; }, else => unreachable, } }
/// The field called `name`, or null if there is none -- or if this is not /// struct-like at all. For a union from the context, naming a variant /// other than the active one is `error.InactiveUnionField`. pub fn field(v: Value, name: []const u8, why: *[]const u8) Host.Error!?Value { switch (v) { .record => |r| { for (r) |f| if (std.mem.eql(u8, f.name, name)) return f.value; return null; }, .host => |h| return if (h.vt.kind == .list) null else h.vt.field(h.ptr, name, why), else => return null, } }
/// The fields of a struct-like value as a record, reading a struct from /// the context one level deep. pub fn fields(v: Value, arena: Allocator, why: *[]const u8) (Host.Error || Allocator.Error)![]const Field { if (v == .record) return v.record; const n = v.fieldCount().?; const out = try arena.alloc(Field, n); for (out, 0..) |*f, i| f.* = try v.fieldAt(i, why); return out; }
// -- list-like views ----------------------------------------------------- // // A list, a slice, array or tuple from the context, and `.{}` -- which is // as much an empty list as it is an empty struct.
/// How many items, or null if this is not list-like. pub fn itemCount(v: Value) ?usize { return switch (v) { .list => |l| l.len, .record => |r| if (r.len == 0) 0 else null, .host => |h| if (h.vt.kind == .list) h.vt.len(h.ptr) else null, else => null, }; }
/// The `i`th item of a list-like value; `i` must be below `itemCount`. pub fn itemAt(v: Value, i: usize, why: *[]const u8) Host.Error!Value { return switch (v) { .list => |l| l[i], .host => |h| h.vt.index(h.ptr, i, why), else => unreachable, }; }
/// The items of a list-like value as a list. pub fn items(v: Value, arena: Allocator, why: *[]const u8) (Host.Error || Allocator.Error)![]const Value { if (v == .list) return v.list; const n = v.itemCount().?; const out = try arena.alloc(Value, n); for (out, 0..) |*item, i| item.* = try v.itemAt(i, why); return out; }
// -- comparison ----------------------------------------------------------
pub const EqlError = Host.Error || error{LimitExceeded};
/// Whether two values are equal, the way `==` decides. /// /// Numbers compare by value whatever their kind, so `1 == 1.0`. An enum /// literal equals a union from the context whose active tag it names, as /// `u == .tag` does in Zig. Lists and structs compare item by item and /// field by field. `budget` is decremented for every value visited, since /// shared structure can make a small program describe an enormous value. pub fn eql(a: Value, b: Value, budget: *usize, why: *[]const u8) EqlError!bool { if (budget.* == 0) return error.LimitExceeded; budget.* -= 1; switch (a) { .null => return b == .null, .bool => |x| return b == .bool and b.bool == x, .int => |x| return switch (b) { .int => |y| x == y, .float => |y| @as(f64, @floatFromInt(x)) == y, else => false, }, .float => |x| return switch (b) { .int => |y| x == @as(f64, @floatFromInt(y)), .float => |y| x == y, else => false, }, .string => |x| return b == .string and std.mem.eql(u8, x, b.string), .enum_literal => |x| return switch (b) { .enum_literal => |y| std.mem.eql(u8, x, y), .host => |h| h.vt.kind == .@"union" and std.mem.eql(u8, x, h.vt.tag(h.ptr)), else => false, }, .list, .record, .host => { if (a == .host and a.host.vt.kind == .@"union" and b == .enum_literal) return eql(b, a, budget, why); if (a.itemCount()) |n| { const m = b.itemCount() orelse return false; if (n != m) return false; for (0..n) |i| { if (!try eql(try a.itemAt(i, why), try b.itemAt(i, why), budget, why)) return false; } return true; } const n = a.fieldCount().?; const m = b.fieldCount() orelse return false; if (n != m) return false; for (0..n) |i| { const fa = try a.fieldAt(i, why); const vb = (b.field(fa.name, why) catch |err| switch (err) { error.InactiveUnionField => return false, else => return err, }) orelse return false; if (!try eql(fa.value, vb, budget, why)) return false; } return true; }, } }
// -- printing ------------------------------------------------------------
/// The value as ZON, on one line: what a program would have to say to /// produce it. Anything in the context that cannot be read prints as /// `@"<unreadable>"`. /// /// This visits shared structure as many times as it is shared, so it is /// for tests and debugging, not for values a stranger's program built. pub fn format(v: Value, w: *std.Io.Writer) std.Io.Writer.Error!void { var why: []const u8 = ""; switch (v) { .null => try w.writeAll("null"), .bool => |b| try w.writeAll(if (b) "true" else "false"), .int => |i| try w.print("{d}", .{i}), .float => |f| try formatFloat(f, w), .string => |s| try w.print("\"{f}\"", .{std.zig.fmtString(s)}), .enum_literal => |name| try w.print(".{f}", .{std.zig.fmtIdPU(name)}), .list, .record, .host => { if (v.itemCount()) |n| { if (n == 0) return w.writeAll(".{}"); try w.writeAll(".{ "); for (0..n) |i| { if (i > 0) try w.writeAll(", "); const item = v.itemAt(i, &why) catch { try w.writeAll("@\"<unreadable>\""); continue; }; try item.format(w); } return w.writeAll(" }"); } const n = v.fieldCount().?; if (n == 0) return w.writeAll(".{}"); try w.writeAll(".{ "); for (0..n) |i| { if (i > 0) try w.writeAll(", "); const f = v.fieldAt(i, &why) catch { try w.writeAll("@\"<unreadable>\""); continue; }; try w.print(".{f} = {f}", .{ std.zig.fmtIdPU(f.name), f.value }); } try w.writeAll(" }"); }, } }
/// A float as ZON, which always looks like one: `1.0` rather than `1`. fn formatFloat(f: f64, w: *std.Io.Writer) std.Io.Writer.Error!void { if (std.math.isNan(f)) return w.writeAll("nan"); if (std.math.isInf(f)) return w.writeAll(if (f < 0) "-inf" else "inf"); // `{d}` writes every digit, never an exponent: `1e300` is 301 of // them, and the longest of all, `-std.math.floatTrueMin(f64)`, is 327 // characters. var buf: [512]u8 = undefined; const text = std.fmt.bufPrint(&buf, "{d}", .{f}) catch unreachable; try w.writeAll(text); if (std.mem.findAny(u8, text, ".e") == null) try w.writeAll(".0"); }};
/// A value in the caller's context: a pointer to it, and the functions that/// know how to read its type. `host.zig` makes these.pub const Host = struct { ptr: *const anyopaque, vt: *const VTable,
/// What can go wrong reading the context. Whatever went wrong is named /// in the `why` each function is handed: the type that cannot be read, /// or the variant a union actually holds. pub const Error = error{ /// An integer that does not fit an `i64`. IntegerOutOfRange, /// A type the language has no value for: a function, an opaque /// pointer, an untagged union, an enum value with no name. Unsupported, /// A union variant named that is not the active one. InactiveUnionField, };
pub const Kind = enum { @"struct", @"union", list };
pub const VTable = struct { type_name: []const u8, kind: Kind, /// For a struct, its fields; for a union, its variants. field_names: []const []const u8 = &.{}, field: *const fn (ptr: *const anyopaque, name: []const u8, why: *[]const u8) Error!?Value = noField, /// The active variant of a union. tag: *const fn (ptr: *const anyopaque) []const u8 = noTag, len: *const fn (ptr: *const anyopaque) usize = noLen, index: *const fn (ptr: *const anyopaque, i: usize, why: *[]const u8) Error!Value = noIndex,
fn noField(_: *const anyopaque, _: []const u8, _: *[]const u8) Error!?Value { return null; } fn noTag(_: *const anyopaque) []const u8 { unreachable; } fn noLen(_: *const anyopaque) usize { unreachable; } fn noIndex(_: *const anyopaque, _: usize, _: *[]const u8) Error!Value { unreachable; } };};
fn expectFormat(expected: []const u8, v: Value) !void { var buf: [512]u8 = undefined; try std.testing.expectEqualStrings(expected, try std.fmt.bufPrint(&buf, "{f}", .{v}));}
test "values print as ZON" { try expectFormat("null", .null); try expectFormat("1.0", .{ .float = 1 }); try expectFormat("-2.5", .{ .float = -2.5 }); // Every digit, however many: this used to overrun the buffer. try expectFormat("1" ++ @as([300]u8, @splat('0')) ++ ".0", .{ .float = 1e300 }); try expectFormat("\"a\\\"b\\n\"", .{ .string = "a\"b\n" }); try expectFormat(".debug", .{ .enum_literal = "debug" }); try expectFormat(".@\"a b\"", .{ .enum_literal = "a b" }); try expectFormat(".{ 1, true }", .{ .list = &.{ .{ .int = 1 }, .{ .bool = true } } }); try expectFormat(".{ .a = 1, .@\"if\" = .{} }", .{ .record = &.{ .{ .name = "a", .value = .{ .int = 1 } }, .{ .name = "if", .value = .{ .record = &.{} } }, } });}
test "equality" { var budget: usize = 1000; var why: []const u8 = ""; const t = std.testing; try t.expect(try Value.eql(.{ .int = 1 }, .{ .float = 1.0 }, &budget, &why)); try t.expect(!try Value.eql(.{ .int = 1 }, .{ .string = "1" }, &budget, &why)); try t.expect(try Value.eql(.{ .record = &.{} }, .{ .list = &.{} }, &budget, &why)); const a: Value = .{ .record = &.{ .{ .name = "x", .value = .{ .int = 1 } }, .{ .name = "y", .value = .null } } }; const b: Value = .{ .record = &.{ .{ .name = "y", .value = .null }, .{ .name = "x", .value = .{ .int = 1 } } } }; const c: Value = .{ .record = &.{ .{ .name = "y", .value = .null }, .{ .name = "z", .value = .{ .int = 1 } } } }; try t.expect(try Value.eql(a, b, &budget, &why)); try t.expect(!try Value.eql(a, c, &budget, &why));}
test "equality is bounded" { var budget: usize = 2; var why: []const u8 = ""; const l: Value = .{ .list = &.{ .{ .int = 1 }, .{ .int = 2 } } }; try std.testing.expectError(error.LimitExceeded, Value.eql(l, l, &budget, &why));}