From ba996acfcad1698adbf4a1834fe50e73b1c6cab9 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sat, 19 Sep 2026 23:28:22 -0300 Subject: 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 --- introspect/src/linux/provider.zig | 604 -------------------------------------- 1 file changed, 604 deletions(-) delete mode 100644 introspect/src/linux/provider.zig (limited to 'introspect/src/linux/provider.zig') diff --git a/introspect/src/linux/provider.zig b/introspect/src/linux/provider.zig deleted file mode 100644 index 9952398..0000000 --- a/introspect/src/linux/provider.zig +++ /dev/null @@ -1,604 +0,0 @@ -//! Adapts `debug.Debug` into core `Provider`s. The core mounts providers at -//! top level only, so one `DebugProvider` registers six of them, all sharing -//! the same `Debug` and the same snapshot pool: -//! -//! /threads//{name,stat,stack,regs} (lists only live tids) -//! /addr/ "fn\nfile:line:col\nmodule\n" -//! /mem/maps, /mem/ /proc/self/maps; raw bytes at address+offset (writable) -//! /hex/ hexdump of 256 bytes at the address -//! /breakpoints//{stack,regs,ctl} ctl accepts "continue" (lists only paused tids) -//! /panic/{message,stack,ctl} ctl accepts "continue" -//! -//! /addr, /mem, /hex and /panic carry a README; /threads and /breakpoints -//! list nothing but tids so that a shell glob over them sees only threads. -//! -//! Handles encode `(kind, tid-or-address)` in 56 bits (the core keeps the -//! low 56 bits of a handle for the qid path): kind in bits 48..55, value in -//! bits 0..47. Handles carry no reference count, so `clunk` is a no-op. -//! -//! The core hands providers a buffer-based `read`, not a writer, so every -//! text file is generated at `open` into one of `snapshot_slots` fixed slots -//! (keyed by handle, reference counted across fids) and served from there; a -//! read at offset 0 regenerates, like the core's own dynamic files. `/mem/` -//! is read and written directly at address+offset and never snapshotted, and -//! `/mem/maps` is read straight from /proc/self/maps at the requested offset -//! (a big process has more mappings than a snapshot slot holds). -const std = @import("std"); -const cloud9 = @import("cloud9"); -const core = @import("../core.zig"); -const debug = @import("debug.zig"); -const Writer = std.Io.Writer; -const Provider = core.Provider; -const Handle = Provider.Handle; -const Error = Provider.Error; -const NodeStat = core.NodeStat; - -pub const snapshot_slots = 8; -pub const snapshot_bytes = 32 * 1024; -/// Bytes shown by /hex/. -pub const hex_bytes = 256; - -pub const Tree = enum(u8) { threads, addr, mem, hex, breakpoints, panic }; -pub const tree_names = [_][]const u8{ "threads", "addr", "mem", "hex", "breakpoints", "panic" }; - -const Kind = enum(u8) { - root = 0, - readme, - thread_dir, - thread_name, - thread_stat, - thread_stack, - thread_regs, - addr_file, - maps, - mem_file, - hex_file, - bp_dir, - bp_stack, - bp_regs, - bp_ctl, - panic_message, - panic_stack, - panic_ctl, - - fn isDir(k: Kind) bool { - return k == .root or k == .thread_dir or k == .bp_dir; - } - - /// Text files generated into a snapshot slot at open. - fn isText(k: Kind) bool { - return switch (k) { - .readme, .thread_name, .thread_stat, .thread_stack, .thread_regs, .addr_file, .hex_file, .bp_stack, .bp_regs, .panic_message, .panic_stack => true, - else => false, - }; - } - - fn isCtl(k: Kind) bool { - return k == .bp_ctl or k == .panic_ctl; - } - - fn mode(k: Kind) u32 { - if (k.isDir()) return cloud9.dmdir | 0o555; - if (k.isCtl()) return 0o222; - if (k == .mem_file) return 0o666; - return 0o444; - } - - fn fixedName(k: Kind) ?[]const u8 { - return switch (k) { - .readme => "README", - .thread_name => "name", - .thread_stat => "stat", - .thread_stack, .bp_stack, .panic_stack => "stack", - .thread_regs, .bp_regs => "regs", - .maps => "maps", - .bp_ctl, .panic_ctl => "ctl", - .panic_message => "message", - else => null, - }; - } -}; - -const value_bits = 48; -const value_mask: u64 = (1 << value_bits) - 1; - -fn mk(kind: Kind, value: u64) Handle { - return (@as(u64, @intFromEnum(kind)) << value_bits) | (value & value_mask); -} - -fn kindOf(h: Handle) Kind { - return @enumFromInt(@as(u8, @truncate(h >> value_bits))); -} - -fn valueOf(h: Handle) u64 { - return h & value_mask; -} - -const readme_threads = - \\One directory per thread of this process, named by tid: - \\ name the thread's comm - \\ stat state and a few fields of /proc/self/task//stat - \\ stack "#n 0x in (::)" per frame - \\ regs general registers captured while the thread was stopped - \\ -; -const readme_addr = - \\Walk any hex address: /addr/ reads as "fn\nfile:line:col\nmodule\n". - \\ -; -const readme_mem = - \\maps /proc/self/maps - \\ raw process memory at that address (+ file offset); writable - \\ -; -const readme_hex = - \\Walk any hex address: /hex/ is a hexdump of the 256 bytes there. - \\ -; -const readme_breakpoints = - \\One directory per thread stopped in @breakpoint(), named by tid: - \\ stack, regs as under /threads - \\ ctl write "continue" to resume the thread - \\ -; -const readme_panic = - \\message the first panic's message (empty before any panic) - \\stack frames of the panicking thread - \\ctl write "continue" to let the default panic handler run - \\ -; - -fn readmeFor(tree: Tree) []const u8 { - return switch (tree) { - .threads => readme_threads, - .addr => readme_addr, - .mem => readme_mem, - .hex => readme_hex, - .breakpoints => readme_breakpoints, - .panic => readme_panic, - }; -} - -const Slot = struct { - handle: Handle = 0, - refs: u32 = 0, - len: u32 = 0, - buf: [snapshot_bytes]u8 = undefined, -}; - -pub const DebugProvider = struct { - d: *debug.Debug, - slots: [snapshot_slots]Slot = @splat(.{}), - /// Backs `NodeStat.name` until the next call. - name_buf: [32]u8 = undefined, - - pub fn init(dp: *DebugProvider, d: *debug.Debug) void { - dp.* = .{ .d = d }; - } - - /// The provider for one tree, to pass to `Shared.addProvider`. - pub fn provider(dp: *DebugProvider, comptime tree: Tree) Provider { - return .{ .name = tree_names[@intFromEnum(tree)], .ctx = dp, .vtable = vtableFor(tree) }; - } - - /// Mounts all six trees; `shared` is a `Server(cfg).Shared`. - pub fn mountAll(dp: *DebugProvider, shared: anytype) error{Full}!void { - inline for (comptime std.meta.tags(Tree)) |tree| try shared.addProvider(dp.provider(tree)); - } - - fn self(ctx: *anyopaque) *DebugProvider { - return @ptrCast(@alignCast(ctx)); - } - - fn vtableFor(comptime tree: Tree) *const Provider.VTable { - return &struct { - const vt: Provider.VTable = .{ - .walk = walkFn, - .stat = statFn, - .list = listFn, - .open = openFn, - .read = readFn, - .write = writeFn, - .close = closeFn, - .clunk = clunkFn, - }; - fn walkFn(ctx: *anyopaque, parent: Handle, name: []const u8) Error!Handle { - return self(ctx).walk(tree, parent, name); - } - fn statFn(ctx: *anyopaque, h: Handle, out: *NodeStat) Error!void { - return self(ctx).stat(tree, h, out); - } - fn listFn(ctx: *anyopaque, dir: Handle, index: usize, out: *NodeStat) Error!bool { - return self(ctx).list(tree, dir, index, out); - } - fn openFn(ctx: *anyopaque, h: Handle, mode: u8) Error!void { - return self(ctx).open(tree, h, mode); - } - fn readFn(ctx: *anyopaque, h: Handle, offset: u64, buf: []u8) Error!usize { - return self(ctx).read(tree, h, offset, buf); - } - fn writeFn(ctx: *anyopaque, h: Handle, offset: u64, data: []const u8) Error!usize { - return self(ctx).write(tree, h, offset, data); - } - fn closeFn(ctx: *anyopaque, h: Handle) void { - self(ctx).close(h); - } - fn clunkFn(_: *anyopaque, _: Handle) void {} - }.vt; - } - - // -- naming ------------------------------------------------------------ - - fn parseTid(name: []const u8) ?u32 { - if (name.len == 0 or name.len > 10) return null; - for (name) |ch| if (!std.ascii.isDigit(ch)) return null; - return std.fmt.parseInt(u32, name, 10) catch null; - } - - fn parseHex(name: []const u8) ?u64 { - const digits = if (std.mem.startsWith(u8, name, "0x")) name[2..] else name; - if (digits.len == 0 or digits.len > 12) return null; - for (digits) |ch| if (!std.ascii.isHex(ch)) return null; - const v = std.fmt.parseInt(u64, digits, 16) catch return null; - if (v > value_mask) return null; - return v; - } - - fn nodeName(dp: *DebugProvider, h: Handle) []const u8 { - const k = kindOf(h); - if (k.fixedName()) |n| return n; - return switch (k) { - .root => "", - .thread_dir, .bp_dir => std.fmt.bufPrint(&dp.name_buf, "{d}", .{valueOf(h)}) catch unreachable, - .addr_file, .mem_file, .hex_file => std.fmt.bufPrint(&dp.name_buf, "{x}", .{valueOf(h)}) catch unreachable, - else => unreachable, - }; - } - - // -- vtable ------------------------------------------------------------ - - fn walk(dp: *DebugProvider, tree: Tree, parent: Handle, name: []const u8) Error!Handle { - const k = kindOf(parent); - if (std.mem.eql(u8, name, ".")) return parent; - if (!k.isDir()) return error.NotDir; - if (std.mem.eql(u8, name, "..")) return Provider.root; - switch (k) { - .root => { - if (tree != .threads and tree != .breakpoints and std.mem.eql(u8, name, "README")) return mk(.readme, 0); - switch (tree) { - .threads => { - const tid = parseTid(name) orelse return error.NotFound; - if (!dp.d.threadExists(tid)) return error.NotFound; - return mk(.thread_dir, tid); - }, - .addr => return mk(.addr_file, parseHex(name) orelse return error.NotFound), - .mem => { - if (std.mem.eql(u8, name, "maps")) return mk(.maps, 0); - return mk(.mem_file, parseHex(name) orelse return error.NotFound); - }, - .hex => return mk(.hex_file, parseHex(name) orelse return error.NotFound), - .breakpoints => { - const tid = parseTid(name) orelse return error.NotFound; - if (!dp.d.isPaused(tid)) return error.NotFound; - return mk(.bp_dir, tid); - }, - .panic => { - if (std.mem.eql(u8, name, "message")) return mk(.panic_message, 0); - if (std.mem.eql(u8, name, "stack")) return mk(.panic_stack, 0); - if (std.mem.eql(u8, name, "ctl")) return mk(.panic_ctl, 0); - return error.NotFound; - }, - } - }, - .thread_dir => { - const tid = valueOf(parent); - if (std.mem.eql(u8, name, "name")) return mk(.thread_name, tid); - if (std.mem.eql(u8, name, "stat")) return mk(.thread_stat, tid); - if (std.mem.eql(u8, name, "stack")) return mk(.thread_stack, tid); - if (std.mem.eql(u8, name, "regs")) return mk(.thread_regs, tid); - return error.NotFound; - }, - .bp_dir => { - const tid = valueOf(parent); - if (std.mem.eql(u8, name, "stack")) return mk(.bp_stack, tid); - if (std.mem.eql(u8, name, "regs")) return mk(.bp_regs, tid); - if (std.mem.eql(u8, name, "ctl")) return mk(.bp_ctl, tid); - return error.NotFound; - }, - else => unreachable, - } - } - - fn fill(dp: *DebugProvider, tree: Tree, h: Handle, out: *NodeStat) void { - const k = kindOf(h); - out.* = .{ - .mode = k.mode(), - .length = if (k == .readme) readmeFor(tree).len else 0, - .name = dp.nodeName(h), - .handle = h, - }; - } - - fn stat(dp: *DebugProvider, tree: Tree, h: Handle, out: *NodeStat) Error!void { - dp.fill(tree, h, out); - } - - fn list(dp: *DebugProvider, tree: Tree, dir: Handle, index: usize, out: *NodeStat) Error!bool { - const k = kindOf(dir); - if (!k.isDir()) return error.NotDir; - const h: Handle = switch (k) { - .root => switch (tree) { - .threads => mk(.thread_dir, dp.d.threadAt(index) orelse return false), - .addr, .hex => if (index == 0) mk(.readme, 0) else return false, - .mem => switch (index) { - 0 => mk(.readme, 0), - 1 => mk(.maps, 0), - else => return false, - }, - .breakpoints => mk(.bp_dir, dp.d.pausedAt(index) orelse return false), - .panic => switch (index) { - 0 => mk(.readme, 0), - 1 => mk(.panic_message, 0), - 2 => mk(.panic_stack, 0), - 3 => mk(.panic_ctl, 0), - else => return false, - }, - }, - .thread_dir => switch (index) { - 0 => mk(.thread_name, valueOf(dir)), - 1 => mk(.thread_stat, valueOf(dir)), - 2 => mk(.thread_stack, valueOf(dir)), - 3 => mk(.thread_regs, valueOf(dir)), - else => return false, - }, - .bp_dir => switch (index) { - 0 => mk(.bp_stack, valueOf(dir)), - 1 => mk(.bp_regs, valueOf(dir)), - 2 => mk(.bp_ctl, valueOf(dir)), - else => return false, - }, - else => unreachable, - }; - dp.fill(tree, h, out); - return true; - } - - fn open(dp: *DebugProvider, tree: Tree, h: Handle, mode: u8) Error!void { - const k = kindOf(h); - const acc = mode & 3; - const wants_write = acc == cloud9.owrite or acc == cloud9.ordwr; - const wants_read = acc != cloud9.owrite; - if (k.isDir()) { - if (wants_write or mode & cloud9.otrunc != 0) return error.IsDir; - return; - } - if (k.isCtl()) { - if (wants_read) return error.Perm; - return; - } - if (k == .mem_file) return; - if (wants_write or mode & cloud9.otrunc != 0) return error.Perm; - if (k == .maps) return; - std.debug.assert(k.isText()); - const slot = dp.takeSlot(h) orelse return error.NoSpace; - errdefer dp.releaseSlot(slot); - try dp.generate(tree, h, slot); - } - - fn close(dp: *DebugProvider, h: Handle) void { - if (!kindOf(h).isText()) return; - if (dp.findSlot(h)) |s| dp.releaseSlot(s); - } - - fn read(dp: *DebugProvider, tree: Tree, h: Handle, offset: u64, buf: []u8) Error!usize { - const k = kindOf(h); - if (k.isDir()) return error.IsDir; - if (k.isCtl()) return error.Perm; - if (k == .mem_file) { - const addr = valueOf(h) +% offset; - return dp.d.readMem(addr, buf) catch |e| mapErr(e); - } - if (k == .maps) return dp.d.readMaps(offset, buf) catch |e| mapErr(e); - const slot = dp.findSlot(h) orelse return error.Io; - if (offset == 0) try dp.generate(tree, h, slot); - if (offset >= slot.len) return 0; - const off: usize = @intCast(offset); - const n = @min(buf.len, slot.len - off); - @memcpy(buf[0..n], slot.buf[off..][0..n]); - return n; - } - - fn write(dp: *DebugProvider, tree: Tree, h: Handle, offset: u64, data: []const u8) Error!usize { - _ = tree; - const k = kindOf(h); - if (k.isDir()) return error.IsDir; - switch (k) { - .mem_file => { - const addr = valueOf(h) +% offset; - return dp.d.writeMem(addr, data) catch |e| mapErr(e); - }, - .bp_ctl, .panic_ctl => { - const cmd = std.mem.trim(u8, data, " \t\r\n\x00"); - if (!std.mem.eql(u8, cmd, "continue")) return error.Unsupported; - if (k == .bp_ctl) { - dp.d.resumeThread(@intCast(valueOf(h))) catch |e| return mapErr(e); - } else { - dp.d.panicContinue() catch |e| return mapErr(e); - } - return data.len; - }, - else => return error.Perm, - } - } - - // -- snapshots --------------------------------------------------------- - - fn findSlot(dp: *DebugProvider, h: Handle) ?*Slot { - for (&dp.slots) |*s| if (s.refs > 0 and s.handle == h) return s; - return null; - } - - fn takeSlot(dp: *DebugProvider, h: Handle) ?*Slot { - if (dp.findSlot(h)) |s| { - s.refs += 1; - return s; - } - for (&dp.slots) |*s| if (s.refs == 0) { - s.* = .{ .handle = h, .refs = 1 }; - return s; - }; - return null; - } - - fn releaseSlot(_: *DebugProvider, s: *Slot) void { - s.refs -= 1; - } - - /// (Re)generates the text of `h` into `slot`. A text that does not fit is - /// truncated, not an error. - fn generate(dp: *DebugProvider, tree: Tree, h: Handle, slot: *Slot) Error!void { - var w: Writer = .fixed(&slot.buf); - slot.len = 0; - dp.render(tree, h, &w) catch |e| switch (e) { - error.WriteFailed => {}, - else => return mapErr(e), - }; - slot.len = @intCast(w.buffered().len); - } - - fn render(dp: *DebugProvider, tree: Tree, h: Handle, w: *Writer) debug.Error!void { - const d = dp.d; - const v = valueOf(h); - switch (kindOf(h)) { - .readme => w.writeAll(readmeFor(tree)) catch return error.WriteFailed, - .thread_name => try d.threadName(@intCast(v), w), - .thread_stat => try d.threadStat(@intCast(v), w), - .thread_stack => try d.threadStack(@intCast(v), w), - .thread_regs => try d.threadRegs(@intCast(v), w), - .addr_file => try d.resolveAddr(@intCast(v), w), - .hex_file => try d.hexdump(@intCast(v), hex_bytes, w), - .bp_stack => try d.pausedStack(@intCast(v), w), - .bp_regs => try d.pausedRegs(@intCast(v), w), - .panic_message => try d.panicMessage(w), - .panic_stack => try d.panicStack(w), - else => unreachable, - } - } - - fn mapErr(e: debug.Error) Error { - return switch (e) { - error.NoThread, error.NotPaused, error.NoPanic => error.NotFound, - error.Unsupported => error.Unsupported, - error.WriteFailed => error.NoSpace, - error.Timeout, error.Busy, error.Unmapped, error.Unexpected, error.AlreadyInitialized, error.InvalidOptions => error.Io, - }; - } -}; - -// --------------------------------------------------------------------------- -// Tests (through the core's in-memory harness) -// --------------------------------------------------------------------------- - -const testing = std.testing; - -const TestCfg: core.Config = .{ .name = "dbgtest", .msize = 8192, .max_fids = 16, .max_providers = 6, .snapshot_slots = 2, .snapshot_bytes = 512 }; -const TS = core.Server(TestCfg); - -test "debug provider: threads, addr, mem, hex, breakpoints, panic through the core" { - var text_buf: [16 * 1024]u8 = undefined; - var d: debug.Debug = undefined; - try d.init(.{ .io = testing.io, .text_buf = &text_buf }); - defer d.deinit(); - var dp: DebugProvider = undefined; - dp.init(&d); - - var dummy: u8 = 0; - var shared: TS.Shared = .init(&dummy); - try dp.mountAll(&shared); - var storage: TS.Storage = undefined; - var h: TS.Harness = undefined; - try h.init(&shared, &storage); - defer h.deinit(); - - // /threads lists tids only, among them this thread. - const names = try h.listPath(&.{"threads"}); - defer TS.Harness.freeNames(names); - try testing.expect(names.len >= 1); - try testing.expect(!TS.Harness.hasName(names, "README")); - const no_readme = try h.ok(.{ .walk = .{ .fid = 0, .newfid = 7, .names = &.{ "threads", "README" } } }); - try testing.expectEqual(@as(u16, 1), no_readme.walk.nwqid); - var tid_buf: [16]u8 = undefined; - const tid = try std.fmt.bufPrint(&tid_buf, "{d}", .{std.os.linux.gettid()}); - try testing.expect(TS.Harness.hasName(names, tid)); - - // Own stack names this test function's file. - const stack = try h.readPath(&.{ "threads", tid, "stack" }); - defer testing.allocator.free(stack); - try testing.expect(std.mem.indexOf(u8, stack, "#0 0x") != null); - - // /addr/ of a function here resolves to this file. - var addr_buf: [32]u8 = undefined; - const addr_name = try std.fmt.bufPrint(&addr_buf, "{x}", .{@intFromPtr(&DebugProvider.parseTid)}); - const resolved = try h.readPath(&.{ "addr", addr_name }); - defer testing.allocator.free(resolved); - try testing.expect(std.mem.indexOf(u8, resolved, "provider.zig") != null); - const partial = try h.ok(.{ .walk = .{ .fid = 0, .newfid = 5, .names = &.{ "addr", "zzz" } } }); - try testing.expectEqual(@as(u16, 1), partial.walk.nwqid); - try h.walkTo(5, &.{"addr"}); - try h.expectFail(.{ .walk = .{ .fid = 5, .newfid = 6, .names = &.{"zzz"} } }, "file does not exist"); - _ = try h.ok(.{ .clunk = .{ .fid = 5 } }); - - // /mem/ reads and writes live memory; unmapped is an error. - var cell: [8]u8 = "abcdefgh".*; - var mem_buf: [32]u8 = undefined; - const mem_name = try std.fmt.bufPrint(&mem_buf, "{x}", .{@intFromPtr(&cell)}); - try h.walkTo(1, &.{ "mem", mem_name }); - _ = try h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } }); - const r = try h.ok(.{ .read = .{ .fid = 1, .offset = 2, .count = 4 } }); - try testing.expectEqualStrings("cdef", r.read); - _ = try h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "XY" } }); - try testing.expectEqualStrings("XYcdefgh", &cell); - _ = try h.ok(.{ .clunk = .{ .fid = 1 } }); - try h.walkTo(2, &.{ "mem", "8" }); - _ = try h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - try h.expectFail(.{ .read = .{ .fid = 2, .offset = 0, .count = 4 } }, "i/o error"); - _ = try h.ok(.{ .clunk = .{ .fid = 2 } }); - const maps = try h.readPath(&.{ "mem", "maps" }); - defer testing.allocator.free(maps); - try testing.expect(std.mem.indexOf(u8, maps, "r-xp") != null or std.mem.indexOf(u8, maps, "r--p") != null); - - // /hex/ is a hexdump. - const hex = try h.readPath(&.{ "hex", mem_name }); - defer testing.allocator.free(hex); - try testing.expect(std.mem.indexOf(u8, hex, "XYcdefgh") != null); - - // Nothing paused, no panic. - const bps = try h.listPath(&.{"breakpoints"}); - defer TS.Harness.freeNames(bps); - try testing.expectEqual(@as(usize, 0), bps.len); - const msg = try h.readPath(&.{ "panic", "message" }); - defer testing.allocator.free(msg); - try testing.expectEqualStrings("", msg); - try h.walkTo(3, &.{ "panic", "ctl" }); - _ = try h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.owrite } }); - try h.expectFail(.{ .write = .{ .fid = 3, .offset = 0, .data = "continue" } }, "file does not exist"); - try h.expectFail(.{ .write = .{ .fid = 3, .offset = 0, .data = "bogus" } }, "not supported"); - _ = try h.ok(.{ .clunk = .{ .fid = 3 } }); - - // Snapshot slots are released on clunk: open more files than slots, sequentially. - for (0..4) |_| { - const t = try h.readPath(&.{ "threads", tid, "name" }); - testing.allocator.free(t); - } - for (&dp.slots) |s| try testing.expectEqual(@as(u32, 0), s.refs); -} - -test "handle encoding round-trips" { - const h = mk(.mem_file, 0x7fff_dead_beef); - try testing.expectEqual(Kind.mem_file, kindOf(h)); - try testing.expectEqual(@as(u64, 0x7fff_dead_beef), valueOf(h)); - try testing.expect(h < (1 << 56)); - try testing.expectEqual(@as(?u64, null), DebugProvider.parseHex("1_0")); - try testing.expectEqual(@as(?u64, 0x10), DebugProvider.parseHex("0x10")); - try testing.expectEqual(@as(?u32, null), DebugProvider.parseTid("+5")); -} -- cgit v1.3