summaryrefslogtreecommitdiff
path: root/9proc/src/vars.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-19 23:28:22 -0300
committerGabriel Schneider <[email protected]>2026-09-19 23:28:22 -0300
commitba996acfcad1698adbf4a1834fe50e73b1c6cab9 (patch)
tree282ba00ce5b10d7416aecb9f2f0f0a439340a57d /9proc/src/vars.zig
parentb05abcba3ea09ea106ad28364c6e40a3ec31b890 (diff)
downloadcloud9-ba996acfcad1698adbf4a1834fe50e73b1c6cab9.tar.gz
cloud9-ba996acfcad1698adbf4a1834fe50e73b1c6cab9.zip
Rename programs: 9player -> 9ns, introspect -> 9proc, app -> web (9web)
Directories, binaries, build options (-D9ns, -D9proc), step names, module name (9proc), thread and fs names, env var NINEPLAYER_MOUNT -> NINE_MOUNT, docs and test scripts. Browser assets move to web/static. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to '9proc/src/vars.zig')
-rw-r--r--9proc/src/vars.zig824
1 files changed, 824 insertions, 0 deletions
diff --git a/9proc/src/vars.zig b/9proc/src/vars.zig
new file mode 100644
index 0000000..20cadfc
--- /dev/null
+++ b/9proc/src/vars.zig
@@ -0,0 +1,824 @@
+//! Comptime value renderers for /vars. For a type T, `vtableFor(T)` builds (at
+//! comptime) a flat table of the files and directories that describe a value of
+//! that type:
+//!
+//! <var>/value rendered text <var>/type @typeName
+//! <var>/size @sizeOf <var>/addr 0x…
+//! <var>/raw the bytes <var>/f/<field>/... recursively (depth <= max_depth)
+//!
+//! The core serves a variable by walking this table; a node index is the whole
+//! state it needs. Rendering writes into a `*std.Io.Writer` and never allocates.
+//! Writes to scalar `value` files are plain stores (not atomic).
+const std = @import("std");
+const Writer = std.Io.Writer;
+
+/// Deepest `f/` nesting: /vars/x/f/a/f/b/f/c/f/d/value is depth 4.
+pub const max_depth: u8 = 4;
+/// Longest string rendered from a `[]const u8` / `[*:0]const u8` before "…".
+pub const max_string: usize = 256;
+/// Most array/slice elements rendered before "…".
+pub const max_elems: usize = 64;
+
+pub const Kind = enum(u8) {
+ /// The directory of a value: value, type, size, addr, raw, [f].
+ dir,
+ /// Rendered text; writable when `set` is non-null.
+ value,
+ /// @typeName, static content.
+ type_name,
+ /// @sizeOf, static content.
+ size,
+ /// "0x…" of the value's address.
+ addr,
+ /// The bytes of the value, length = size.
+ raw,
+ /// The `f` directory: one `dir` per struct field.
+ fields,
+};
+
+pub const RenderFn = *const fn (base: [*]const u8, w: *Writer) Writer.Error!void;
+pub const SetFn = *const fn (base: [*]u8, text: []const u8) SetError!void;
+pub const SetError = error{ Invalid, Unsupported };
+
+/// One node of a type's tree. Children occupy `first..first+count`.
+pub const Node = struct {
+ name: []const u8,
+ kind: Kind,
+ parent: u32,
+ first: u32 = 0,
+ count: u32 = 0,
+ /// Byte offset of the described value from the variable's base address.
+ offset: usize,
+ /// @sizeOf the described value.
+ size: usize,
+ /// Static text for `type_name` and `size` leaves.
+ content: []const u8 = "",
+ render: ?RenderFn = null,
+ set: ?SetFn = null,
+
+ pub fn isDir(n: Node) bool {
+ return n.kind == .dir or n.kind == .fields;
+ }
+
+ pub fn writable(n: Node) bool {
+ return n.kind == .value and n.set != null;
+ }
+};
+
+pub const VTable = struct {
+ nodes: []const Node,
+ type_name: []const u8,
+ size: usize,
+
+ /// The child of `dir` named `name`, if any.
+ pub fn child(vt: *const VTable, dir: u32, name: []const u8) ?u32 {
+ const d = vt.nodes[dir];
+ for (d.first..d.first + d.count) |i| {
+ if (std.mem.eql(u8, vt.nodes[i].name, name)) return @intCast(i);
+ }
+ return null;
+ }
+};
+
+/// The comptime-generated table for `T`; the same pointer for the same `T`.
+pub fn vtableFor(comptime T: type) *const VTable {
+ const S = struct {
+ const nodes = buildTable(T);
+ const vt: VTable = .{ .nodes = &nodes, .type_name = @typeName(T), .size = @sizeOf(T) };
+ };
+ return &S.vt;
+}
+
+/// Whether a struct's fields get an `f/` directory at this depth.
+fn hasFields(comptime T: type, depth: u8) bool {
+ if (depth >= max_depth) return false;
+ return switch (@typeInfo(T)) {
+ .@"struct" => |s| s.layout != .@"packed" and fieldCount(T) > 0,
+ else => false,
+ };
+}
+
+fn fieldCount(comptime T: type) usize {
+ var n: usize = 0;
+ for (@typeInfo(T).@"struct".fields) |f| {
+ if (!f.is_comptime and @sizeOf(f.type) != 0) n += 1;
+ }
+ return n;
+}
+
+fn countNodes(comptime T: type, depth: u8) usize {
+ var n: usize = 6; // dir + value, type, size, addr, raw
+ if (hasFields(T, depth)) {
+ n += 1; // f
+ for (@typeInfo(T).@"struct".fields) |f| {
+ if (f.is_comptime or @sizeOf(f.type) == 0) continue;
+ n += countNodes(f.type, depth + 1);
+ }
+ }
+ return n;
+}
+
+fn fill(nodes: []Node, next: *usize, idx: usize, comptime T: type, name: []const u8, offset: usize, depth: u8, parent: u32) void {
+ const with_fields = hasFields(T, depth);
+ const count: u32 = if (with_fields) 6 else 5;
+ const first = next.*;
+ next.* += count;
+ nodes[idx] = .{ .name = name, .kind = .dir, .parent = parent, .first = @intCast(first), .count = count, .offset = offset, .size = @sizeOf(T) };
+ const me: u32 = @intCast(idx);
+ nodes[first + 0] = .{ .name = "value", .kind = .value, .parent = me, .offset = offset, .size = @sizeOf(T), .render = renderFor(T), .set = setFor(T) };
+ nodes[first + 1] = .{ .name = "type", .kind = .type_name, .parent = me, .offset = offset, .size = @sizeOf(T), .content = @typeName(T) };
+ nodes[first + 2] = .{ .name = "size", .kind = .size, .parent = me, .offset = offset, .size = @sizeOf(T), .content = std.fmt.comptimePrint("{d}", .{@sizeOf(T)}) };
+ nodes[first + 3] = .{ .name = "addr", .kind = .addr, .parent = me, .offset = offset, .size = @sizeOf(T) };
+ nodes[first + 4] = .{ .name = "raw", .kind = .raw, .parent = me, .offset = offset, .size = @sizeOf(T) };
+ if (with_fields) {
+ const fdir: u32 = @intCast(first + 5);
+ const nf = fieldCount(T);
+ const ffirst = next.*;
+ next.* += nf;
+ nodes[fdir] = .{ .name = "f", .kind = .fields, .parent = me, .first = @intCast(ffirst), .count = @intCast(nf), .offset = offset, .size = @sizeOf(T) };
+ var i: usize = 0;
+ for (@typeInfo(T).@"struct".fields) |f| {
+ if (f.is_comptime or @sizeOf(f.type) == 0) continue;
+ fill(nodes, next, ffirst + i, f.type, f.name, offset + @offsetOf(T, f.name), depth + 1, fdir);
+ i += 1;
+ }
+ }
+}
+
+fn buildTable(comptime T: type) [countNodes(T, 0)]Node {
+ @setEvalBranchQuota(1_000_000);
+ var nodes: [countNodes(T, 0)]Node = undefined;
+ var next: usize = 1;
+ fill(&nodes, &next, 0, T, "", 0, 0, 0);
+ std.debug.assert(next == nodes.len);
+ return nodes;
+}
+
+fn renderFor(comptime T: type) RenderFn {
+ return &struct {
+ fn f(base: [*]const u8, w: *Writer) Writer.Error!void {
+ const p: *const T = @ptrCast(@alignCast(base));
+ try render(p, w, max_depth);
+ }
+ }.f;
+}
+
+fn setFor(comptime T: type) ?SetFn {
+ if (!settable(T)) return null;
+ return &struct {
+ fn f(base: [*]u8, text: []const u8) SetError!void {
+ const p: *T = @ptrCast(@alignCast(base));
+ try set(p, text);
+ }
+ }.f;
+}
+
+fn settable(comptime T: type) bool {
+ return switch (@typeInfo(T)) {
+ .int, .float, .bool, .@"enum" => true,
+ else => false,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Rendering
+// ---------------------------------------------------------------------------
+
+/// Renders `ptr.*`. Structs become "field: value" lines (nested structs
+/// indented); everything else is a single line without a trailing newline.
+/// `depth` bounds struct/union/optional nesting; deeper values print as "…".
+pub fn render(ptr: anytype, w: *Writer, depth: usize) Writer.Error!void {
+ const T = @TypeOf(ptr.*);
+ if (comptime isPlainStruct(T)) {
+ try renderStruct(T, ptr, w, depth, 0);
+ } else {
+ try renderValue(T, ptr, w, depth);
+ }
+}
+
+fn isPlainStruct(comptime T: type) bool {
+ return switch (@typeInfo(T)) {
+ .@"struct" => |s| !s.is_tuple and s.fields.len > 0,
+ else => false,
+ };
+}
+
+/// The multi-line form: each field on its own line, nested structs indented.
+fn renderStruct(comptime T: type, ptr: *const T, w: *Writer, depth: usize, indent: usize) Writer.Error!void {
+ if (depth == 0) {
+ try w.splatByteAll(' ', indent);
+ try w.writeAll("…\n");
+ return;
+ }
+ const packed_layout = @typeInfo(T).@"struct".layout == .@"packed";
+ inline for (@typeInfo(T).@"struct".fields) |f| {
+ try w.splatByteAll(' ', indent);
+ try w.writeAll(f.name);
+ try w.writeByte(':');
+ if (comptime f.is_comptime) {
+ try w.writeAll(" (comptime)\n");
+ } else if (comptime packed_layout) {
+ // Fields of a packed struct have no byte address: render a copy.
+ const v = @field(ptr.*, f.name);
+ try w.writeByte(' ');
+ try renderValue(f.type, &v, w, depth - 1);
+ try w.writeByte('\n');
+ } else if (comptime isPlainStruct(f.type)) {
+ try w.writeByte('\n');
+ try renderStruct(f.type, &@field(ptr.*, f.name), w, depth - 1, indent + 2);
+ } else {
+ try w.writeByte(' ');
+ try renderValue(f.type, &@field(ptr.*, f.name), w, depth - 1);
+ try w.writeByte('\n');
+ }
+ }
+}
+
+/// The single-line form of any value.
+fn renderValue(comptime T: type, ptr: *const T, w: *Writer, depth: usize) Writer.Error!void {
+ switch (@typeInfo(T)) {
+ .int, .comptime_int => try w.print("{d}", .{ptr.*}),
+ .float, .comptime_float => try w.print("{d}", .{ptr.*}),
+ .bool => {
+ // The variable is live memory that anything (a debugger's /mem write,
+ // a torn update) may have corrupted: judge the byte, not the bool.
+ const b = @as(*const u8, @ptrCast(ptr)).*;
+ switch (b) {
+ 0 => try w.writeAll("false"),
+ 1 => try w.writeAll("true"),
+ else => try w.print("{d}", .{b}),
+ }
+ },
+ .void => try w.writeAll("{}"),
+ .@"enum" => |e| {
+ // Read the storage bytes as one integer: @tagName/switch on a corrupt
+ // value is a safety panic, and the value is caller memory we do not
+ // control. A load through the tag type would truncate to its bit
+ // width (a u2 tag in a byte), so the full storage width is read.
+ if (@sizeOf(T) == 0) return w.writeAll(e.fields[0].name);
+ const Raw = std.meta.Int(.unsigned, @sizeOf(T) * 8);
+ const raw = @as(*align(@alignOf(T)) const Raw, @ptrCast(ptr)).*;
+ const TagU = std.meta.Int(.unsigned, @bitSizeOf(e.tag_type));
+ const padding: Raw = if (@bitSizeOf(TagU) == @bitSizeOf(Raw)) 0 else ~@as(Raw, std.math.maxInt(TagU));
+ if (raw & padding == 0) {
+ const low: TagU = @truncate(raw);
+ inline for (e.fields) |f| {
+ if (low == @as(TagU, @bitCast(@as(e.tag_type, f.value)))) return w.writeAll(f.name);
+ }
+ }
+ try w.print("{d}", .{raw});
+ },
+ .error_set => try w.print("error.{s}", .{@errorName(ptr.*)}),
+ .error_union => |eu| if (ptr.*) |v| {
+ try renderValue(eu.payload, &v, w, depth);
+ } else |e| {
+ try w.print("error.{s}", .{@errorName(e)});
+ },
+ .optional => |o| if (ptr.*) |v| {
+ try renderValue(o.child, &v, w, depth);
+ } else {
+ try w.writeAll("null");
+ },
+ .pointer => |p| switch (p.size) {
+ .slice => if (p.child == u8) {
+ try renderString(ptr.*, w);
+ } else {
+ try renderElems(p.child, ptr.*, w, depth);
+ },
+ .many => if (p.child == u8 and p.sentinel() == 0) {
+ try renderCString(ptr.*, w);
+ } else {
+ try w.print("0x{x}", .{@intFromPtr(ptr.*)});
+ },
+ .one, .c => try w.print("0x{x}", .{@intFromPtr(ptr.*)}),
+ },
+ .array => |a| if (a.child == u8) {
+ try renderString(ptr.*[0..], w);
+ } else {
+ try renderElems(a.child, ptr.*[0..], w, depth);
+ },
+ .vector => |v| {
+ const arr: [v.len]v.child = ptr.*;
+ try renderElems(v.child, &arr, w, depth);
+ },
+ .@"struct" => |s| {
+ if (depth == 0) {
+ try w.writeAll("…");
+ return;
+ }
+ if (s.fields.len == 0) {
+ try w.writeAll("{}");
+ return;
+ }
+ try w.writeAll("{ ");
+ inline for (s.fields, 0..) |f, i| {
+ if (i != 0) try w.writeAll(", ");
+ if (!s.is_tuple) {
+ try w.writeAll(f.name);
+ try w.writeAll(": ");
+ }
+ if (comptime f.is_comptime) {
+ try w.writeAll("(comptime)");
+ } else if (comptime s.layout == .@"packed") {
+ const v = @field(ptr.*, f.name);
+ try renderValue(f.type, &v, w, depth - 1);
+ } else {
+ try renderValue(f.type, &@field(ptr.*, f.name), w, depth - 1);
+ }
+ }
+ try w.writeAll(" }");
+ },
+ .@"union" => |u| {
+ const Tag = u.tag_type orelse {
+ try w.print("(untagged union, {d} bytes)", .{@sizeOf(T)});
+ return;
+ };
+ if (depth == 0) {
+ try w.writeAll("…");
+ return;
+ }
+ // A switch on a corrupt tag is a safety panic: match the integer first.
+ const raw = @intFromEnum(@as(Tag, ptr.*));
+ inline for (u.fields) |f| {
+ if (raw == @intFromEnum(@field(Tag, f.name))) {
+ try w.writeAll(f.name);
+ if (f.type != void) {
+ try w.writeAll(": ");
+ try renderValue(f.type, &@field(ptr.*, f.name), w, depth - 1);
+ }
+ return;
+ }
+ }
+ try w.print("(invalid tag {d})", .{raw});
+ },
+ .@"fn" => try w.print("0x{x}", .{@intFromPtr(ptr)}),
+ else => try w.print("<{s}>", .{@typeName(T)}),
+ }
+}
+
+fn renderElems(comptime E: type, items: []const E, w: *Writer, depth: usize) Writer.Error!void {
+ try w.writeByte('[');
+ for (items, 0..) |*item, i| {
+ if (i == max_elems) {
+ try w.writeAll(", …");
+ break;
+ }
+ if (i != 0) try w.writeAll(", ");
+ try renderValue(E, item, w, depth);
+ }
+ try w.writeByte(']');
+}
+
+/// A NUL-terminated string, scanning at most `max_string` + 1 bytes for the
+/// terminator so that a missing one cannot walk off the end of the mapping.
+fn renderCString(s: [*:0]const u8, w: *Writer) Writer.Error!void {
+ var n: usize = 0;
+ while (n <= max_string and s[n] != 0) n += 1;
+ try renderString(s[0..n], w);
+}
+
+/// A double-quoted string with C-style escapes, truncated to `max_string` bytes.
+fn renderString(s: []const u8, w: *Writer) Writer.Error!void {
+ try w.writeByte('"');
+ for (s[0..@min(s.len, max_string)]) |b| switch (b) {
+ '\n' => try w.writeAll("\\n"),
+ '\r' => try w.writeAll("\\r"),
+ '\t' => try w.writeAll("\\t"),
+ '\\' => try w.writeAll("\\\\"),
+ '"' => try w.writeAll("\\\""),
+ ' '...'!', '#'...'[', ']'...'~' => try w.writeByte(b),
+ else => {
+ const hex = "0123456789abcdef";
+ try w.writeAll("\\x");
+ try w.writeByte(hex[b >> 4]);
+ try w.writeByte(hex[b & 15]);
+ },
+ };
+ try w.writeByte('"');
+ if (s.len > max_string) try w.writeAll("…");
+}
+
+// ---------------------------------------------------------------------------
+// Setting
+// ---------------------------------------------------------------------------
+
+/// Parses `text` and stores it into `ptr.*`: ints in decimal or 0x/0o/0b,
+/// floats, bools (true/false/1/0), enums by tag name (or by integer value for
+/// non-exhaustive enums). Other types are `error.Unsupported`.
+pub fn set(ptr: anytype, text: []const u8) SetError!void {
+ const T = @TypeOf(ptr.*);
+ const s = std.mem.trim(u8, text, " \t\r\n\x00");
+ switch (@typeInfo(T)) {
+ .int => ptr.* = std.fmt.parseInt(T, s, 0) catch return error.Invalid,
+ .float => ptr.* = std.fmt.parseFloat(T, s) catch return error.Invalid,
+ .bool => {
+ if (std.mem.eql(u8, s, "true") or std.mem.eql(u8, s, "1")) {
+ ptr.* = true;
+ } else if (std.mem.eql(u8, s, "false") or std.mem.eql(u8, s, "0")) {
+ ptr.* = false;
+ } else return error.Invalid;
+ },
+ .@"enum" => |e| {
+ if (std.meta.stringToEnum(T, s)) |v| {
+ ptr.* = v;
+ } else if (!e.is_exhaustive) {
+ const raw = std.fmt.parseInt(e.tag_type, s, 0) catch return error.Invalid;
+ ptr.* = @enumFromInt(raw);
+ } else return error.Invalid;
+ },
+ else => return error.Unsupported,
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+const testing = std.testing;
+
+fn renderToBuf(buf: []u8, ptr: anytype) ![]const u8 {
+ var w: Writer = .fixed(buf);
+ try render(ptr, &w, max_depth);
+ return w.buffered();
+}
+
+test "render scalars, strings, pointers, optionals, enums, arrays" {
+ var buf: [512]u8 = undefined;
+ const i: i32 = -42;
+ try testing.expectEqualStrings("-42", try renderToBuf(&buf, &i));
+ const f: f32 = 1.5;
+ try testing.expectEqualStrings("1.5", try renderToBuf(&buf, &f));
+ const b: bool = true;
+ try testing.expectEqualStrings("true", try renderToBuf(&buf, &b));
+ const s: []const u8 = "hi \"there\"\n";
+ try testing.expectEqualStrings("\"hi \\\"there\\\"\\n\"", try renderToBuf(&buf, &s));
+ const z: [*:0]const u8 = "zed";
+ try testing.expectEqualStrings("\"zed\"", try renderToBuf(&buf, &z));
+ const p: *const i32 = &i;
+ var expect_buf: [32]u8 = undefined;
+ const expect = try std.fmt.bufPrint(&expect_buf, "0x{x}", .{@intFromPtr(&i)});
+ try testing.expectEqualStrings(expect, try renderToBuf(&buf, &p));
+ const o: ?u8 = null;
+ try testing.expectEqualStrings("null", try renderToBuf(&buf, &o));
+ const o2: ?u8 = 7;
+ try testing.expectEqualStrings("7", try renderToBuf(&buf, &o2));
+ const E = enum { red, green };
+ const e: E = .green;
+ try testing.expectEqualStrings("green", try renderToBuf(&buf, &e));
+ const NE = enum(u8) { a, _ };
+ const ne: NE = @enumFromInt(9);
+ try testing.expectEqualStrings("9", try renderToBuf(&buf, &ne));
+ const arr = [_]u16{ 1, 2, 3 };
+ try testing.expectEqualStrings("[1, 2, 3]", try renderToBuf(&buf, &arr));
+ const bytes = [_]u8{ 0, 'a', 0xff };
+ try testing.expectEqualStrings("\"\\x00a\\xff\"", try renderToBuf(&buf, &bytes));
+ const U = union(enum) { none, some: u32 };
+ const u: U = .{ .some = 5 };
+ try testing.expectEqualStrings("some: 5", try renderToBuf(&buf, &u));
+ const un: U = .none;
+ try testing.expectEqualStrings("none", try renderToBuf(&buf, &un));
+}
+
+test "render structs multi-line with nested indentation and depth limit" {
+ const Inner = struct { x: f32, flags: [2]bool };
+ const Outer = struct { a: u32, b: bool, name: []const u8, inner: Inner, items: []const Inner };
+ const v: Outer = .{ .a = 1, .b = false, .name = "n", .inner = .{ .x = 2.5, .flags = .{ true, false } }, .items = &.{.{ .x = 0, .flags = .{ false, false } }} };
+ var buf: [512]u8 = undefined;
+ try testing.expectEqualStrings(
+ \\a: 1
+ \\b: false
+ \\name: "n"
+ \\inner:
+ \\ x: 2.5
+ \\ flags: [true, false]
+ \\items: [{ x: 0, flags: [false, false] }]
+ \\
+ , try renderToBuf(&buf, &v));
+ var w: Writer = .fixed(&buf);
+ try render(&v, &w, 1);
+ try testing.expectEqualStrings(
+ \\a: 1
+ \\b: false
+ \\name: "n"
+ \\inner:
+ \\ …
+ \\items: […]
+ \\
+ , w.buffered());
+}
+
+test "long strings and arrays are truncated" {
+ const long = [_]u8{'x'} ** 300;
+ var buf: [1024]u8 = undefined;
+ const s: []const u8 = &long;
+ const out = try renderToBuf(&buf, &s);
+ try testing.expectEqual(@as(usize, 1 + max_string + 1 + "…".len), out.len);
+ try testing.expect(std.mem.endsWith(u8, out, "\"…"));
+ const nums: [100]u32 = @splat(1);
+ const out2 = try renderToBuf(&buf, &nums);
+ try testing.expect(std.mem.endsWith(u8, out2, ", …]"));
+ try testing.expectEqual(@as(usize, max_elems), std.mem.count(u8, out2, "1"));
+}
+
+test "set parses ints, floats, bools and enums" {
+ var i: u32 = 0;
+ try set(&i, "42\n");
+ try testing.expectEqual(@as(u32, 42), i);
+ try set(&i, "0x10");
+ try testing.expectEqual(@as(u32, 16), i);
+ try testing.expectError(error.Invalid, set(&i, "-1"));
+ try testing.expectError(error.Invalid, set(&i, "abc"));
+ var si: i8 = 0;
+ try set(&si, " -7 ");
+ try testing.expectEqual(@as(i8, -7), si);
+ try testing.expectError(error.Invalid, set(&si, "200"));
+ var f: f64 = 0;
+ try set(&f, "2.25");
+ try testing.expectEqual(@as(f64, 2.25), f);
+ var b: bool = false;
+ try set(&b, "true");
+ try testing.expect(b);
+ try set(&b, "0");
+ try testing.expect(!b);
+ try testing.expectError(error.Invalid, set(&b, "maybe"));
+ const E = enum { off, on };
+ var e: E = .off;
+ try set(&e, "on");
+ try testing.expectEqual(E.on, e);
+ try testing.expectError(error.Invalid, set(&e, "blue"));
+ var s: []const u8 = "x";
+ try testing.expectError(error.Unsupported, set(&s, "y"));
+}
+
+test "vtable table layout for a nested struct" {
+ const Inner = struct { x: f32 };
+ const T = struct { a: u32, b: bool, name: []const u8, inner: Inner };
+ const vt = vtableFor(T);
+ try testing.expectEqual(vt, vtableFor(T));
+ try testing.expectEqualStrings(@typeName(T), vt.type_name);
+ const root = vt.nodes[0];
+ try testing.expect(root.isDir());
+ try testing.expectEqual(@as(u32, 6), root.count);
+ const value = vt.child(0, "value").?;
+ try testing.expect(!vt.nodes[value].writable()); // a struct is not settable
+ try testing.expectEqualStrings(std.fmt.comptimePrint("{d}", .{@sizeOf(T)}), vt.nodes[vt.child(0, "size").?].content);
+ const f = vt.child(0, "f").?;
+ try testing.expectEqual(Kind.fields, vt.nodes[f].kind);
+ try testing.expectEqual(@as(u32, 4), vt.nodes[f].count);
+ const a = vt.child(f, "a").?;
+ try testing.expectEqual(@offsetOf(T, "a"), vt.nodes[a].offset);
+ const a_value = vt.child(a, "value").?;
+ try testing.expect(vt.nodes[a_value].writable());
+ try testing.expectEqual(@as(usize, 4), vt.nodes[a_value].size);
+ try testing.expectEqual(a, vt.nodes[a_value].parent);
+ const inner = vt.child(f, "inner").?;
+ const inner_f = vt.child(inner, "f").?;
+ const x = vt.child(inner_f, "x").?;
+ try testing.expectEqual(@offsetOf(T, "inner") + @offsetOf(Inner, "x"), vt.nodes[x].offset);
+ try testing.expectEqualStrings("f32", vt.nodes[vt.child(x, "type").?].content);
+ try testing.expect(vt.child(f, "nope") == null);
+ // rendering and setting through the table
+ var v: T = .{ .a = 1, .b = true, .name = "n", .inner = .{ .x = 0.5 } };
+ const base: [*]u8 = @ptrCast(&v);
+ var buf: [256]u8 = undefined;
+ var w: Writer = .fixed(&buf);
+ const x_value = vt.child(x, "value").?;
+ try vt.nodes[x_value].render.?(base + vt.nodes[x_value].offset, &w);
+ try testing.expectEqualStrings("0.5", w.buffered());
+ try vt.nodes[a_value].set.?(base + vt.nodes[a_value].offset, "42");
+ try testing.expectEqual(@as(u32, 42), v.a);
+ w = .fixed(&buf);
+ try vt.nodes[value].render.?(base, &w);
+ try testing.expect(std.mem.startsWith(u8, w.buffered(), "a: 42\nb: true\n"));
+}
+
+test "depth limit stops the f/ tree at max_depth" {
+ const L4 = struct { v: u8 };
+ const L3 = struct { l4: L4 };
+ const L2 = struct { l3: L3 };
+ const L1 = struct { l2: L2 };
+ const L0 = struct { l1: L1 };
+ const vt = vtableFor(L0);
+ var node: u32 = 0;
+ var depth: usize = 0;
+ while (vt.child(node, "f")) |f| : (depth += 1) {
+ node = vt.nodes[f].first; // the single field
+ }
+ try testing.expectEqual(@as(usize, max_depth), depth);
+ try testing.expect(vt.child(node, "value") != null);
+}
+
+test "every @typeInfo category renders without dereferencing anything unbounded" {
+ var buf: [2048]u8 = undefined;
+ // packed and extern structs (packed fields have no address: rendered by copy)
+ const Packed = packed struct { a: u3, b: bool, c: u12, e: enum(u2) { p, q, r } };
+ const pk: Packed = .{ .a = 5, .b = true, .c = 300, .e = .r };
+ try testing.expectEqualStrings("a: 5\nb: true\nc: 300\ne: r\n", try renderToBuf(&buf, &pk));
+ const Ext = extern struct { x: u16, y: f32, inner: extern struct { z: u8 } };
+ const ex: Ext = .{ .x = 1, .y = 0.5, .inner = .{ .z = 9 } };
+ try testing.expectEqualStrings("x: 1\ny: 0.5\ninner:\n z: 9\n", try renderToBuf(&buf, &ex));
+ const Holder = struct { p: Packed, list: [2]Packed };
+ const ho: Holder = .{ .p = pk, .list = .{ pk, pk } };
+ try testing.expect(std.mem.startsWith(u8, try renderToBuf(&buf, &ho), "p:\n a: 5\n"));
+ // the f/ tree has no entries for a packed struct and works through a table
+ const vt = vtableFor(Packed);
+ try testing.expect(vt.child(0, "f") == null);
+ var w: Writer = .fixed(&buf);
+ try vt.nodes[vt.child(0, "value").?].render.?(@ptrCast(&pk), &w);
+ try testing.expect(std.mem.startsWith(u8, w.buffered(), "a: 5\n"));
+ // optionals of pointers are printed, never followed
+ var target: u32 = 7;
+ const op: ?*u32 = &target;
+ var expect_buf: [32]u8 = undefined;
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expect_buf, "0x{x}", .{@intFromPtr(&target)}), try renderToBuf(&buf, &op));
+ const np: ?*u32 = null;
+ try testing.expectEqualStrings("null", try renderToBuf(&buf, &np));
+ const dangling: *const u32 = @ptrFromInt(0x1000);
+ try testing.expectEqualStrings("0x1000", try renderToBuf(&buf, &dangling));
+ const cptr: [*c]const u8 = @ptrFromInt(0x2000);
+ try testing.expectEqualStrings("0x2000", try renderToBuf(&buf, &cptr));
+ const manyp: [*]const u32 = @ptrFromInt(0x3000);
+ try testing.expectEqualStrings("0x3000", try renderToBuf(&buf, &manyp));
+ // untagged and tagged unions, error unions, error sets
+ const Untagged = union { a: u32, b: f32 };
+ const un: Untagged = .{ .a = 1 };
+ try testing.expectEqualStrings(std.fmt.comptimePrint("(untagged union, {d} bytes)", .{@sizeOf(Untagged)}), try renderToBuf(&buf, &un));
+ const Tagged = union(enum(u8)) { none, some: u32, pair: struct { l: u8, r: u8 } };
+ const tg: Tagged = .{ .pair = .{ .l = 1, .r = 2 } };
+ try testing.expectEqualStrings("pair: { l: 1, r: 2 }", try renderToBuf(&buf, &tg));
+ const eu: anyerror!u8 = error.Boom;
+ try testing.expectEqualStrings("error.Boom", try renderToBuf(&buf, &eu));
+ const eu2: error{X}!u8 = 4;
+ try testing.expectEqualStrings("4", try renderToBuf(&buf, &eu2));
+ const es: anyerror = error.Zap;
+ try testing.expectEqualStrings("error.Zap", try renderToBuf(&buf, &es));
+ // wide ints and floats, vectors, sentinel arrays, slices of slices, void, comptime fields
+ const big: u128 = std.math.maxInt(u128);
+ try testing.expectEqualStrings("340282366920938463463374607431768211455", try renderToBuf(&buf, &big));
+ const neg: i128 = std.math.minInt(i128);
+ try testing.expectEqualStrings("-170141183460469231731687303715884105728", try renderToBuf(&buf, &neg));
+ const h: f16 = 1.5;
+ try testing.expectEqualStrings("1.5", try renderToBuf(&buf, &h));
+ const ld: f80 = 2.25;
+ try testing.expectEqualStrings("2.25", try renderToBuf(&buf, &ld));
+ const quad: f128 = 3.125;
+ try testing.expectEqualStrings("3.125", try renderToBuf(&buf, &quad));
+ const vec: @Vector(4, i16) = .{ 1, -2, 3, -4 };
+ try testing.expectEqualStrings("[1, -2, 3, -4]", try renderToBuf(&buf, &vec));
+ const sarr: [3:0]u8 = .{ 'a', 'b', 'c' };
+ try testing.expectEqualStrings("\"abc\"", try renderToBuf(&buf, &sarr));
+ const rows: []const []const u8 = &.{ "ab", "cd" };
+ try testing.expectEqualStrings("[\"ab\", \"cd\"]", try renderToBuf(&buf, &rows));
+ const Odd = struct { v: void, comptime k: u8 = 3, n: u8 };
+ const odd: Odd = .{ .v = {}, .n = 1 };
+ try testing.expectEqualStrings("v: {}\nk: (comptime)\nn: 1\n", try renderToBuf(&buf, &odd));
+ try testing.expectEqual(@as(u32, 1), vtableFor(Odd).nodes[vtableFor(Odd).child(0, "f").?].count);
+ // self-referential through a pointer: rendered as an address, table stays finite
+ const Link = struct { next: ?*const @This(), v: u8 };
+ var a: Link = .{ .next = null, .v = 1 };
+ const b: Link = .{ .next = &a, .v = 2 };
+ a.next = &b;
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expect_buf, "next: 0x{x}\nv: 2\n", .{@intFromPtr(&a)}), try renderToBuf(&buf, &b));
+ try testing.expect(vtableFor(Link).nodes.len < 32);
+ // tuples
+ const tup: struct { u8, []const u8 } = .{ 1, "x" };
+ try testing.expectEqualStrings("{ 1, \"x\" }", try renderToBuf(&buf, &tup));
+}
+
+test "corrupt live memory renders instead of trapping: enums, unions, bools" {
+ var buf: [128]u8 = undefined;
+ const E = enum(u8) { a, b };
+ var raw_e: u8 = 7;
+ try testing.expectEqualStrings("7", try renderToBuf(&buf, @as(*const E, @ptrCast(&raw_e))));
+ raw_e = 1;
+ try testing.expectEqualStrings("b", try renderToBuf(&buf, @as(*const E, @ptrCast(&raw_e))));
+ // a u2 tag in a byte: the whole byte is judged, not the truncated tag (ReleaseSafe would say "c")
+ const E3 = enum { a, b, c };
+ var raw3: u8 = 0xEE;
+ try testing.expectEqualStrings("238", try renderToBuf(&buf, @as(*const E3, @ptrCast(&raw3))));
+ raw3 = 3;
+ try testing.expectEqualStrings("3", try renderToBuf(&buf, @as(*const E3, @ptrCast(&raw3))));
+ raw3 = 2;
+ try testing.expectEqualStrings("c", try renderToBuf(&buf, @as(*const E3, @ptrCast(&raw3))));
+ const E12 = enum(u12) { p = 5, q = 4095 };
+ var raw12: u16 = 0xF005;
+ try testing.expectEqualStrings("61445", try renderToBuf(&buf, @as(*const E12, @ptrCast(&raw12))));
+ raw12 = 4095;
+ try testing.expectEqualStrings("q", try renderToBuf(&buf, @as(*const E12, @ptrCast(&raw12))));
+ const ES = enum(i8) { neg = -3, pos = 7 };
+ var raws: u8 = 0xFD;
+ try testing.expectEqualStrings("neg", try renderToBuf(&buf, @as(*const ES, @ptrCast(&raws))));
+ raws = 0x80;
+ try testing.expectEqualStrings("128", try renderToBuf(&buf, @as(*const ES, @ptrCast(&raws))));
+ const E1 = enum { only };
+ const e1: E1 = .only;
+ try testing.expectEqualStrings("only", try renderToBuf(&buf, &e1));
+ const NE = enum(u16) { x = 5, _ };
+ var raw_ne: u16 = 5;
+ try testing.expectEqualStrings("x", try renderToBuf(&buf, @as(*const NE, @ptrCast(&raw_ne))));
+ raw_ne = 6;
+ try testing.expectEqualStrings("6", try renderToBuf(&buf, @as(*const NE, @ptrCast(&raw_ne))));
+ var raw_b: u8 = 2;
+ try testing.expectEqualStrings("2", try renderToBuf(&buf, @as(*const bool, @ptrCast(&raw_b))));
+ const U = union(enum(u8)) { x: u32, y: bool };
+ var raw_u: [@sizeOf(U)]u8 align(@alignOf(U)) = @splat(0x55);
+ const out = try renderToBuf(&buf, @as(*const U, @ptrCast(&raw_u)));
+ try testing.expectEqualStrings("(invalid tag 85)", out);
+ const S = struct { e: E, u: U, b: bool };
+ var raw_s: [@sizeOf(S)]u8 align(@alignOf(S)) = @splat(0xEE);
+ const ps: *const S = @ptrCast(&raw_s);
+ _ = try renderToBuf(&buf, ps); // no trap
+ try testing.expect(std.mem.indexOf(u8, try renderToBuf(&buf, ps), "238") != null);
+}
+
+test "a [*:0]const u8 without a terminator is read at most max_string + 1 bytes" {
+ // Only the first max_string + 1 bytes exist; anything beyond is the
+ // testing allocator's guard, which a wider scan would touch.
+ const mem = try testing.allocator.alloc(u8, max_string + 1);
+ defer testing.allocator.free(mem);
+ @memset(mem, 'x');
+ const z: [*:0]const u8 = @ptrCast(mem.ptr);
+ var buf: [1024]u8 = undefined;
+ const out = try renderToBuf(&buf, &z);
+ try testing.expectEqual(@as(usize, 1 + max_string + 1 + "…".len), out.len);
+ try testing.expect(std.mem.endsWith(u8, out, "\"…"));
+ // exactly max_string bytes then NUL: no ellipsis
+ const mem2 = try testing.allocator.alloc(u8, max_string + 1);
+ defer testing.allocator.free(mem2);
+ @memset(mem2, 'y');
+ mem2[max_string] = 0;
+ const z2: [*:0]const u8 = @ptrCast(mem2.ptr);
+ const out2 = try renderToBuf(&buf, &z2);
+ try testing.expectEqual(@as(usize, 1 + max_string + 1), out2.len);
+ // a garbage-length []const u8 still reads at most max_string bytes
+ const garbage: []const u8 = mem[0..max_string];
+ _ = try renderToBuf(&buf, &garbage);
+}
+
+test "set rejects hostile input without partial writes" {
+ var u: u8 = 200;
+ for ([_][]const u8{ "-1", "256", "1e3", "0x", "", " ", "1.5", "+", "0b2", "١", "12abc", "0x100", "\x00", "1 2" }) |bad| {
+ try testing.expectError(error.Invalid, set(&u, bad));
+ try testing.expectEqual(@as(u8, 200), u);
+ }
+ try set(&u, "0b1111_1111");
+ try testing.expectEqual(@as(u8, 255), u);
+ try set(&u, "+0o17");
+ try testing.expectEqual(@as(u8, 15), u);
+ var i: i64 = 1;
+ try set(&i, "-9223372036854775808");
+ try testing.expectEqual(std.math.minInt(i64), i);
+ try testing.expectError(error.Invalid, set(&i, "9223372036854775808"));
+ var w: u128 = 0;
+ try set(&w, "340282366920938463463374607431768211455");
+ try testing.expectEqual(std.math.maxInt(u128), w);
+ try testing.expectError(error.Invalid, set(&w, "340282366920938463463374607431768211456"));
+ // floats: exponents, hex floats, inf/nan spellings, and junk
+ var f: f32 = 1;
+ try set(&f, "1.5e3");
+ try testing.expectEqual(@as(f32, 1500), f);
+ try set(&f, "-0x1p-2");
+ try testing.expectEqual(@as(f32, -0.25), f);
+ try set(&f, "1e999");
+ try testing.expect(std.math.isInf(f));
+ try testing.expectError(error.Invalid, set(&f, "1.5.5"));
+ try testing.expectError(error.Invalid, set(&f, "e5"));
+ try testing.expectError(error.Invalid, set(&f, ""));
+ var h: f16 = 0;
+ try set(&h, "65504");
+ try testing.expectEqual(@as(f16, 65504), h);
+ var q: f128 = 0;
+ try set(&q, "2.5");
+ try testing.expectEqual(@as(f128, 2.5), q);
+ // enums: NULs inside the tag, case, trailing junk; non-exhaustive by integer only when out of names
+ const E = enum(u8) { off, on };
+ var e: E = .off;
+ for ([_][]const u8{ "on\x00x", "On", "on x", "1", "0x1", "" }) |bad| {
+ try testing.expectError(error.Invalid, set(&e, bad));
+ try testing.expectEqual(E.off, e);
+ }
+ try set(&e, "\x00on\n");
+ try testing.expectEqual(E.on, e);
+ const NE = enum(u8) { a, _ };
+ var ne: NE = .a;
+ try set(&ne, "200");
+ try testing.expectEqual(@as(u8, 200), @intFromEnum(ne));
+ try testing.expectError(error.Invalid, set(&ne, "256"));
+ try testing.expectError(error.Invalid, set(&ne, "-1"));
+ try set(&ne, "a");
+ try testing.expectEqual(NE.a, ne);
+ // bools
+ var b: bool = true;
+ for ([_][]const u8{ "yes", "TRUE", "2", "", "01" }) |bad| {
+ try testing.expectError(error.Invalid, set(&b, bad));
+ try testing.expect(b);
+ }
+ // unsupported types are refused without touching memory
+ var opt: ?u8 = 3;
+ try testing.expectError(error.Unsupported, set(&opt, "4"));
+ try testing.expectEqual(@as(?u8, 3), opt);
+ var arr: [2]u8 = .{ 1, 2 };
+ try testing.expectError(error.Unsupported, set(&arr, "x"));
+ var un: union(enum) { a: u8 } = .{ .a = 1 };
+ try testing.expectError(error.Unsupported, set(&un, "a"));
+}