tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

io_driver.zig (69578B)


      1 const std = @import("std");
      2 const Arena = @import("arena.zig").Arena;
      3 const codecs = @import("codecs.zig");
      4 const source_text = @import("source_text.zig");
      5 const indexed = @import("indexed.zig");
      6 const reduce = @import("reduce.zig");
      7 const tree = @import("tree.zig");
      8 
      9 const c = @cImport({
     10     // Import declarations, not glibc's fortified C inline wrappers. With the
     11     // pinned Zig/glibc, optimized translate-c diagnoses the variadic open/openat
     12     // wrapper bodies even though this driver calls uv_fs_open, not libc open.
     13     // This is local to header translation: libuv and C callers retain their
     14     // normal build hardening, and Zig ReleaseSafe retains runtime checks.
     15     @cUndef("_FORTIFY_SOURCE");
     16     @cDefine("_FORTIFY_SOURCE", "0");
     17     @cInclude("uv.h");
     18 });
     19 
     20 // ---------------------------------------------------------------------------
     21 // Action tag constants (must match lib/io.tri and IODriver.hs)
     22 // ---------------------------------------------------------------------------
     23 
     24 pub const ActionTag = enum(u8) {
     25     pure = 0,
     26     bind = 1,
     27     putStr = 10,
     28     getLine = 11,
     29     readFile = 20,
     30     readSourceFile = 37,
     31     encodeSourceText = 38,
     32     readIndexedTree = 50,
     33     hashIndexedTree = 51,
     34     writeIndexedTreeIfAbsent = 52,
     35     writeFile = 21,
     36     writeBytes = 22,
     37     renameFile = 24,
     38     createDirectory = 25,
     39     fileExists = 27,
     40     sha256Hex = 28,
     41     writeBytesIfAbsent = 32,
     42     atomicWriteBytes = 33,
     43     ask = 30,
     44     local = 31,
     45     get = 40,
     46     put = 41,
     47     fork = 60,
     48     await = 61,
     49     yield = 62,
     50     sleep = 63,
     51 };
     52 
     53 const FileWrite = struct { path: u32, contents: u32 };
     54 
     55 pub const Action = union(ActionTag) {
     56     pure: u32,
     57     bind: struct { left: u32, k: u32 },
     58     putStr: u32,
     59     getLine,
     60     readFile: u32,
     61     readSourceFile: u32,
     62     encodeSourceText: u32,
     63     readIndexedTree: struct { path: u32, digest: u32 },
     64     hashIndexedTree: u32,
     65     writeIndexedTreeIfAbsent: struct { path: u32, digest: u32, tree: u32 },
     66     writeFile: FileWrite,
     67     writeBytes: FileWrite,
     68     renameFile: struct { old: u32, new: u32 },
     69     createDirectory: u32,
     70     fileExists: u32,
     71     sha256Hex: u32,
     72     writeBytesIfAbsent: FileWrite,
     73     atomicWriteBytes: FileWrite,
     74     ask,
     75     local: struct { f: u32, action: u32 },
     76     get,
     77     put: u32,
     78     fork: u32,
     79     await: u32,
     80     yield,
     81     sleep: u32,
     82 };
     83 
     84 // ---------------------------------------------------------------------------
     85 // Error codes (must match IODriver.hs)
     86 // ---------------------------------------------------------------------------
     87 
     88 const ERR_DOES_NOT_EXIST: u64 = 1;
     89 const ERR_PERMISSION: u64 = 2;
     90 const ERR_ALREADY_EXISTS: u64 = 3;
     91 const ERR_IO_OTHER: u64 = 4;
     92 const ERR_POLICY_DENY: u64 = 20;
     93 const ERR_INVALID_ACTION: u64 = 40;
     94 const ERR_INVALID_STRING: u64 = 41;
     95 
     96 // ---------------------------------------------------------------------------
     97 // Permissions
     98 // ---------------------------------------------------------------------------
     99 
    100 pub const IOPerms = struct {
    101     allow_read_all: bool = false,
    102     allow_write_all: bool = false,
    103 };
    104 
    105 // ---------------------------------------------------------------------------
    106 // IO sentinel detection
    107 // ---------------------------------------------------------------------------
    108 
    109 pub fn isIOSentinel(arena: *Arena, root: u32) !?u32 {
    110     const node = arena.get(try reduce.reduce(root, arena, 1_000_000)).*;
    111     if (node != .fork) return null;
    112 
    113     const sentinel = node.fork.left;
    114     const rest = node.fork.right;
    115 
    116     const sentinel_str = try codecs.toString(arena, sentinel);
    117     defer {
    118         if (sentinel_str) |s| {
    119             arena.allocator.free(s);
    120         }
    121     }
    122     if (sentinel_str == null) return null;
    123     if (!std.mem.eql(u8, sentinel_str.?, "tricuIO")) return null;
    124 
    125     const rest_node = arena.get(try reduce.reduce(rest, arena, 1_000_000)).*;
    126     if (rest_node != .fork) return null;
    127 
    128     const version_num = try codecs.toNumber(arena, rest_node.fork.left);
    129     if (version_num == null or version_num.? != 1) return null;
    130 
    131     return rest_node.fork.right;
    132 }
    133 
    134 // ---------------------------------------------------------------------------
    135 // Action decoding
    136 // ---------------------------------------------------------------------------
    137 
    138 pub fn decodeAction(arena: *Arena, root: u32) !?Action {
    139     const node = arena.get(root).*;
    140     if (node != .fork) return null;
    141 
    142     const tag_num = try codecs.toNumber(arena, node.fork.left);
    143     if (tag_num == null) return null;
    144 
    145     const tag: ActionTag = switch (tag_num.?) {
    146         0 => .pure,
    147         1 => .bind,
    148         10 => .putStr,
    149         11 => .getLine,
    150         20 => .readFile,
    151         37 => .readSourceFile,
    152         38 => .encodeSourceText,
    153         50 => .readIndexedTree,
    154         51 => .hashIndexedTree,
    155         52 => .writeIndexedTreeIfAbsent,
    156         21 => .writeFile,
    157         22 => .writeBytes,
    158         24 => .renameFile,
    159         25 => .createDirectory,
    160         27 => .fileExists,
    161         28 => .sha256Hex,
    162         32 => .writeBytesIfAbsent,
    163         33 => .atomicWriteBytes,
    164         30 => .ask,
    165         31 => .local,
    166         40 => .get,
    167         41 => .put,
    168         60 => .fork,
    169         61 => .await,
    170         62 => .yield,
    171         63 => .sleep,
    172         else => return null,
    173     };
    174 
    175     const payload = node.fork.right;
    176 
    177     return switch (tag) {
    178         .pure => Action{ .pure = payload },
    179         .bind => blk: {
    180             const payload_node = arena.get(try reduce.reduce(payload, arena, 1_000_000));
    181             if (payload_node.* != .fork) return null;
    182             break :blk Action{ .bind = .{ .left = payload_node.fork.left, .k = payload_node.fork.right } };
    183         },
    184         .putStr => Action{ .putStr = payload },
    185         .getLine => Action.getLine,
    186         .readFile => Action{ .readFile = payload },
    187         .readSourceFile => Action{ .readSourceFile = payload },
    188         .encodeSourceText => Action{ .encodeSourceText = payload },
    189         .hashIndexedTree => Action{ .hashIndexedTree = payload },
    190         .readIndexedTree, .writeIndexedTreeIfAbsent => blk: {
    191             const pair = arena.get(try reduce.reduce(payload, arena, 1_000_000)).*;
    192             if (pair != .fork) return null;
    193             if (tag == .readIndexedTree) break :blk .{ .readIndexedTree = .{ .path = pair.fork.left, .digest = pair.fork.right } };
    194             const rest = arena.get(try reduce.reduce(pair.fork.right, arena, 1_000_000)).*;
    195             if (rest != .fork) return null;
    196             break :blk .{ .writeIndexedTreeIfAbsent = .{ .path = pair.fork.left, .digest = rest.fork.left, .tree = rest.fork.right } };
    197         },
    198         .writeFile, .writeBytes, .writeBytesIfAbsent, .atomicWriteBytes, .renameFile => blk: {
    199             const pair = arena.get(try reduce.reduce(payload, arena, 1_000_000));
    200             if (pair.* != .fork) return null;
    201             break :blk switch (tag) {
    202                 .writeFile => Action{ .writeFile = .{ .path = pair.fork.left, .contents = pair.fork.right } },
    203                 .writeBytes => Action{ .writeBytes = .{ .path = pair.fork.left, .contents = pair.fork.right } },
    204                 .writeBytesIfAbsent => Action{ .writeBytesIfAbsent = .{ .path = pair.fork.left, .contents = pair.fork.right } },
    205                 .atomicWriteBytes => Action{ .atomicWriteBytes = .{ .path = pair.fork.left, .contents = pair.fork.right } },
    206                 .renameFile => Action{ .renameFile = .{ .old = pair.fork.left, .new = pair.fork.right } },
    207                 else => unreachable,
    208             };
    209         },
    210         .createDirectory => Action{ .createDirectory = payload },
    211         .fileExists => Action{ .fileExists = payload },
    212         .sha256Hex => Action{ .sha256Hex = payload },
    213         .ask => Action.ask,
    214         .local => blk: {
    215             const payload_node = arena.get(try reduce.reduce(payload, arena, 1_000_000));
    216             if (payload_node.* != .fork) return null;
    217             break :blk Action{ .local = .{ .f = payload_node.fork.left, .action = payload_node.fork.right } };
    218         },
    219         .get => Action.get,
    220         .put => Action{ .put = payload },
    221         .fork => Action{ .fork = payload },
    222         .await => Action{ .await = payload },
    223         .yield => Action.yield,
    224         .sleep => Action{ .sleep = payload },
    225     };
    226 }
    227 
    228 // ---------------------------------------------------------------------------
    229 // Response tree constructors
    230 // ---------------------------------------------------------------------------
    231 
    232 pub fn makePure(arena: *Arena, val: u32) !u32 {
    233     const tag = try codecs.ofNumber(arena, 0);
    234     return try arena.alloc(.{ .fork = .{ .left = tag, .right = val } });
    235 }
    236 
    237 pub fn makeOkResult(arena: *Arena, val: u32) !u32 {
    238     const ok_tag = try arena.alloc(.{ .stem = .{ .child = try arena.alloc(.leaf) } });
    239     const val_pair = try arena.alloc(.{ .fork = .{ .left = val, .right = try arena.alloc(.leaf) } });
    240     return try arena.alloc(.{ .fork = .{ .left = ok_tag, .right = val_pair } });
    241 }
    242 
    243 pub fn makeErrString(arena: *Arena, message: []const u8) !u32 {
    244     return makeErrValue(arena, try codecs.ofString(arena, message));
    245 }
    246 
    247 fn makeErrValue(arena: *Arena, text: u32) !u32 {
    248     const value = try arena.alloc(.{ .fork = .{ .left = text, .right = try arena.alloc(.leaf) } });
    249     return try arena.alloc(.{ .fork = .{ .left = try arena.alloc(.leaf), .right = value } });
    250 }
    251 
    252 fn sourceReadError(arena: *Arena, path: []const u8, reason: []const u8) !u32 {
    253     const message = try std.fmt.allocPrint(arena.allocator, "{s}: {s}", .{ path, reason });
    254     defer arena.allocator.free(message);
    255     return makeErrValue(arena, try source_text.fromUtf8(arena, message));
    256 }
    257 
    258 fn sourceReadResult(arena: *Arena, path: []const u8, bytes: []const u8) !u32 {
    259     const scalars = source_text.fromUtf8(arena, bytes) catch |err| switch (err) {
    260         error.InvalidUtf8 => return sourceReadError(arena, path, "invalid UTF-8 source"),
    261         else => return err,
    262     };
    263     if (std.mem.startsWith(u8, bytes, "\xef\xbb\xbf"))
    264         return sourceReadError(arena, path, "UTF-8 source BOM is not permitted");
    265     const raw = try codecs.ofBytes(arena, bytes);
    266     const pair = try arena.alloc(.{ .fork = .{ .left = raw, .right = scalars } });
    267     return makeOkResult(arena, pair);
    268 }
    269 
    270 pub fn makeErrResult(arena: *Arena, code: u64) !u32 {
    271     // The portable IO protocol carries text errors, matching the Haskell
    272     // driver; numeric values remain internal filesystem status codes.
    273     return makeErrString(arena, fsErrorMessage(code));
    274 }
    275 
    276 // A null callback makes libuv filesystem operations synchronous. Each
    277 // request is cleaned up; temp files live in the destination directory so
    278 // both link (no replace) and rename (replace) are atomic on that filesystem.
    279 const FsWriteResult = union(enum) { written: bool, failure: u64 };
    280 
    281 const FsReadResult = union(enum) { bytes: []u8, failure: u64 };
    282 
    283 fn readFs(loop: *c.uv_loop_t, gpa: std.mem.Allocator, path: [:0]const u8) !FsReadResult {
    284     var req: c.uv_fs_t = undefined;
    285     const fd = c.uv_fs_open(loop, &req, path.ptr, c.O_RDONLY, 0, null);
    286     c.uv_fs_req_cleanup(&req);
    287     if (fd < 0) return .{ .failure = mapUvErr(fd) };
    288     defer {
    289         _ = c.uv_fs_close(loop, &req, fd, null);
    290         c.uv_fs_req_cleanup(&req);
    291     }
    292     var bytes: std.ArrayList(u8) = .empty;
    293     defer bytes.deinit(gpa);
    294     var buffer: [4096]u8 = undefined;
    295     var uv_buffer = c.uv_buf_init(&buffer, buffer.len);
    296     while (true) {
    297         const count = c.uv_fs_read(loop, &req, fd, &uv_buffer, 1, -1, null);
    298         c.uv_fs_req_cleanup(&req);
    299         if (count < 0) return .{ .failure = mapUvErr(count) };
    300         if (count == 0) return .{ .bytes = try bytes.toOwnedSlice(gpa) };
    301         try bytes.appendSlice(gpa, buffer[0..@intCast(count)]);
    302     }
    303 }
    304 
    305 fn uvFileError(rc: c_int) u64 {
    306     return mapUvErr(rc);
    307 }
    308 
    309 fn syncFsPath(loop: *c.uv_loop_t, path: [*:0]const u8, directory: bool) ?u64 {
    310     var open_req: c.uv_fs_t = undefined;
    311     const fd = c.uv_fs_open(loop, &open_req, path, c.O_RDONLY | (if (directory) @as(c_int, c.O_DIRECTORY) else 0), 0, null);
    312     c.uv_fs_req_cleanup(&open_req);
    313     if (fd < 0) return uvFileError(fd);
    314     var sync_req: c.uv_fs_t = undefined;
    315     const synced = c.uv_fs_fsync(loop, &sync_req, @intCast(fd), null);
    316     c.uv_fs_req_cleanup(&sync_req);
    317     var close_req: c.uv_fs_t = undefined;
    318     const closed = c.uv_fs_close(loop, &close_req, @intCast(fd), null);
    319     c.uv_fs_req_cleanup(&close_req);
    320     if (synced < 0) return uvFileError(synced);
    321     if (closed < 0) return uvFileError(closed);
    322     return null;
    323 }
    324 
    325 fn syncFsParent(loop: *c.uv_loop_t, gpa: std.mem.Allocator, path: []const u8) !?u64 {
    326     const parent = std.fs.path.dirname(path) orelse ".";
    327     const parent_z = try gpa.dupeZ(u8, parent);
    328     defer gpa.free(parent_z);
    329     return syncFsPath(loop, parent_z, true);
    330 }
    331 
    332 fn writeAtomicFs(loop: *c.uv_loop_t, gpa: std.mem.Allocator, path: [:0]const u8, contents: []const u8, exclusive: bool) !FsWriteResult {
    333     const template = try std.fmt.allocPrintSentinel(gpa, "{s}.tmp.XXXXXX", .{path}, 0);
    334     defer gpa.free(template);
    335     var req: c.uv_fs_t = undefined;
    336     const fd = c.uv_fs_mkstemp(loop, &req, template.ptr, null);
    337     if (fd < 0) {
    338         c.uv_fs_req_cleanup(&req);
    339         return .{ .failure = uvFileError(fd) };
    340     }
    341     const tmp = gpa.dupeZ(u8, std.mem.span(req.path)) catch |err| {
    342         var close_req: c.uv_fs_t = undefined;
    343         _ = c.uv_fs_close(loop, &close_req, fd, null);
    344         c.uv_fs_req_cleanup(&close_req);
    345         var unlink_req: c.uv_fs_t = undefined;
    346         _ = c.uv_fs_unlink(loop, &unlink_req, req.path, null);
    347         c.uv_fs_req_cleanup(&unlink_req);
    348         c.uv_fs_req_cleanup(&req);
    349         return err;
    350     };
    351     c.uv_fs_req_cleanup(&req);
    352     defer gpa.free(tmp);
    353     defer {
    354         var unlink_req: c.uv_fs_t = undefined;
    355         _ = c.uv_fs_unlink(loop, &unlink_req, tmp.ptr, null);
    356         c.uv_fs_req_cleanup(&unlink_req);
    357     }
    358     var offset: usize = 0;
    359     var failure: ?u64 = null;
    360     while (offset < contents.len) {
    361         const remain = contents[offset..];
    362         var buf = c.uv_buf_init(@ptrCast(@constCast(remain.ptr)), @intCast(@min(remain.len, std.math.maxInt(c_uint))));
    363         var write_req: c.uv_fs_t = undefined;
    364         const n = c.uv_fs_write(loop, &write_req, fd, &buf, 1, @intCast(offset), null);
    365         c.uv_fs_req_cleanup(&write_req);
    366         if (n <= 0) {
    367             failure = if (n == 0) ERR_IO_OTHER else uvFileError(n);
    368             break;
    369         }
    370         offset += @intCast(n);
    371     }
    372     var close_req: c.uv_fs_t = undefined;
    373     const closed = c.uv_fs_close(loop, &close_req, fd, null);
    374     c.uv_fs_req_cleanup(&close_req);
    375     if (failure) |err| return .{ .failure = err };
    376     if (closed < 0) return .{ .failure = uvFileError(closed) };
    377     if (syncFsPath(loop, tmp.ptr, false)) |err| return .{ .failure = err };
    378     var publish_req: c.uv_fs_t = undefined;
    379     const published = if (exclusive)
    380         c.uv_fs_link(loop, &publish_req, tmp.ptr, path.ptr, null)
    381     else
    382         c.uv_fs_rename(loop, &publish_req, tmp.ptr, path.ptr, null);
    383     c.uv_fs_req_cleanup(&publish_req);
    384     if (published == c.UV_EEXIST and exclusive) {
    385         if (syncFsPath(loop, path.ptr, false)) |err| return .{ .failure = err };
    386         if (try syncFsParent(loop, gpa, path)) |err| return .{ .failure = err };
    387         return .{ .written = false };
    388     }
    389     if (published < 0) return .{ .failure = uvFileError(published) };
    390     if (try syncFsParent(loop, gpa, path)) |err| return .{ .failure = err };
    391     return .{ .written = true };
    392 }
    393 
    394 fn fsErrorMessage(err: u64) []const u8 {
    395     return switch (err) {
    396         ERR_DOES_NOT_EXIST => "does not exist",
    397         ERR_PERMISSION, ERR_POLICY_DENY => "permission denied",
    398         ERR_ALREADY_EXISTS => "already exists",
    399         ERR_INVALID_STRING => "invalid string",
    400         ERR_INVALID_ACTION => "invalid action",
    401         else => "io error",
    402     };
    403 }
    404 
    405 // ---------------------------------------------------------------------------
    406 // Frame stack and runtime
    407 // ---------------------------------------------------------------------------
    408 
    409 const Frame = union(enum) {
    410     bind: u32, // continuation k
    411     local: u32, // old env
    412 };
    413 
    414 const Runtime = struct {
    415     env: u32,
    416     state: u32,
    417 };
    418 
    419 // ---------------------------------------------------------------------------
    420 // Task
    421 // ---------------------------------------------------------------------------
    422 
    423 const Task = struct {
    424     id: u64,
    425     parent: ?*Task,
    426     frames: std.ArrayList(Frame),
    427     runtime: Runtime,
    428     current: u32,
    429     status: enum { runnable, blocked, completed },
    430     result: ?u32,
    431     waiting_for: ?u64,
    432 
    433     fn init(gpa: std.mem.Allocator, id: u64, parent: ?*Task, env: u32, state: u32, current: u32) !*Task {
    434         const task = try gpa.create(Task);
    435         task.* = .{
    436             .id = id,
    437             .parent = parent,
    438             .frames = std.ArrayList(Frame).empty,
    439             .runtime = .{ .env = env, .state = state },
    440             .current = current,
    441             .status = .runnable,
    442             .result = null,
    443             .waiting_for = null,
    444         };
    445         return task;
    446     }
    447 
    448     fn deinit(self: *Task, gpa: std.mem.Allocator) void {
    449         self.frames.deinit(gpa);
    450         gpa.destroy(self);
    451     }
    452 
    453     // finishValue processes a value through the frame stack.
    454     // Returns true if the task has completed (no more frames).
    455     fn finishValue(self: *Task, arena: *Arena, value: u32) !bool {
    456         if (self.frames.pop()) |frame| {
    457             switch (frame) {
    458                 .bind => |k| {
    459                     self.current = try arena.alloc(.{ .app = .{ .func = k, .arg = value } });
    460                     return false;
    461                 },
    462                 .local => |old_env| {
    463                     self.runtime.env = old_env;
    464                     self.current = try makePure(arena, value);
    465                     return false;
    466                 },
    467             }
    468         } else {
    469             self.current = value;
    470             return true;
    471         }
    472     }
    473 };
    474 
    475 // ---------------------------------------------------------------------------
    476 // Scheduler
    477 // ---------------------------------------------------------------------------
    478 
    479 const Scheduler = struct {
    480     gpa: std.mem.Allocator,
    481     loop: *c.uv_loop_t,
    482     arena: *Arena,
    483     tasks: std.ArrayList(*Task),
    484     runnable: std.ArrayList(*Task),
    485     next_id: u64,
    486     perms: IOPerms,
    487 
    488     fn init(gpa: std.mem.Allocator, loop: *c.uv_loop_t, arena: *Arena, perms: IOPerms) !Scheduler {
    489         const sched = Scheduler{
    490             .gpa = gpa,
    491             .loop = loop,
    492             .arena = arena,
    493             .tasks = std.ArrayList(*Task).empty,
    494             .runnable = std.ArrayList(*Task).empty,
    495             .next_id = 1,
    496             .perms = perms,
    497         };
    498         return sched;
    499     }
    500 
    501     fn deinit(self: *Scheduler) void {
    502         for (self.tasks.items) |task| {
    503             task.deinit(self.gpa);
    504         }
    505         self.tasks.deinit(self.gpa);
    506         self.runnable.deinit(self.gpa);
    507     }
    508 
    509     fn createTask(self: *Scheduler, parent: ?*Task, env: u32, state: u32, current: u32) !*Task {
    510         const id = self.next_id;
    511         self.next_id += 1;
    512         const task = try Task.init(self.gpa, id, parent, env, state, current);
    513         errdefer task.deinit(self.gpa);
    514         try self.tasks.append(self.gpa, task);
    515         return task;
    516     }
    517 
    518     fn run(self: *Scheduler) !void {
    519         while (true) {
    520             if (self.runnable.items.len > 0) {
    521                 const task = self.runnable.orderedRemove(0);
    522                 try self.stepTask(task);
    523             } else if (self.hasPendingHandles()) {
    524                 _ = c.uv_run(self.loop, c.UV_RUN_ONCE);
    525             } else {
    526                 break;
    527             }
    528         }
    529     }
    530 
    531     fn hasPendingHandles(self: *Scheduler) bool {
    532         return c.uv_loop_alive(self.loop) != 0;
    533     }
    534 
    535     fn completeTask(self: *Scheduler, task: *Task) !void {
    536         task.status = .completed;
    537         task.result = task.current;
    538         // Unblock any tasks waiting for this one
    539         for (self.tasks.items) |t| {
    540             if (t.status == .blocked and t.waiting_for == task.id) {
    541                 t.status = .runnable;
    542                 t.waiting_for = null;
    543                 t.current = try makePure(self.arena, task.result.?);
    544                 try self.runnable.append(self.gpa, t);
    545             }
    546         }
    547     }
    548 
    549     fn completeIO(self: *Scheduler, task: *Task, result: u32) !void {
    550         if (try task.finishValue(self.arena, result)) {
    551             try self.completeTask(task);
    552         } else {
    553             try self.runnable.append(self.gpa, task);
    554         }
    555     }
    556 
    557     fn fsReply(self: *Scheduler, task: *Task, err: u64) !void {
    558         try self.completeIO(task, try makeErrString(self.arena, fsErrorMessage(err)));
    559     }
    560 
    561     // Like the native CAS operations, source reads use synchronous libuv calls.
    562     // This avoids entering callback/reducer code while local trees are unrooted;
    563     // allocation and filesystem failures propagate normally to the host.
    564     // A source read occupies one scheduler turn (it is not a streaming action).
    565     fn readSource(self: *Scheduler, task: *Task, path_tree: u32) !void {
    566         const path = source_text.toUtf8(self.arena, path_tree) catch |err| switch (err) {
    567             error.InvalidScalarList => return self.fsReply(task, ERR_INVALID_STRING),
    568             else => return err,
    569         };
    570         defer self.arena.allocator.free(path);
    571         if (!self.perms.allow_read_all) return self.fsReply(task, ERR_POLICY_DENY);
    572         if (std.mem.indexOfScalar(u8, path, 0) != null) return self.fsReply(task, ERR_INVALID_STRING);
    573         const path_z = try self.gpa.dupeZ(u8, path);
    574         defer self.gpa.free(path_z);
    575         const read = try readFs(self.loop, self.gpa, path_z);
    576         switch (read) {
    577             .failure => |code| try self.fsReply(task, code),
    578             .bytes => |bytes| {
    579                 defer self.gpa.free(bytes);
    580                 try self.completeIO(task, try sourceReadResult(self.arena, path, bytes));
    581             },
    582         }
    583     }
    584 
    585     // IO protocol adapters: codec and filesystem helpers know nothing about
    586     // compiler manifests, cache aliases, path selection or publication order.
    587     fn indexedIO(self: *Scheduler, task: *Task, path_tree: u32, expected_tree: u32, value: ?u32) !void {
    588         const path = source_text.toUtf8(self.arena, path_tree) catch |err| switch (err) {
    589             error.InvalidScalarList => return self.fsReply(task, ERR_INVALID_STRING),
    590             else => return err,
    591         };
    592         defer self.arena.allocator.free(path);
    593         if (std.mem.indexOfScalar(u8, path, 0) != null) return self.fsReply(task, ERR_INVALID_STRING);
    594         const expected = source_text.toUtf8(self.arena, expected_tree) catch |err| switch (err) {
    595             error.InvalidScalarList => return self.completeIO(task, try makeErrString(self.arena, "invalid CAS digest")),
    596             else => return err,
    597         };
    598         defer self.arena.allocator.free(expected);
    599         if (expected.len != 64) return self.completeIO(task, try makeErrString(self.arena, "invalid CAS digest"));
    600         for (expected) |ch| if (!(ch >= '0' and ch <= '9' or ch >= 'a' and ch <= 'f'))
    601             return self.completeIO(task, try makeErrString(self.arena, "invalid CAS digest"));
    602         if (!self.perms.allow_read_all or (value != null and !self.perms.allow_write_all)) return self.fsReply(task, ERR_POLICY_DENY);
    603         const path_z = try self.gpa.dupeZ(u8, path);
    604         defer self.gpa.free(path_z);
    605         var encoded: ?[]u8 = null;
    606         defer if (encoded) |bytes| self.arena.allocator.free(bytes);
    607         var fresh = false;
    608         if (value) |tree_value| {
    609             encoded = try indexed.encode(self.arena, tree_value);
    610             if (!std.mem.eql(u8, &indexed.digest(encoded.?), expected))
    611                 return self.completeIO(task, try makeErrString(self.arena, "CAS digest mismatch"));
    612             switch (try writeAtomicFs(self.loop, self.gpa, path_z, encoded.?, true)) {
    613                 .failure => |code| return self.fsReply(task, code),
    614                 .written => |written| fresh = written,
    615             }
    616         }
    617         // Authenticate before decode; for publication compare the actual winner
    618         // byte-for-byte, including after an EEXIST race. Never overwrite it.
    619         switch (try readFs(self.loop, self.gpa, path_z)) {
    620             .failure => |code| return self.fsReply(task, code),
    621             .bytes => |bytes| {
    622                 defer self.gpa.free(bytes);
    623                 const result = if (encoded) |ours| blk: {
    624                     if (!std.mem.eql(u8, bytes, ours)) return self.completeIO(task, try makeErrString(self.arena, "CAS object exists with different bytes"));
    625                     break :blk if (fresh) try self.arena.alloc(.{ .stem = .{ .child = try self.arena.alloc(.leaf) } }) else try self.arena.alloc(.leaf);
    626                 } else blk: {
    627                     if (!std.mem.eql(u8, &indexed.digest(bytes), expected)) return self.completeIO(task, try makeErrString(self.arena, "CAS digest mismatch"));
    628                     break :blk indexed.load(self.arena, bytes, "root", true) catch |err| switch (err) {
    629                         error.OutOfMemory => return err,
    630                         else => return self.completeIO(task, try makeErrString(self.arena, "invalid indexed tree")),
    631                     };
    632                 };
    633                 try self.completeIO(task, try makeOkResult(self.arena, result));
    634             },
    635         }
    636     }
    637 
    638     fn stepTask(self: *Scheduler, task: *Task) !void {
    639         // Snapshot every scheduler-owned node for this turn. A turn may force
    640         // repeatedly; frames popped during it remain rooted until it completes.
    641         // Callbacks only allocate/link trees and never enter the reducer.
    642         const base = self.arena.roots.items.len;
    643         defer self.arena.roots.shrinkRetainingCapacity(base);
    644         for (self.tasks.items) |t| {
    645             try self.arena.roots.appendSlice(self.gpa, &.{ t.current, t.runtime.env, t.runtime.state });
    646             if (t.result) |result| try self.arena.roots.append(self.gpa, result);
    647             for (t.frames.items) |frame| switch (frame) {
    648                 .bind => |k| try self.arena.roots.append(self.gpa, k),
    649                 .local => |env| try self.arena.roots.append(self.gpa, env),
    650             };
    651         }
    652         const reduced = try reduce.reduce(task.current, self.arena, std.math.maxInt(u64));
    653 
    654         const decoded = try decodeAction(self.arena, reduced);
    655         if (decoded == null) {
    656             // Not a recognized action — if no frames, it's the final result.
    657             // Otherwise treat as invalid.
    658             if (task.frames.items.len == 0) {
    659                 task.current = reduced;
    660                 try self.completeTask(task);
    661                 return;
    662             }
    663             const invalid = self.arena.get(reduced).*;
    664             const tag = if (invalid == .fork) try codecs.toNumber(self.arena, invalid.fork.left) else null;
    665             var diagnostic: [96]u8 = undefined;
    666             const message = if (tag) |n| try std.fmt.bufPrint(&diagnostic, "invalid or unsupported IO action tag {d}", .{n}) else "invalid action";
    667             const err = try makeErrString(self.arena, message);
    668             if (try task.finishValue(self.arena, err)) {
    669                 try self.completeTask(task);
    670             } else {
    671                 try self.runnable.append(self.gpa, task);
    672             }
    673             return;
    674         }
    675 
    676         const action = decoded.?;
    677         switch (action) {
    678             .pure => |val| {
    679                 if (try task.finishValue(self.arena, val)) {
    680                     try self.completeTask(task);
    681                 } else {
    682                     try self.runnable.append(self.gpa, task);
    683                 }
    684             },
    685 
    686             .bind => |b| {
    687                 try task.frames.append(self.gpa, .{ .bind = b.k });
    688                 task.current = b.left;
    689                 try self.runnable.append(self.gpa, task);
    690             },
    691 
    692             .putStr => |str_tree| {
    693                 const str = try codecs.toString(self.arena, str_tree) orelse {
    694                     const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
    695                     if (try task.finishValue(self.arena, err)) {
    696                         try self.completeTask(task);
    697                     } else {
    698                         try self.runnable.append(self.gpa, task);
    699                     }
    700                     return;
    701                 };
    702                 defer self.gpa.free(str);
    703                 var written: usize = 0;
    704                 while (written < str.len) {
    705                     const n = std.c.write(1, str[written..].ptr, str.len - written);
    706                     if (n <= 0) return error.WriteFailed;
    707                     written += @intCast(n);
    708                 }
    709                 const leaf = try self.arena.alloc(.leaf);
    710                 if (try task.finishValue(self.arena, leaf)) {
    711                     try self.completeTask(task);
    712                 } else {
    713                     try self.runnable.append(self.gpa, task);
    714                 }
    715             },
    716 
    717             .getLine => {
    718                 var buf: [4096]u8 = undefined;
    719                 var len: usize = 0;
    720                 while (len < buf.len) {
    721                     const n = std.c.read(0, buf[len..].ptr, 1);
    722                     if (n <= 0) break;
    723                     if (buf[len] == '\n') break;
    724                     len += 1;
    725                 }
    726                 const line = buf[0..len];
    727                 const str_tree = try codecs.ofString(self.arena, line);
    728                 if (try task.finishValue(self.arena, str_tree)) {
    729                     try self.completeTask(task);
    730                 } else {
    731                     try self.runnable.append(self.gpa, task);
    732                 }
    733             },
    734 
    735             .readSourceFile => |path_tree| try self.readSource(task, path_tree),
    736             .readIndexedTree => |read| try self.indexedIO(task, read.path, read.digest, null),
    737             .writeIndexedTreeIfAbsent => |write| try self.indexedIO(task, write.path, write.digest, write.tree),
    738             .hashIndexedTree => |value| {
    739                 const bytes = try indexed.encode(self.arena, value);
    740                 defer self.arena.allocator.free(bytes);
    741                 const hex = indexed.digest(bytes);
    742                 try self.completeIO(task, try makeOkResult(self.arena, try codecs.ofString(self.arena, &hex)));
    743             },
    744 
    745             .encodeSourceText => |text| {
    746                 const bytes = source_text.toUtf8(self.arena, text) catch |err| switch (err) {
    747                     error.InvalidScalarList => {
    748                         try self.completeIO(task, try makeErrString(self.arena, "invalid source scalar list"));
    749                         return;
    750                     },
    751                     else => return err,
    752                 };
    753                 defer self.arena.allocator.free(bytes);
    754                 try self.completeIO(task, try makeOkResult(self.arena, try codecs.ofBytes(self.arena, bytes)));
    755             },
    756 
    757             .readFile => |path_tree| {
    758                 const path = try codecs.toString(self.arena, path_tree) orelse {
    759                     const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
    760                     if (try task.finishValue(self.arena, err)) {
    761                         try self.completeTask(task);
    762                     } else {
    763                         try self.runnable.append(self.gpa, task);
    764                     }
    765                     return;
    766                 };
    767 
    768                 if (!self.perms.allow_read_all) {
    769                     self.arena.allocator.free(path);
    770                     const err = try makeErrResult(self.arena, ERR_POLICY_DENY);
    771                     if (try task.finishValue(self.arena, err)) {
    772                         try self.completeTask(task);
    773                     } else {
    774                         try self.runnable.append(self.gpa, task);
    775                     }
    776                     return;
    777                 }
    778 
    779                 const ctx = try self.gpa.create(FileReadCtx);
    780                 ctx.* = .{
    781                     .scheduler = self,
    782                     .task = task,
    783                     .arena = self.arena,
    784                     .gpa = self.gpa,
    785                     .fd = -1,
    786                     .buf = std.ArrayList(u8).empty,
    787                     .path = try self.gpa.dupeZ(u8, path),
    788                     .req = undefined,
    789                     .read_buf = null,
    790                 };
    791                 self.arena.allocator.free(path);
    792                 ctx.req.data = ctx;
    793                 _ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, c.O_RDONLY, 0, file_open_cb);
    794             },
    795 
    796             .writeFile => |wf| {
    797                 const path = try codecs.toString(self.arena, wf.path) orelse {
    798                     const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
    799                     if (try task.finishValue(self.arena, err)) {
    800                         try self.completeTask(task);
    801                     } else {
    802                         try self.runnable.append(self.gpa, task);
    803                     }
    804                     return;
    805                 };
    806 
    807                 const contents = try codecs.toString(self.arena, wf.contents) orelse {
    808                     self.arena.allocator.free(path);
    809                     const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
    810                     if (try task.finishValue(self.arena, err)) {
    811                         try self.completeTask(task);
    812                     } else {
    813                         try self.runnable.append(self.gpa, task);
    814                     }
    815                     return;
    816                 };
    817 
    818                 if (!self.perms.allow_write_all) {
    819                     self.arena.allocator.free(path);
    820                     self.arena.allocator.free(contents);
    821                     const err = try makeErrResult(self.arena, ERR_POLICY_DENY);
    822                     if (try task.finishValue(self.arena, err)) {
    823                         try self.completeTask(task);
    824                     } else {
    825                         try self.runnable.append(self.gpa, task);
    826                     }
    827                     return;
    828                 }
    829 
    830                 const ctx = try self.gpa.create(FileWriteCtx);
    831                 ctx.* = .{
    832                     .scheduler = self,
    833                     .task = task,
    834                     .arena = self.arena,
    835                     .gpa = self.gpa,
    836                     .fd = -1,
    837                     .path = try self.gpa.dupeZ(u8, path),
    838                     .contents = contents,
    839                     .offset = 0,
    840                     .req = undefined,
    841                 };
    842                 self.arena.allocator.free(path);
    843                 ctx.req.data = ctx;
    844                 const flags = c.O_WRONLY | c.O_CREAT | c.O_TRUNC;
    845                 _ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, flags, 0o644, file_write_open_cb);
    846             },
    847 
    848             .writeBytes, .writeBytesIfAbsent, .atomicWriteBytes => |write| {
    849                 const path = try codecs.toString(self.arena, write.path) orelse {
    850                     try self.fsReply(task, ERR_INVALID_STRING);
    851                     return;
    852                 };
    853                 defer self.arena.allocator.free(path);
    854                 const contents = try codecs.toBytes(self.arena, write.contents) orelse {
    855                     try self.completeIO(task, try makeErrString(self.arena, "invalid bytes"));
    856                     return;
    857                 };
    858                 defer self.arena.allocator.free(contents);
    859                 if (!self.perms.allow_write_all) {
    860                     try self.fsReply(task, ERR_POLICY_DENY);
    861                     return;
    862                 }
    863                 const path_z = try self.gpa.dupeZ(u8, path);
    864                 defer self.gpa.free(path_z);
    865                 const exclusive = action == .writeBytesIfAbsent;
    866                 const published = try writeAtomicFs(self.loop, self.gpa, path_z, contents, exclusive);
    867                 switch (published) {
    868                     .failure => |code| try self.fsReply(task, code),
    869                     .written => |fresh| {
    870                         const value = if (action == .writeBytes) try self.arena.alloc(.leaf) else if (fresh) try self.arena.alloc(.{ .stem = .{ .child = try self.arena.alloc(.leaf) } }) else try self.arena.alloc(.leaf);
    871                         try self.completeIO(task, try makeOkResult(self.arena, value));
    872                     },
    873                 }
    874             },
    875 
    876             .createDirectory, .fileExists => |path_tree| {
    877                 const path = try codecs.toString(self.arena, path_tree) orelse {
    878                     try self.fsReply(task, ERR_INVALID_STRING);
    879                     return;
    880                 };
    881                 defer self.arena.allocator.free(path);
    882                 const is_create = action == .createDirectory;
    883                 if (is_create and !self.perms.allow_write_all or
    884                     !is_create and !self.perms.allow_read_all)
    885                 {
    886                     try self.fsReply(task, ERR_POLICY_DENY);
    887                     return;
    888                 }
    889                 const path_z = try self.gpa.dupeZ(u8, path);
    890                 defer self.gpa.free(path_z);
    891                 var req: c.uv_fs_t = undefined;
    892                 const rc = if (is_create) c.uv_fs_mkdir(self.loop, &req, path_z.ptr, 0o755, null) else c.uv_fs_stat(self.loop, &req, path_z.ptr, null);
    893                 c.uv_fs_req_cleanup(&req);
    894                 if (rc < 0 and (is_create and rc != c.UV_EEXIST or
    895                     !is_create and rc != c.UV_ENOENT))
    896                 {
    897                     try self.fsReply(task, uvFileError(rc));
    898                     return;
    899                 }
    900                 if (is_create and rc == c.UV_EEXIST) {
    901                     var stat_req: c.uv_fs_t = undefined;
    902                     const stat = c.uv_fs_stat(self.loop, &stat_req, path_z.ptr, null);
    903                     const is_dir = stat >= 0 and stat_req.statbuf.st_mode & c.S_IFMT == c.S_IFDIR;
    904                     c.uv_fs_req_cleanup(&stat_req);
    905                     if (!is_dir) {
    906                         try self.fsReply(task, ERR_ALREADY_EXISTS);
    907                         return;
    908                     }
    909                 }
    910                 if (is_create) {
    911                     if (syncFsPath(self.loop, path_z.ptr, true)) |err| {
    912                         try self.fsReply(task, err);
    913                         return;
    914                     }
    915                     if (try syncFsParent(self.loop, self.gpa, path_z)) |err| {
    916                         try self.fsReply(task, err);
    917                         return;
    918                     }
    919                 }
    920                 const value = if (is_create) try self.arena.alloc(.leaf) else if (rc == 0) try self.arena.alloc(.{ .stem = .{ .child = try self.arena.alloc(.leaf) } }) else try self.arena.alloc(.leaf);
    921                 try self.completeIO(task, try makeOkResult(self.arena, value));
    922             },
    923 
    924             .renameFile => |paths| {
    925                 const old = try codecs.toString(self.arena, paths.old) orelse {
    926                     try self.fsReply(task, ERR_INVALID_STRING);
    927                     return;
    928                 };
    929                 defer self.arena.allocator.free(old);
    930                 const new = try codecs.toString(self.arena, paths.new) orelse {
    931                     try self.fsReply(task, ERR_INVALID_STRING);
    932                     return;
    933                 };
    934                 defer self.arena.allocator.free(new);
    935                 if (!self.perms.allow_write_all) {
    936                     try self.fsReply(task, ERR_POLICY_DENY);
    937                     return;
    938                 }
    939                 const old_z = try self.gpa.dupeZ(u8, old);
    940                 defer self.gpa.free(old_z);
    941                 const new_z = try self.gpa.dupeZ(u8, new);
    942                 defer self.gpa.free(new_z);
    943                 var req: c.uv_fs_t = undefined;
    944                 const rc = c.uv_fs_rename(self.loop, &req, old_z.ptr, new_z.ptr, null);
    945                 c.uv_fs_req_cleanup(&req);
    946                 if (rc < 0) return self.fsReply(task, uvFileError(rc));
    947                 try self.completeIO(task, try makeOkResult(self.arena, try self.arena.alloc(.leaf)));
    948             },
    949 
    950             .sha256Hex => |bytes_tree| {
    951                 const bytes = try codecs.toBytes(self.arena, bytes_tree) orelse {
    952                     try self.completeIO(task, try makeErrString(self.arena, "invalid bytes"));
    953                     return;
    954                 };
    955                 defer self.arena.allocator.free(bytes);
    956                 var digest: [32]u8 = undefined;
    957                 std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{});
    958                 const hex = std.fmt.bytesToHex(digest, .lower);
    959                 try self.completeIO(task, try makeOkResult(self.arena, try codecs.ofString(self.arena, &hex)));
    960             },
    961 
    962             .ask => {
    963                 if (try task.finishValue(self.arena, task.runtime.env)) {
    964                     try self.completeTask(task);
    965                 } else {
    966                     try self.runnable.append(self.gpa, task);
    967                 }
    968             },
    969 
    970             .local => |loc| {
    971                 const new_env = try reduce.reduce(try self.arena.alloc(.{ .app = .{ .func = loc.f, .arg = task.runtime.env } }), self.arena, std.math.maxInt(u64));
    972                 try task.frames.append(self.gpa, .{ .local = task.runtime.env });
    973                 task.runtime.env = new_env;
    974                 task.current = loc.action;
    975                 try self.runnable.append(self.gpa, task);
    976             },
    977 
    978             .get => {
    979                 if (try task.finishValue(self.arena, task.runtime.state)) {
    980                     try self.completeTask(task);
    981                 } else {
    982                     try self.runnable.append(self.gpa, task);
    983                 }
    984             },
    985 
    986             .put => |new_state| {
    987                 task.runtime.state = new_state;
    988                 const leaf = try self.arena.alloc(.leaf);
    989                 if (try task.finishValue(self.arena, leaf)) {
    990                     try self.completeTask(task);
    991                 } else {
    992                     try self.runnable.append(self.gpa, task);
    993                 }
    994             },
    995 
    996             .fork => |child_action| {
    997                 const child = try self.createTask(task, task.runtime.env, task.runtime.state, child_action);
    998                 try self.runnable.append(self.gpa, child);
    999                 const handle = try codecs.ofNumber(self.arena, child.id);
   1000                 if (try task.finishValue(self.arena, handle)) {
   1001                     try self.completeTask(task);
   1002                 } else {
   1003                     try self.runnable.append(self.gpa, task);
   1004                 }
   1005             },
   1006 
   1007             .await => |handle_tree| {
   1008                 const handle = try codecs.toNumber(self.arena, handle_tree) orelse {
   1009                     const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
   1010                     if (try task.finishValue(self.arena, err)) {
   1011                         try self.completeTask(task);
   1012                     } else {
   1013                         try self.runnable.append(self.gpa, task);
   1014                     }
   1015                     return;
   1016                 };
   1017                 var found: ?*Task = null;
   1018                 for (self.tasks.items) |t| {
   1019                     if (t.id == handle) {
   1020                         found = t;
   1021                         break;
   1022                     }
   1023                 }
   1024                 if (found == null) {
   1025                     const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
   1026                     if (try task.finishValue(self.arena, err)) {
   1027                         try self.completeTask(task);
   1028                     } else {
   1029                         try self.runnable.append(self.gpa, task);
   1030                     }
   1031                     return;
   1032                 }
   1033                 if (found.?.status == .completed) {
   1034                     const result = found.?.result.?;
   1035                     if (try task.finishValue(self.arena, result)) {
   1036                         try self.completeTask(task);
   1037                     } else {
   1038                         try self.runnable.append(self.gpa, task);
   1039                     }
   1040                 } else {
   1041                     task.status = .blocked;
   1042                     task.waiting_for = handle;
   1043                     // Task remains out of runnable until child completes
   1044                 }
   1045             },
   1046 
   1047             .yield => {
   1048                 const leaf = try self.arena.alloc(.leaf);
   1049                 if (try task.finishValue(self.arena, leaf)) {
   1050                     try self.completeTask(task);
   1051                 } else {
   1052                     try self.runnable.append(self.gpa, task);
   1053                 }
   1054             },
   1055 
   1056             .sleep => |ms_tree| {
   1057                 const ms = try codecs.toNumber(self.arena, ms_tree) orelse 0;
   1058                 const ctx = try self.gpa.create(SleepCtx);
   1059                 ctx.* = .{
   1060                     .scheduler = self,
   1061                     .task = task,
   1062                     .arena = self.arena,
   1063                     .timer = undefined,
   1064                 };
   1065                 ctx.timer.data = ctx;
   1066                 _ = c.uv_timer_init(self.loop, &ctx.timer);
   1067                 _ = c.uv_timer_start(&ctx.timer, sleep_cb, @intCast(ms), 0);
   1068             },
   1069         }
   1070     }
   1071 };
   1072 
   1073 // ---------------------------------------------------------------------------
   1074 // Async file read
   1075 // ---------------------------------------------------------------------------
   1076 
   1077 const FileReadCtx = struct {
   1078     scheduler: *Scheduler,
   1079     task: *Task,
   1080     arena: *Arena,
   1081     gpa: std.mem.Allocator,
   1082     fd: c_int,
   1083     buf: std.ArrayList(u8),
   1084     path: [:0]const u8,
   1085     req: c.uv_fs_t,
   1086     read_buf: ?[]u8,
   1087 };
   1088 
   1089 fn mapUvErr(uv_err: c_int) u64 {
   1090     return switch (uv_err) {
   1091         c.UV_ENOENT => ERR_DOES_NOT_EXIST,
   1092         c.UV_EACCES => ERR_PERMISSION,
   1093         c.UV_EEXIST => ERR_ALREADY_EXISTS,
   1094         else => ERR_IO_OTHER,
   1095     };
   1096 }
   1097 
   1098 fn file_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
   1099     const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data)));
   1100     const result = req.*.result;
   1101     c.uv_fs_req_cleanup(req);
   1102     if (result < 0) {
   1103         const err = makeErrResult(ctx.arena, mapUvErr(@intCast(result))) catch {
   1104             ctx.gpa.destroy(ctx);
   1105             return;
   1106         };
   1107         if (ctx.task.finishValue(ctx.arena, err) catch false) {
   1108             ctx.scheduler.completeTask(ctx.task) catch {};
   1109         } else {
   1110             ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
   1111         }
   1112         ctx.buf.deinit(ctx.gpa);
   1113         ctx.gpa.free(ctx.path);
   1114         ctx.gpa.destroy(ctx);
   1115         return;
   1116     }
   1117     ctx.fd = @intCast(result);
   1118     const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable;
   1119     ctx.read_buf = read_buf;
   1120     var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len));
   1121     _ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb);
   1122 }
   1123 
   1124 fn file_read_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
   1125     const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data)));
   1126     const nread = req.*.result;
   1127     c.uv_fs_req_cleanup(req);
   1128     if (nread < 0) {
   1129         _ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
   1130         const err = makeErrResult(ctx.arena, mapUvErr(@intCast(nread))) catch {
   1131             ctx.gpa.destroy(ctx);
   1132             return;
   1133         };
   1134         if (ctx.task.finishValue(ctx.arena, err) catch false) {
   1135             ctx.scheduler.completeTask(ctx.task) catch {};
   1136         } else {
   1137             ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
   1138         }
   1139         if (ctx.read_buf) |b| ctx.gpa.free(b);
   1140         ctx.buf.deinit(ctx.gpa);
   1141         ctx.gpa.free(ctx.path);
   1142         ctx.gpa.destroy(ctx);
   1143         return;
   1144     }
   1145     if (nread == 0) {
   1146         // EOF
   1147         _ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
   1148         const bytes_tree = codecs.ofBytes(ctx.arena, ctx.buf.items) catch {
   1149             ctx.gpa.destroy(ctx);
   1150             return;
   1151         };
   1152         const ok = makeOkResult(ctx.arena, bytes_tree) catch {
   1153             ctx.gpa.destroy(ctx);
   1154             return;
   1155         };
   1156         if (ctx.task.finishValue(ctx.arena, ok) catch false) {
   1157             ctx.scheduler.completeTask(ctx.task) catch {};
   1158         } else {
   1159             ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
   1160         }
   1161         if (ctx.read_buf) |b| ctx.gpa.free(b);
   1162         ctx.buf.deinit(ctx.gpa);
   1163         ctx.gpa.free(ctx.path);
   1164         ctx.gpa.destroy(ctx);
   1165         return;
   1166     }
   1167     const data = ctx.read_buf.?[0..@intCast(nread)];
   1168     ctx.buf.appendSlice(ctx.gpa, data) catch unreachable;
   1169     ctx.gpa.free(ctx.read_buf.?);
   1170     const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable;
   1171     ctx.read_buf = read_buf;
   1172     var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len));
   1173     _ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb);
   1174 }
   1175 
   1176 // ---------------------------------------------------------------------------
   1177 // Async file write
   1178 // ---------------------------------------------------------------------------
   1179 
   1180 const FileWriteCtx = struct {
   1181     scheduler: *Scheduler,
   1182     task: *Task,
   1183     arena: *Arena,
   1184     gpa: std.mem.Allocator,
   1185     fd: c_int,
   1186     path: [:0]const u8,
   1187     contents: []const u8,
   1188     offset: usize,
   1189     req: c.uv_fs_t,
   1190 };
   1191 
   1192 fn file_write_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
   1193     const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
   1194     const result = req.*.result;
   1195     c.uv_fs_req_cleanup(req);
   1196     if (result < 0) {
   1197         const err = makeErrResult(ctx.arena, mapUvErr(@intCast(result))) catch {
   1198             ctx.gpa.destroy(ctx);
   1199             return;
   1200         };
   1201         if (ctx.task.finishValue(ctx.arena, err) catch false) {
   1202             ctx.scheduler.completeTask(ctx.task) catch {};
   1203         } else {
   1204             ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
   1205         }
   1206         ctx.gpa.free(ctx.path);
   1207         ctx.gpa.free(ctx.contents);
   1208         ctx.gpa.destroy(ctx);
   1209         return;
   1210     }
   1211     ctx.fd = @intCast(result);
   1212     if (ctx.contents.len == 0) {
   1213         _ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, file_write_close_cb);
   1214         return;
   1215     }
   1216     file_write_next(ctx, req);
   1217 }
   1218 
   1219 fn file_write_next(ctx: *FileWriteCtx, req: [*c]c.uv_fs_t) void {
   1220     const remaining = ctx.contents[ctx.offset..];
   1221     var uv_buf = c.uv_buf_init(@ptrCast(@constCast(remaining.ptr)), @intCast(@min(remaining.len, std.math.maxInt(c_uint))));
   1222     _ = c.uv_fs_write(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, @intCast(ctx.offset), file_write_cb);
   1223 }
   1224 
   1225 fn file_write_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
   1226     const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
   1227     const nwrite = req.*.result;
   1228     c.uv_fs_req_cleanup(req);
   1229     if (nwrite <= 0) {
   1230         _ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
   1231         const err = makeErrResult(ctx.arena, if (nwrite == 0) ERR_IO_OTHER else mapUvErr(@intCast(nwrite))) catch {
   1232             ctx.gpa.destroy(ctx);
   1233             return;
   1234         };
   1235         if (ctx.task.finishValue(ctx.arena, err) catch false) {
   1236             ctx.scheduler.completeTask(ctx.task) catch {};
   1237         } else {
   1238             ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
   1239         }
   1240         ctx.gpa.free(ctx.path);
   1241         ctx.gpa.free(ctx.contents);
   1242         ctx.gpa.destroy(ctx);
   1243         return;
   1244     }
   1245     ctx.offset += @intCast(nwrite);
   1246     if (ctx.offset < ctx.contents.len) {
   1247         file_write_next(ctx, req);
   1248         return;
   1249     }
   1250     _ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, file_write_close_cb);
   1251 }
   1252 
   1253 fn file_write_close_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
   1254     const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
   1255     c.uv_fs_req_cleanup(req);
   1256     const leaf = ctx.arena.alloc(.leaf) catch {
   1257         ctx.gpa.destroy(ctx);
   1258         return;
   1259     };
   1260     const ok = makeOkResult(ctx.arena, leaf) catch {
   1261         ctx.gpa.destroy(ctx);
   1262         return;
   1263     };
   1264     if (ctx.task.finishValue(ctx.arena, ok) catch false) {
   1265         ctx.scheduler.completeTask(ctx.task) catch {};
   1266     } else {
   1267         ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
   1268     }
   1269     ctx.gpa.free(ctx.path);
   1270     ctx.gpa.free(ctx.contents);
   1271     ctx.gpa.destroy(ctx);
   1272 }
   1273 
   1274 // ---------------------------------------------------------------------------
   1275 // Async sleep
   1276 // ---------------------------------------------------------------------------
   1277 
   1278 const SleepCtx = struct {
   1279     scheduler: *Scheduler,
   1280     task: *Task,
   1281     arena: *Arena,
   1282     timer: c.uv_timer_t,
   1283 };
   1284 
   1285 fn sleep_cb(handle: [*c]c.uv_timer_t) callconv(.c) void {
   1286     const ctx = @as(*SleepCtx, @ptrCast(@alignCast(handle.*.data)));
   1287     defer ctx.scheduler.gpa.destroy(ctx);
   1288     const leaf = ctx.arena.alloc(.leaf) catch {
   1289         ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
   1290         return;
   1291     };
   1292     if (ctx.task.finishValue(ctx.arena, leaf) catch false) {
   1293         ctx.scheduler.completeTask(ctx.task) catch {};
   1294     } else {
   1295         ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
   1296     }
   1297 }
   1298 
   1299 // ---------------------------------------------------------------------------
   1300 // Public entry point
   1301 // ---------------------------------------------------------------------------
   1302 
   1303 pub fn runIO(gpa: std.mem.Allocator, arena: *Arena, program: u32, perms: IOPerms) !u32 {
   1304     const base = arena.roots.items.len;
   1305     defer arena.roots.shrinkRetainingCapacity(base);
   1306     try arena.roots.append(gpa, program);
   1307     const action_tree = try isIOSentinel(arena, program) orelse {
   1308         return error.InvalidIOSentinel;
   1309     };
   1310 
   1311     var loop: c.uv_loop_t = undefined;
   1312     const rc = c.uv_loop_init(&loop);
   1313     if (rc != 0) return error.LoopInitFailed;
   1314     defer _ = c.uv_loop_close(&loop);
   1315 
   1316     var scheduler = try Scheduler.init(gpa, &loop, arena, perms);
   1317     defer scheduler.deinit();
   1318 
   1319     const main_task = try scheduler.createTask(null, try arena.alloc(.leaf), try arena.alloc(.leaf), action_tree);
   1320     try scheduler.runnable.append(gpa, main_task);
   1321 
   1322     try scheduler.run();
   1323 
   1324     // Return the main task's result
   1325     return main_task.result orelse program;
   1326 }
   1327 
   1328 test "scheduler safepoint roots include blocked tasks, frames, environment, state and results" {
   1329     const testing = std.testing;
   1330     var arena = Arena.init(testing.allocator);
   1331     defer arena.deinit();
   1332     var loop: c.uv_loop_t = undefined;
   1333     try testing.expectEqual(@as(c_int, 0), c.uv_loop_init(&loop));
   1334     defer _ = c.uv_loop_close(&loop);
   1335     var scheduler = try Scheduler.init(testing.allocator, &loop, &arena, .{});
   1336     defer scheduler.deinit();
   1337     const leaf = try arena.alloc(.leaf);
   1338     const external = try codecs.ofNumber(&arena, 99);
   1339     try arena.roots.append(arena.allocator, external);
   1340     const value = try codecs.ofNumber(&arena, 42);
   1341     const action = try makePure(&arena, value);
   1342     const task = try scheduler.createTask(null, leaf, leaf, action);
   1343     const blocked = try scheduler.createTask(null, try codecs.ofNumber(&arena, 17), try codecs.ofNumber(&arena, 23), leaf);
   1344     blocked.status = .blocked;
   1345     blocked.result = try codecs.ofNumber(&arena, 31);
   1346     const saved_env = try codecs.ofNumber(&arena, 47);
   1347     const continuation = try codecs.ofNumber(&arena, 53);
   1348     try blocked.frames.append(arena.allocator, .{ .local = saved_env });
   1349     try blocked.frames.append(arena.allocator, .{ .bind = continuation });
   1350     for (0..1000) |_| _ = try arena.alloc(.leaf);
   1351     const before_collection = arena.len();
   1352     arena.gc_enabled = true;
   1353     try arena.setGcPolicy(1, 1);
   1354     try scheduler.stepTask(task);
   1355     try testing.expect(arena.collections > 0);
   1356     try testing.expect(before_collection - arena.len() + arena.free.items.len >= 1000);
   1357     try testing.expectEqual(@as(usize, 1), arena.roots.items.len);
   1358     // Observations below are outside a scheduler turn; explicitly hold its
   1359     // roots by stopping automatic collection, not by accidentally relying on
   1360     // an allocation threshold.
   1361     arena.gc_enabled = false;
   1362     const ids = [_]u32{ external, task.result.?, blocked.runtime.env, blocked.runtime.state, blocked.result.?, saved_env, continuation };
   1363     const expected = [_]u64{ 99, 42, 17, 23, 31, 47, 53 };
   1364     for (ids, expected) |id, n| try testing.expectEqual(n, (try codecs.toNumber(&arena, id)).?);
   1365 }
   1366 
   1367 test "IO sentinel forces delayed outer and version/action pair" {
   1368     const testing = std.testing;
   1369     var arena = Arena.init(testing.allocator);
   1370     defer arena.deinit();
   1371     const leaf = try arena.alloc(.leaf);
   1372     const value = try codecs.ofNumber(&arena, 42);
   1373     const action = try makePure(&arena, value);
   1374     const version = try codecs.ofNumber(&arena, 1);
   1375     const pair = try arena.alloc(.{ .fork = .{ .left = version, .right = action } });
   1376     const k_pair = try arena.alloc(.{ .fork = .{ .left = leaf, .right = pair } });
   1377     const delayed_pair = try arena.alloc(.{ .app = .{ .func = k_pair, .arg = leaf } });
   1378     const marker = try codecs.ofString(&arena, "tricuIO");
   1379     const sentinel = try arena.alloc(.{ .fork = .{ .left = marker, .right = delayed_pair } });
   1380     const k_sentinel = try arena.alloc(.{ .fork = .{ .left = leaf, .right = sentinel } });
   1381     const program = try arena.alloc(.{ .app = .{ .func = k_sentinel, .arg = leaf } });
   1382     arena.gc_enabled = true;
   1383     try arena.setGcPolicy(1, 1);
   1384     const result = try runIO(arena.allocator, &arena, program, .{});
   1385     arena.gc_enabled = false; // result is held locally, outside a registered scope
   1386     try testing.expectEqual(@as(u64, 42), (try codecs.toNumber(&arena, result)).?);
   1387     try testing.expectEqual(@as(usize, 0), arena.roots.items.len);
   1388 }
   1389 
   1390 fn testAction(arena: *Arena, tag: u64, payload: u32, perms: IOPerms) !codecs.UnwrapResult {
   1391     const action = try arena.alloc(.{ .fork = .{ .left = try codecs.ofNumber(arena, tag), .right = payload } });
   1392     const rest = try arena.alloc(.{ .fork = .{ .left = try codecs.ofNumber(arena, 1), .right = action } });
   1393     const program = try arena.alloc(.{ .fork = .{ .left = try codecs.ofString(arena, "tricuIO"), .right = rest } });
   1394     arena.gc_enabled = true;
   1395     try arena.setGcPolicy(1, 1);
   1396     const result = try runIO(arena.allocator, arena, program, perms);
   1397     arena.gc_enabled = false; // observations are local, outside the scheduler
   1398     return (try codecs.unwrapResult(arena, result)).?;
   1399 }
   1400 
   1401 fn expectErrorText(arena: *Arena, reply: codecs.UnwrapResult, text: []const u8) !void {
   1402     try std.testing.expect(!reply.ok);
   1403     const message = try source_text.toUtf8(arena, reply.value);
   1404     defer arena.allocator.free(message);
   1405     try std.testing.expect(std.mem.indexOf(u8, message, text) != null);
   1406 }
   1407 
   1408 test "encodeSourceText is strict, lazy, permission-free and distinct from byte codecs" {
   1409     var arena = Arena.init(std.testing.allocator);
   1410     defer arena.deinit();
   1411     const expected = "\u{feff}aĆ©šŸ˜€\x00\r\ne\u{301}";
   1412     const list = try source_text.fromUtf8(&arena, expected);
   1413     const leaf = try arena.alloc(.leaf);
   1414     const constant = try arena.alloc(.{ .fork = .{ .left = leaf, .right = list } });
   1415     const delayed = try arena.alloc(.{ .app = .{ .func = constant, .arg = leaf } });
   1416     const result = try testAction(&arena, 38, delayed, .{});
   1417     try std.testing.expect(result.ok);
   1418     const bytes = (try codecs.toBytes(&arena, result.value)).?;
   1419     defer arena.allocator.free(bytes);
   1420     try std.testing.expectEqualStrings(expected, bytes);
   1421     for ([_]u64{ 0xd800, 0x110000 }) |n| {
   1422         const bad = try codecs.ofList(&arena, &.{try codecs.ofNumber(&arena, n)});
   1423         try expectErrorText(&arena, try testAction(&arena, 38, bad, .{}), "invalid source scalar list");
   1424     }
   1425     // Noncanonical zero has the right numeric value, but is not a scalar tree.
   1426     const zero = try arena.alloc(.leaf);
   1427     const redundant = try arena.alloc(.{ .fork = .{ .left = zero, .right = zero } });
   1428     const bad = try codecs.ofList(&arena, &.{redundant});
   1429     try expectErrorText(&arena, try testAction(&arena, 38, bad, .{}), "invalid source scalar list");
   1430 }
   1431 
   1432 test "readSourceFile preserves bytes and scalars, rejects malformed input, and enforces read permission" {
   1433     var tmp = std.testing.tmpDir(.{});
   1434     defer tmp.cleanup();
   1435     const directory = try tmp.dir.realPathFileAlloc(std.testing.io, ".", std.testing.allocator);
   1436     defer std.testing.allocator.free(directory);
   1437     const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/Ć©šŸ˜€.tri", .{directory});
   1438     defer std.testing.allocator.free(path);
   1439     var arena = Arena.init(std.testing.allocator);
   1440     defer arena.deinit();
   1441     var bytes: std.ArrayList(u8) = .empty;
   1442     defer bytes.deinit(std.testing.allocator);
   1443     try bytes.appendNTimes(std.testing.allocator, 'a', 4095);
   1444     try bytes.appendSlice(std.testing.allocator, "šŸ˜€\r\nĆ©\x00\u{feff}e\u{301}");
   1445     for ([_][]const u8{ bytes.items, "" }) |contents| {
   1446         try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "Ć©šŸ˜€.tri", .data = contents });
   1447         const reply = try testAction(&arena, 37, try source_text.fromUtf8(&arena, path), .{ .allow_read_all = true });
   1448         try std.testing.expect(reply.ok);
   1449         const pair = arena.get(reply.value).fork;
   1450         const raw = (try codecs.toBytes(&arena, pair.left)).?;
   1451         defer arena.allocator.free(raw);
   1452         const text = try source_text.toUtf8(&arena, pair.right);
   1453         defer arena.allocator.free(text);
   1454         try std.testing.expectEqualStrings(contents, raw);
   1455         try std.testing.expectEqualStrings(contents, text);
   1456     }
   1457     try expectErrorText(&arena, try testAction(&arena, 37, try source_text.fromUtf8(&arena, path), .{}), "permission denied");
   1458     for ([_][]const u8{ "\x80", "\xc0\xaf", "\xe2\x82", "\xed\xa0\x80", "\xf4\x90\x80\x80", "\xef\xbb\xbfok" }) |contents| {
   1459         try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "Ć©šŸ˜€.tri", .data = contents });
   1460         const reply = try testAction(&arena, 37, try source_text.fromUtf8(&arena, path), .{ .allow_read_all = true });
   1461         try expectErrorText(&arena, reply, path);
   1462         try expectErrorText(&arena, reply, if (std.mem.startsWith(u8, contents, "\xef\xbb\xbf")) "BOM is not permitted" else "invalid UTF-8 source");
   1463     }
   1464     // An embedded NUL must not turn a different path into the existing file.
   1465     const nul_path = try std.fmt.allocPrint(std.testing.allocator, "{s}\x00ignored", .{path});
   1466     defer std.testing.allocator.free(nul_path);
   1467     try expectErrorText(&arena, try testAction(&arena, 37, try source_text.fromUtf8(&arena, nul_path), .{ .allow_read_all = true }), "invalid string");
   1468     try tmp.dir.deleteFile(std.testing.io, "Ć©šŸ˜€.tri");
   1469     try expectErrorText(&arena, try testAction(&arena, 37, try source_text.fromUtf8(&arena, path), .{ .allow_read_all = true }), "does not exist");
   1470     // Ordinary readFile remains a byte transport: malformed UTF-8 and BOMs
   1471     // are not interpreted. Use its existing byte-oriented path convention.
   1472     const binary = "\xff\x00\xef\xbb\xbf\r\n";
   1473     try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "Ć©šŸ˜€.tri", .data = binary });
   1474     const reply = try testAction(&arena, 20, try codecs.ofString(&arena, path), .{ .allow_read_all = true });
   1475     try std.testing.expect(reply.ok);
   1476     const raw = (try codecs.toBytes(&arena, reply.value)).?;
   1477     defer arena.allocator.free(raw);
   1478     try std.testing.expectEqualStrings(binary, raw);
   1479 }
   1480 
   1481 fn sourceIOAllocationProbe(allocator: std.mem.Allocator, path: []const u8) !void {
   1482     var arena = Arena.init(allocator);
   1483     defer arena.deinit();
   1484     const reply = try testAction(&arena, 37, try source_text.fromUtf8(&arena, path), .{ .allow_read_all = true });
   1485     try std.testing.expect(reply.ok);
   1486     const scalars = arena.get(reply.value).fork.right;
   1487     const encoded = try testAction(&arena, 38, scalars, .{});
   1488     try std.testing.expect(encoded.ok);
   1489     const raw = (try codecs.toBytes(&arena, encoded.value)).?;
   1490     defer allocator.free(raw);
   1491     try std.testing.expectEqualStrings("aĆ©šŸ˜€", raw);
   1492 }
   1493 
   1494 test "source IO unwinds allocation failures without leaking tasks, buffers or requests" {
   1495     var tmp = std.testing.tmpDir(.{});
   1496     defer tmp.cleanup();
   1497     try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "source.tri", .data = "aĆ©šŸ˜€" });
   1498     const path = try tmp.dir.realPathFileAlloc(std.testing.io, "source.tri", std.testing.allocator);
   1499     defer std.testing.allocator.free(path);
   1500     try std.testing.checkAllAllocationFailures(std.testing.allocator, sourceIOAllocationProbe, .{path});
   1501 }
   1502 
   1503 fn testIndexedAction(arena: *Arena, tag: u64, path: []const u8, hash: []const u8, value: u32, perms: IOPerms) !codecs.UnwrapResult {
   1504     const path_tree = try source_text.fromUtf8(arena, path);
   1505     const digest_tree = try codecs.ofString(arena, hash);
   1506     const rest = if (tag == 52) try arena.alloc(.{ .fork = .{ .left = digest_tree, .right = value } }) else digest_tree;
   1507     const args = try arena.alloc(.{ .fork = .{ .left = path_tree, .right = rest } });
   1508     return testAction(arena, tag, args, perms);
   1509 }
   1510 
   1511 test "indexed IO authenticates, enforces permissions before demand, and never replaces a different winner" {
   1512     var tmp = std.testing.tmpDir(.{});
   1513     defer tmp.cleanup();
   1514     const directory = try tmp.dir.realPathFileAlloc(std.testing.io, ".", std.testing.allocator);
   1515     defer std.testing.allocator.free(directory);
   1516     const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/Ć©.arboricx", .{directory});
   1517     defer std.testing.allocator.free(path);
   1518     var arena = Arena.init(std.testing.allocator);
   1519     defer arena.deinit();
   1520     const leaf = try arena.alloc(.leaf);
   1521     // Test helpers turn on collection; retain the tree across independent IO calls.
   1522     try arena.roots.append(arena.allocator, leaf);
   1523     const bytes = try indexed.encode(&arena, leaf);
   1524     defer arena.allocator.free(bytes);
   1525     const hash = indexed.digest(bytes);
   1526     const perms = IOPerms{ .allow_read_all = true, .allow_write_all = true };
   1527     const hashed = try testAction(&arena, 51, leaf, .{});
   1528     try std.testing.expect(hashed.ok);
   1529     const actual = (try codecs.toString(&arena, hashed.value)).?;
   1530     defer arena.allocator.free(actual);
   1531     try std.testing.expectEqualStrings(&hash, actual);
   1532     for ([_]bool{ true, false }) |fresh| {
   1533         const result = try testIndexedAction(&arena, 52, path, &hash, leaf, perms);
   1534         try std.testing.expect(result.ok);
   1535         try std.testing.expectEqual(fresh, (try codecs.toBool(&arena, result.value)).?);
   1536     }
   1537     const read = try testIndexedAction(&arena, 50, path, &hash, leaf, .{ .allow_read_all = true });
   1538     try std.testing.expect(read.ok and arena.get(read.value).* == .leaf);
   1539     // A denied write must not try to normalize a divergent supplied tree.
   1540     const poison = try arena.alloc(.leaf);
   1541     arena.get(poison).* = .{ .app = .{ .func = poison, .arg = leaf } };
   1542     try arena.roots.append(arena.allocator, poison);
   1543     for ([_]IOPerms{ .{}, .{ .allow_read_all = true }, .{ .allow_write_all = true } }) |denied| {
   1544         try expectErrorText(&arena, try testIndexedAction(&arena, 52, path, &hash, poison, denied), "permission denied");
   1545     }
   1546     try expectErrorText(&arena, try testIndexedAction(&arena, 50, path, &hash, leaf, .{}), "permission denied");
   1547     try expectErrorText(&arena, try testIndexedAction(&arena, 50, path, "ABC", leaf, perms), "invalid CAS digest");
   1548     const wrong = "0" ** 64;
   1549     try expectErrorText(&arena, try testIndexedAction(&arena, 50, path, wrong, leaf, perms), "CAS digest mismatch");
   1550     try expectErrorText(&arena, try testIndexedAction(&arena, 52, path, wrong, leaf, perms), "CAS digest mismatch");
   1551     try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "Ć©.arboricx", .data = "corrupt" });
   1552     try expectErrorText(&arena, try testIndexedAction(&arena, 52, path, &hash, leaf, perms), "different bytes");
   1553     const still_corrupt = try readFsForTest(&arena, path);
   1554     defer arena.allocator.free(still_corrupt);
   1555     try std.testing.expectEqualStrings("corrupt", still_corrupt);
   1556     const corrupt_hash = indexed.digest("corrupt");
   1557     try expectErrorText(&arena, try testIndexedAction(&arena, 50, path, &corrupt_hash, leaf, perms), "invalid indexed tree");
   1558 }
   1559 
   1560 fn readFsForTest(arena: *Arena, path: []const u8) ![]u8 {
   1561     var loop: c.uv_loop_t = undefined;
   1562     try std.testing.expectEqual(@as(c_int, 0), c.uv_loop_init(&loop));
   1563     defer _ = c.uv_loop_close(&loop);
   1564     const z = try arena.allocator.dupeZ(u8, path);
   1565     defer arena.allocator.free(z);
   1566     return switch (try readFs(&loop, arena.allocator, z)) {
   1567         .bytes => |b| b,
   1568         .failure => error.TestUnexpectedResult,
   1569     };
   1570 }
   1571 
   1572 fn indexedIOAllocationProbe(allocator: std.mem.Allocator, path: []const u8) !void {
   1573     var arena = Arena.init(allocator);
   1574     defer arena.deinit();
   1575     const leaf = try arena.alloc(.leaf);
   1576     try arena.roots.append(allocator, leaf);
   1577     const hash = "6f93378feaa8bf5ab7cefc071175a654c871bfd53f165d7d96795fb2b38483f1";
   1578     const perms = IOPerms{ .allow_read_all = true, .allow_write_all = true };
   1579     const written = try testIndexedAction(&arena, 52, path, hash, leaf, perms);
   1580     try std.testing.expect(written.ok);
   1581     const read = try testIndexedAction(&arena, 50, path, hash, leaf, perms);
   1582     try std.testing.expect(read.ok and arena.get(read.value).* == .leaf);
   1583 }
   1584 
   1585 test "indexed publication and reads unwind every allocator failure" {
   1586     var tmp = std.testing.tmpDir(.{});
   1587     defer tmp.cleanup();
   1588     const directory = try tmp.dir.realPathFileAlloc(std.testing.io, ".", std.testing.allocator);
   1589     defer std.testing.allocator.free(directory);
   1590     const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/object", .{directory});
   1591     defer std.testing.allocator.free(path);
   1592     try std.testing.checkAllAllocationFailures(std.testing.allocator, indexedIOAllocationProbe, .{path});
   1593 }