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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Turning a program's result into the caller's type.//!//! The rules are ZON's, so that a ZON file means the same thing here as it//! does to `std.zon.parse`: a struct literal fills a struct, and a field it//! leaves out takes the field's default or is an error; a field the struct//! does not have is an error; `.name` is an enum tag or a union variant with//! no payload, and `.{ .name = payload }` is a variant with one. Integers are//! range-checked, and an int is accepted where a float is wanted.//!//! A failure names the path to what failed -- `.listen[1].port` -- and points//! at the nearest place in the program that wrote a field on that path.//!//! Everything the result points to is copied into the result's arena,//! strings included, so it outlives both the program and the context.
const std = @import("std");const Allocator = std.mem.Allocator;
const Diagnostic = @import("Diagnostic.zig");const eval = @import("eval.zig");const Error = eval.Error;const value = @import("value.zig");const Value = value.Value;const Host = value.Host;
pub const Decoder = struct { arena: Allocator, sources: []const Diagnostic.Source, diag: *Diagnostic, steps: *usize, /// The nearest place in the program, as a byte offset, that wrote part /// of what is being decoded. pos: u32, path: [160]u8 = undefined, path_len: usize = 0, why: []const u8 = "",
fn fail(self: *Decoder, comptime fmt: []const u8, args: anytype) error{Eval} { if (self.path_len == 0) { self.diag.setAt(self.sources, self.pos, fmt, args); } else { self.diag.setAt(self.sources, self.pos, "{s}: " ++ fmt, .{self.path[0..self.path_len]} ++ args); } return error.Eval; }
fn hostFail(self: *Decoder, err: Host.Error) error{Eval} { return switch (err) { error.IntegerOutOfRange => self.fail("a value of type {s} does not fit in an i64", .{self.why}), error.Unsupported => self.fail("values of type {s} cannot be read", .{self.why}), error.InactiveUnionField => self.fail("the union holds .{s}", .{self.why}), }; }
fn step(self: *Decoder) error{LimitExceeded}!void { if (self.steps.* == 0) { self.diag.setAt(self.sources, self.pos, "result has too many values to decode", .{}); return error.LimitExceeded; } self.steps.* -= 1; }
/// Add `.name` or `[i]` to the path, returning what to restore. fn push(self: *Decoder, comptime fmt: []const u8, args: anytype) usize { const saved = self.path_len; var w: std.Io.Writer = .fixed(self.path[self.path_len..]); w.print(fmt, args) catch {}; self.path_len += w.end; return saved; }
fn fieldAt(self: *Decoder, v: Value, i: usize) Error!Value.Field { return v.fieldAt(i, &self.why) catch |err| self.hostFail(err); }
fn itemAt(self: *Decoder, v: Value, i: usize) Error!Value { return v.itemAt(i, &self.why) catch |err| self.hostFail(err); }
/// The field called `name`, with where it was written if it was. fn field(self: *Decoder, v: Value, name: []const u8) Error!?Value.Field { if (v == .record) { for (v.record) |f| if (std.mem.eql(u8, f.name, name)) return f; return null; } const found = v.field(name, &self.why) catch |err| switch (err) { // Looking for the variant a union does not hold is not finding it. error.InactiveUnionField => return null, else => |e| return self.hostFail(e), }; return if (found) |x| .{ .name = name, .value = x } else null; }};
pub fn decode(comptime T: type, d: *Decoder, v: Value) Error!T { try d.step(); if (T == Value) return own(d, v); switch (@typeInfo(T)) { .bool => if (v == .bool) return v.bool, .int => if (v == .int) { return std.math.cast(T, v.int) orelse d.fail("{d} does not fit in {s}", .{ v.int, @typeName(T) }); }, .float => switch (v) { .int => |i| return @floatFromInt(i), .float => |f| return @floatCast(f), else => {}, }, .optional => |o| return if (v == .null) null else try decode(o.child, d, v), .@"enum" => switch (v) { .enum_literal, .string => |name| return std.meta.stringToEnum(T, name) orelse d.fail("{s} has no tag .{f}", .{ @typeName(T), std.zig.fmtIdPU(name) }), else => {}, }, .void => if (v == .null or (v.fieldCount() orelse 1) == 0) return {}, .pointer => |p| switch (p.size) { .slice => { if (p.child == u8 and v == .string) { const out = try allocSlice(T, d.arena, v.string.len); @memcpy(out, v.string); return out; } if (v.itemCount()) |n| { const out = try allocSlice(T, d.arena, n); for (out, 0..) |*item, i| { const saved = d.push("[{d}]", .{i}); defer d.path_len = saved; item.* = try decode(p.child, d, try d.itemAt(v, i)); } return out; } }, .one => { const out = try d.arena.create(p.child); out.* = try decode(p.child, d, v); return out; }, else => @compileError("struthio cannot decode into " ++ @typeName(T)), }, .array => |a| { if (a.child == u8 and v == .string) { if (v.string.len != a.len) return d.fail("expected {d} bytes, found {d}", .{ a.len, v.string.len }); var out: T = undefined; @memcpy(out[0..a.len], v.string); return out; } if (v.itemCount()) |n| { if (n != a.len) return d.fail("expected {d} items, found {d}", .{ a.len, n }); var out: T = undefined; for (0..a.len) |i| { const saved = d.push("[{d}]", .{i}); defer d.path_len = saved; out[i] = try decode(a.child, d, try d.itemAt(v, i)); } return out; } }, .@"struct" => |s| { if (s.is_tuple) { if (v.itemCount()) |n| { if (n != s.field_names.len) return d.fail("expected {d} items, found {d}", .{ s.field_names.len, n }); var out: T = undefined; inline for (s.field_types, 0..) |FT, i| { const saved = d.push("[{d}]", .{i}); defer d.path_len = saved; out[i] = try decode(FT, d, try d.itemAt(v, i)); } return out; } } else if (v.fieldCount() != null or (v.itemCount() orelse 1) == 0) { return decodeStruct(T, d, v); } }, .@"union" => |u| { if (u.tag_type == null) @compileError("struthio cannot decode into untagged union " ++ @typeName(T)); return decodeUnion(T, d, v); }, else => @compileError("struthio cannot decode into " ++ @typeName(T)), } return d.fail("expected {s}, found {s}", .{ @typeName(T), v.kindName() });}
/// `T` with its `const` taken off, so that it can be filled in.fn Mutable(comptime T: type) type { const p = @typeInfo(T).pointer; return if (p.sentinel()) |s| [:s]p.child else []p.child;}
fn allocSlice(comptime T: type, arena: Allocator, n: usize) Allocator.Error!Mutable(T) { const p = @typeInfo(T).pointer; if (comptime p.sentinel()) |s| return arena.allocSentinel(p.child, n, s); return arena.alloc(p.child, n);}
fn decodeStruct(comptime T: type, d: *Decoder, v: Value) Error!T { const info = @typeInfo(T).@"struct";
// Every field written has to be one the struct has. const n = v.fieldCount() orelse 0; for (0..n) |i| { const f = try d.fieldAt(v, i); const known = inline for (info.field_names, info.field_attrs) |name, attrs| { if (!attrs.@"comptime" and std.mem.eql(u8, name, f.name)) break true; } else false; if (!known) { if (f.pos != Value.no_pos) d.pos = f.pos; return d.fail("{s} has no field '{s}'", .{ @typeName(T), f.name }); } }
var out: T = undefined; inline for (info.field_names, info.field_types, info.field_attrs) |name, FT, attrs| { if (attrs.@"comptime") continue; if (try d.field(v, name)) |f| { const saved_pos = d.pos; defer d.pos = saved_pos; if (f.pos != Value.no_pos) d.pos = f.pos; const saved = d.push(".{f}", .{std.zig.fmtIdPU(name)}); defer d.path_len = saved; @field(out, name) = try decode(FT, d, f.value); } else if (comptime attrs.defaultValue(FT)) |default| { @field(out, name) = default; } else { return d.fail("missing field '{s}' of {s}", .{ name, @typeName(T) }); } } return out;}
fn decodeUnion(comptime T: type, d: *Decoder, v: Value) Error!T { const info = @typeInfo(T).@"union"; switch (v) { // `.name`, for a variant with no payload. .enum_literal, .string => |name| { inline for (info.field_names, info.field_types) |fname, FT| { if (std.mem.eql(u8, fname, name)) { if (FT != void) return d.fail("variant .{s} of {s} needs a value: .{{ .{s} = ... }}", .{ fname, @typeName(T), fname }); return @unionInit(T, fname, {}); } } return d.fail("{s} has no variant .{f}", .{ @typeName(T), std.zig.fmtIdPU(name) }); }, else => {}, } // `.{ .name = payload }`, exactly one field. if (v.fieldCount()) |n| { if (n != 1) return d.fail("a union is written with exactly one field, not {d}", .{n}); const f = try d.fieldAt(v, 0); inline for (info.field_names, info.field_types) |fname, FT| { if (std.mem.eql(u8, fname, f.name)) { const saved_pos = d.pos; defer d.pos = saved_pos; if (f.pos != Value.no_pos) d.pos = f.pos; const saved = d.push(".{f}", .{std.zig.fmtIdPU(fname)}); defer d.path_len = saved; return @unionInit(T, fname, try decode(FT, d, f.value)); } } if (f.pos != Value.no_pos) d.pos = f.pos; return d.fail("{s} has no variant .{f}", .{ @typeName(T), std.zig.fmtIdPU(f.name) }); } return d.fail("expected {s}, found {s}", .{ @typeName(T), v.kindName() });}
/// A copy of `v` that owns everything it points to, with whatever came from/// the context turned into plain values: the result for a field of type/// `Value`, which must outlive the context as much as any other field.fn own(d: *Decoder, v: Value) Error!Value { switch (v) { .null, .bool, .int, .float => return v, .string => |s| return .{ .string = try d.arena.dupe(u8, s) }, .enum_literal => |s| return .{ .enum_literal = try d.arena.dupe(u8, s) }, .list, .record, .host => { if (v.itemCount()) |n| { const out = try d.arena.alloc(Value, n); for (out, 0..) |*item, i| { try d.step(); item.* = try own(d, try d.itemAt(v, i)); } return .{ .list = out }; } const n = v.fieldCount().?; const out = try d.arena.alloc(Value.Field, n); for (out, 0..) |*f, i| { try d.step(); const src = try d.fieldAt(v, i); f.* = .{ .name = try d.arena.dupe(u8, src.name), .value = try own(d, src.value), .pos = src.pos }; } return .{ .record = out }; }, }}