// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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)); }