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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Reading the caller's context, whatever its type, by reflection.//!//! Scalars become values straight away. A struct, a tagged union, or a list//! of anything but bytes becomes a `Host`: a pointer into the context and a//! table of functions generated for its type, so that `ctx.a.b` reads `a`//! and then `b` and nothing else.//!//! A type the language cannot represent -- a function, an `anyopaque`//! pointer, an untagged union -- is not a compile error. It is an error when//! a program reads it, and only then, so a context may be a program's real//! state, allocator and all, and a program reading its other fields never//! notices.
const std = @import("std");const Value = @import("value.zig").Value;const Host = @import("value.zig").Host;
pub const Error = Host.Error;
/// The value of `ptr.*`. `why` is set to the name of the type that could not/// be read when this fails.pub fn of(comptime T: type, ptr: *const T, why: *[]const u8) Error!Value { switch (@typeInfo(T)) { .void, .null => return .null, .bool => return .{ .bool = ptr.* }, .int => return .{ .int = std.math.cast(i64, ptr.*) orelse { why.* = @typeName(T); return error.IntegerOutOfRange; } }, .float => return .{ .float = @floatCast(ptr.*) }, .@"enum" => return .{ .enum_literal = std.enums.tagName(T, ptr.*) orelse { why.* = @typeName(T); return error.Unsupported; } }, .optional => |o| return if (ptr.*) |*child| of(o.child, child, why) else .null, .pointer => |p| switch (p.size) { .one => switch (@typeInfo(p.child)) { .@"fn", .@"opaque" => {}, else => return of(p.child, ptr.*, why), }, .slice => { if (p.child == u8) return .{ .string = ptr.* }; return .{ .host = .{ .ptr = @ptrCast(ptr), .vt = &ListHost(T).vtable } }; }, .many => if (p.child == u8 and p.sentinel() != null and p.sentinel().? == 0) { return .{ .string = std.mem.span(ptr.*) }; }, .c => {}, }, .array => return .{ .host = .{ .ptr = @ptrCast(ptr), .vt = &ListHost(T).vtable } }, .@"struct" => |s| { const vt = if (s.is_tuple) &ListHost(T).vtable else &StructHost(T).vtable; return .{ .host = .{ .ptr = @ptrCast(ptr), .vt = vt } }; }, .@"union" => |u| if (u.tag_type != null) { return .{ .host = .{ .ptr = @ptrCast(ptr), .vt = &UnionHost(T).vtable } }; }, else => {}, } why.* = @typeName(T); return error.Unsupported;}
fn StructHost(comptime T: type) type { const info = @typeInfo(T).@"struct"; return struct { // A comptime field has no address to point at, and is a constant // rather than a setting, so it is not offered. const names = blk: { var list: []const []const u8 = &.{}; for (info.field_names, info.field_attrs) |name, attrs| { if (!attrs.@"comptime") list = list ++ .{@as([]const u8, name)}; } break :blk list; };
const vtable: Host.VTable = .{ .type_name = @typeName(T), .kind = .@"struct", .field_names = names, .field = field, };
fn field(p: *const anyopaque, name: []const u8, why: *[]const u8) Error!?Value { const self: *const T = @ptrCast(@alignCast(p)); inline for (info.field_names, info.field_types, info.field_attrs) |fname, FT, attrs| { if (attrs.@"comptime") continue; if (std.mem.eql(u8, fname, name)) { if (info.layout == .@"packed") { // A field of a packed struct has no address of its // own, so read it into a local. That is only safe for // values that do not point back at it. if (@typeInfo(FT) == .@"struct" or @typeInfo(FT) == .@"union") { why.* = @typeName(FT); return error.Unsupported; } const copy = @field(self.*, fname); return try of(FT, ©, why); } return try of(FT, &@field(self.*, fname), why); } } return null; } };}
fn UnionHost(comptime T: type) type { const info = @typeInfo(T).@"union"; return struct { const names = blk: { var list: []const []const u8 = &.{}; for (info.field_names) |name| list = list ++ .{@as([]const u8, name)}; break :blk list; };
const vtable: Host.VTable = .{ .type_name = @typeName(T), .kind = .@"union", .field_names = names, .field = field, .tag = tag, };
fn tag(p: *const anyopaque) []const u8 { const self: *const T = @ptrCast(@alignCast(p)); return @tagName(self.*); }
fn field(p: *const anyopaque, name: []const u8, why: *[]const u8) Error!?Value { const self: *const T = @ptrCast(@alignCast(p)); inline for (info.field_names, info.field_types) |fname, FT| { if (std.mem.eql(u8, fname, name)) { if (!std.mem.eql(u8, @tagName(self.*), fname)) { why.* = @tagName(self.*); return error.InactiveUnionField; } return try of(FT, &@field(self.*, fname), why); } } return null; } };}
/// A slice, an array, or a tuple.fn ListHost(comptime T: type) type { return struct { const vtable: Host.VTable = .{ .type_name = @typeName(T), .kind = .list, .len = len, .index = index, };
fn len(p: *const anyopaque) usize { const self: *const T = @ptrCast(@alignCast(p)); return switch (@typeInfo(T)) { .pointer => self.*.len, .array => |a| a.len, .@"struct" => |s| s.field_names.len, else => comptime unreachable, }; }
fn index(p: *const anyopaque, i: usize, why: *[]const u8) Error!Value { const self: *const T = @ptrCast(@alignCast(p)); switch (@typeInfo(T)) { .pointer => |ptr| return of(ptr.child, &self.*[i], why), .array => |a| return of(a.child, &self.*[i], why), .@"struct" => |s| { inline for (s.field_types, 0..) |FT, n| { if (n == i) return of(FT, &self.*[n], why); } unreachable; }, else => comptime unreachable, } } };}
const testing = std.testing;
const Level = enum { debug, info };const Backend = union(enum) { memory, redis: struct { url: []const u8 },};const Context = struct { name: []const u8 = "web", port: u16 = 80, big: u64 = std.math.maxInt(u64), ratio: f32 = 0.5, level: Level = .info, cert: ?[]const u8 = null, tags: []const []const u8 = &.{ "a", "b" }, pair: struct { u8, []const u8 } = .{ 1, "x" }, backend: Backend = .{ .redis = .{ .url = "redis://" } }, alloc: std.mem.Allocator = testing.allocator, next: ?*const Context = null,};
fn get(v: Value, name: []const u8) !Value { var why: []const u8 = ""; return (try v.field(name, &why)).?;}
test "scalars are converted straight away" { const ctx: Context = .{}; var why: []const u8 = ""; const v = try of(Context, &ctx, &why); try testing.expectEqualStrings("web", (try get(v, "name")).string); try testing.expectEqual(@as(i64, 80), (try get(v, "port")).int); try testing.expectEqual(@as(f64, 0.5), (try get(v, "ratio")).float); try testing.expectEqualStrings("info", (try get(v, "level")).enum_literal); try testing.expectEqual(Value.null, try get(v, "cert")); try testing.expectEqual(@as(?Value, null), try v.field("nope", &why));}
test "lists, tuples, unions and recursion" { const inner: Context = .{ .name = "inner" }; const ctx: Context = .{ .next = &inner }; var why: []const u8 = ""; const v = try of(Context, &ctx, &why);
const tags = try get(v, "tags"); try testing.expectEqual(@as(?usize, 2), tags.itemCount()); try testing.expectEqualStrings("b", (try tags.itemAt(1, &why)).string);
const pair = try get(v, "pair"); try testing.expectEqual(@as(i64, 1), (try pair.itemAt(0, &why)).int); try testing.expectEqualStrings("x", (try pair.itemAt(1, &why)).string);
const backend = try get(v, "backend"); try testing.expectEqualStrings("redis", backend.host.vt.tag(backend.host.ptr)); try testing.expectEqualStrings("redis://", (try get(try get(backend, "redis"), "url")).string); try testing.expectError(error.InactiveUnionField, backend.field("memory", &why)); try testing.expectEqualStrings("redis", why);
try testing.expectEqualStrings("inner", (try get(try get(v, "next"), "name")).string);}
test "what cannot be read is an error only when it is read" { const ctx: Context = .{}; var why: []const u8 = ""; const v = try of(Context, &ctx, &why); try testing.expectError(error.IntegerOutOfRange, v.field("big", &why)); try testing.expectEqualStrings("u64", why); const alloc = try get(v, "alloc"); try testing.expectError(error.Unsupported, alloc.field("ptr", &why));}