summaryrefslogtreecommitdiff
path: root/introspect/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 /introspect/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 'introspect/src/vars.zig')
-rw-r--r--introspect/src/vars.zig824
1 files changed, 0 insertions, 824 deletions
diff --git a/introspect/src/vars.zig b/introspect/src/vars.zig
deleted file mode 100644
index 20cadfc..0000000
--- a/introspect/src/vars.zig
+++ /dev/null
@@ -1,824 +0,0 @@
-//! 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"));
-}