Interaction Trees in Zig and simple benchmarks
This commit is contained in:
@@ -31,6 +31,8 @@ pub fn build(b: *std.Build) void {
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe_mod.addImport("kernel_embed", kernel_mod);
|
||||
exe_mod.link_libc = true;
|
||||
exe_mod.linkSystemLibrary("uv", .{});
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "tricu-zig",
|
||||
.root_module = exe_mod,
|
||||
@@ -50,6 +52,8 @@ pub fn build(b: *std.Build) void {
|
||||
});
|
||||
lib_mod.pic = true;
|
||||
lib_mod.addImport("kernel_embed", kernel_mod);
|
||||
lib_mod.link_libc = true;
|
||||
lib_mod.linkSystemLibrary("uv", .{});
|
||||
const static_lib = b.addLibrary(.{
|
||||
.name = "arboricx",
|
||||
.root_module = lib_mod,
|
||||
|
||||
@@ -40,6 +40,25 @@ int arb_to_bool(arb_ctx_t* ctx, uint32_t root, int* out);
|
||||
int arb_unwrap_result(arb_ctx_t* ctx, uint32_t root, int* out_ok, uint32_t* out_value, uint32_t* out_rest);
|
||||
int arb_unwrap_host_value(arb_ctx_t* ctx, uint32_t root, uint64_t* out_tag, uint32_t* out_payload);
|
||||
|
||||
/* Tree inspection (Layer 1 — for custom IO drivers and non-POSIX hosts) */
|
||||
int arb_is_leaf(arb_ctx_t* ctx, uint32_t root);
|
||||
int arb_is_stem(arb_ctx_t* ctx, uint32_t root);
|
||||
int arb_is_fork(arb_ctx_t* ctx, uint32_t root);
|
||||
int arb_is_app(arb_ctx_t* ctx, uint32_t root);
|
||||
int arb_get_stem_child(arb_ctx_t* ctx, uint32_t root, uint32_t* out);
|
||||
int arb_get_fork_children(arb_ctx_t* ctx, uint32_t root,
|
||||
uint32_t* out_left, uint32_t* out_right);
|
||||
int arb_get_app_func_arg(arb_ctx_t* ctx, uint32_t root,
|
||||
uint32_t* out_func, uint32_t* out_arg);
|
||||
|
||||
/* IO driver (Layer 2 — POSIX interaction-tree runtime) */
|
||||
typedef struct {
|
||||
int allow_read_all;
|
||||
int allow_write_all;
|
||||
} arb_io_perms_t;
|
||||
|
||||
uint32_t arb_run_io(arb_ctx_t* ctx, uint32_t program, const arb_io_perms_t* perms);
|
||||
|
||||
/* Kernel entrypoints */
|
||||
uint32_t arb_kernel_root(arb_ctx_t* ctx);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const reduce = @import("reduce.zig");
|
||||
const codecs = @import("codecs.zig");
|
||||
const kernel = @import("kernel.zig");
|
||||
const bundle = @import("bundle.zig");
|
||||
const io_driver = @import("io_driver.zig");
|
||||
|
||||
/// Opaque handle for the C API. Layout is not exposed to C.
|
||||
/// Holds a persistent arena for user-built terms and the kernel.
|
||||
@@ -59,6 +60,57 @@ export fn arb_app(ctx: *ArbCtx, func: u32, arg: u32) u32 {
|
||||
return ctx.arena.alloc(.{ .app = .{ .func = func, .arg = arg } }) catch 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tree inspection (Layer 1 — for custom IO drivers and non-POSIX hosts)
|
||||
// All return 1 on success / true, 0 on failure / false.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export fn arb_is_leaf(ctx: *ArbCtx, root: u32) c_int {
|
||||
if (root >= ctx.arena.len()) return 0;
|
||||
return if (ctx.arena.nodes.items[root] == .leaf) 1 else 0;
|
||||
}
|
||||
|
||||
export fn arb_is_stem(ctx: *ArbCtx, root: u32) c_int {
|
||||
if (root >= ctx.arena.len()) return 0;
|
||||
return if (ctx.arena.nodes.items[root] == .stem) 1 else 0;
|
||||
}
|
||||
|
||||
export fn arb_is_fork(ctx: *ArbCtx, root: u32) c_int {
|
||||
if (root >= ctx.arena.len()) return 0;
|
||||
return if (ctx.arena.nodes.items[root] == .fork) 1 else 0;
|
||||
}
|
||||
|
||||
export fn arb_is_app(ctx: *ArbCtx, root: u32) c_int {
|
||||
if (root >= ctx.arena.len()) return 0;
|
||||
return if (ctx.arena.nodes.items[root] == .app) 1 else 0;
|
||||
}
|
||||
|
||||
export fn arb_get_stem_child(ctx: *ArbCtx, root: u32, out: *u32) c_int {
|
||||
if (root >= ctx.arena.len()) return 0;
|
||||
const node = ctx.arena.nodes.items[root];
|
||||
if (node != .stem) return 0;
|
||||
out.* = node.stem.child;
|
||||
return 1;
|
||||
}
|
||||
|
||||
export fn arb_get_fork_children(ctx: *ArbCtx, root: u32, out_left: *u32, out_right: *u32) c_int {
|
||||
if (root >= ctx.arena.len()) return 0;
|
||||
const node = ctx.arena.nodes.items[root];
|
||||
if (node != .fork) return 0;
|
||||
out_left.* = node.fork.left;
|
||||
out_right.* = node.fork.right;
|
||||
return 1;
|
||||
}
|
||||
|
||||
export fn arb_get_app_func_arg(ctx: *ArbCtx, root: u32, out_func: *u32, out_arg: *u32) c_int {
|
||||
if (root >= ctx.arena.len()) return 0;
|
||||
const node = ctx.arena.nodes.items[root];
|
||||
if (node != .app) return 0;
|
||||
out_func.* = node.app.func;
|
||||
out_arg.* = node.app.arg;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reduction
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -157,6 +209,23 @@ export fn arb_unwrap_host_value(ctx: *ArbCtx, root: u32, out_tag: *u64, out_payl
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IO driver (Layer 2 — POSIX interaction-tree runtime)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub const arb_io_perms_t = extern struct {
|
||||
allow_read_all: c_int,
|
||||
allow_write_all: c_int,
|
||||
};
|
||||
|
||||
export fn arb_run_io(ctx: *ArbCtx, program: u32, perms: ?*const arb_io_perms_t) u32 {
|
||||
const zig_perms = if (perms) |p| io_driver.IOPerms{
|
||||
.allow_read_all = p.allow_read_all != 0,
|
||||
.allow_write_all = p.allow_write_all != 0,
|
||||
} else io_driver.IOPerms{};
|
||||
return io_driver.runIO(ctx.gpa, &ctx.arena, program, zig_perms) catch 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kernel entrypoints
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
845
ext/zig/src/io_driver.zig
Normal file
845
ext/zig/src/io_driver.zig
Normal file
@@ -0,0 +1,845 @@
|
||||
const std = @import("std");
|
||||
const Arena = @import("arena.zig").Arena;
|
||||
const codecs = @import("codecs.zig");
|
||||
const reduce = @import("reduce.zig");
|
||||
const tree = @import("tree.zig");
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("uv.h");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action tag constants (must match lib/io.tri and IODriver.hs)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub const ActionTag = enum(u8) {
|
||||
pure = 0,
|
||||
bind = 1,
|
||||
putStr = 10,
|
||||
getLine = 11,
|
||||
readFile = 20,
|
||||
writeFile = 21,
|
||||
ask = 30,
|
||||
local = 31,
|
||||
get = 40,
|
||||
put = 41,
|
||||
fork = 60,
|
||||
await = 61,
|
||||
yield = 62,
|
||||
sleep = 63,
|
||||
};
|
||||
|
||||
pub const Action = union(ActionTag) {
|
||||
pure: u32,
|
||||
bind: struct { left: u32, k: u32 },
|
||||
putStr: u32,
|
||||
getLine,
|
||||
readFile: u32,
|
||||
writeFile: struct { path: u32, contents: u32 },
|
||||
ask,
|
||||
local: struct { f: u32, action: u32 },
|
||||
get,
|
||||
put: u32,
|
||||
fork: u32,
|
||||
await: u32,
|
||||
yield,
|
||||
sleep: u32,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error codes (must match IODriver.hs)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ERR_DOES_NOT_EXIST: u64 = 1;
|
||||
const ERR_PERMISSION: u64 = 2;
|
||||
const ERR_ALREADY_EXISTS: u64 = 3;
|
||||
const ERR_IO_OTHER: u64 = 4;
|
||||
const ERR_POLICY_DENY: u64 = 20;
|
||||
const ERR_INVALID_ACTION: u64 = 40;
|
||||
const ERR_INVALID_STRING: u64 = 41;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Permissions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub const IOPerms = struct {
|
||||
allow_read_all: bool = false,
|
||||
allow_write_all: bool = false,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IO sentinel detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn isIOSentinel(arena: *Arena, root: u32) !?u32 {
|
||||
const node = arena.get(root);
|
||||
if (node.* != .fork) return null;
|
||||
|
||||
const sentinel = node.fork.left;
|
||||
const rest = node.fork.right;
|
||||
|
||||
const sentinel_str = try codecs.toString(arena, sentinel);
|
||||
defer {
|
||||
if (sentinel_str) |s| {
|
||||
arena.allocator.free(s);
|
||||
}
|
||||
}
|
||||
if (sentinel_str == null) return null;
|
||||
if (!std.mem.eql(u8, sentinel_str.?, "tricuIO")) return null;
|
||||
|
||||
const rest_node = arena.get(rest);
|
||||
if (rest_node.* != .fork) return null;
|
||||
|
||||
const version_num = try codecs.toNumber(arena, rest_node.fork.left);
|
||||
if (version_num == null or version_num.? != 1) return null;
|
||||
|
||||
return rest_node.fork.right;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action decoding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn decodeAction(arena: *Arena, root: u32) !?Action {
|
||||
const node = arena.get(root);
|
||||
if (node.* != .fork) return null;
|
||||
|
||||
const tag_num = try codecs.toNumber(arena, node.fork.left);
|
||||
if (tag_num == null) return null;
|
||||
|
||||
const tag: ActionTag = switch (tag_num.?) {
|
||||
0 => .pure,
|
||||
1 => .bind,
|
||||
10 => .putStr,
|
||||
11 => .getLine,
|
||||
20 => .readFile,
|
||||
21 => .writeFile,
|
||||
30 => .ask,
|
||||
31 => .local,
|
||||
40 => .get,
|
||||
41 => .put,
|
||||
60 => .fork,
|
||||
61 => .await,
|
||||
62 => .yield,
|
||||
63 => .sleep,
|
||||
else => return null,
|
||||
};
|
||||
|
||||
const payload = node.fork.right;
|
||||
|
||||
return switch (tag) {
|
||||
.pure => Action{ .pure = payload },
|
||||
.bind => blk: {
|
||||
const payload_node = arena.get(payload);
|
||||
if (payload_node.* != .fork) return null;
|
||||
break :blk Action{ .bind = .{ .left = payload_node.fork.left, .k = payload_node.fork.right } };
|
||||
},
|
||||
.putStr => Action{ .putStr = payload },
|
||||
.getLine => Action.getLine,
|
||||
.readFile => Action{ .readFile = payload },
|
||||
.writeFile => blk: {
|
||||
const payload_node = arena.get(payload);
|
||||
if (payload_node.* != .fork) return null;
|
||||
break :blk Action{ .writeFile = .{ .path = payload_node.fork.left, .contents = payload_node.fork.right } };
|
||||
},
|
||||
.ask => Action.ask,
|
||||
.local => blk: {
|
||||
const payload_node = arena.get(payload);
|
||||
if (payload_node.* != .fork) return null;
|
||||
break :blk Action{ .local = .{ .f = payload_node.fork.left, .action = payload_node.fork.right } };
|
||||
},
|
||||
.get => Action.get,
|
||||
.put => Action{ .put = payload },
|
||||
.fork => Action{ .fork = payload },
|
||||
.await => Action{ .await = payload },
|
||||
.yield => Action.yield,
|
||||
.sleep => Action{ .sleep = payload },
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Response tree constructors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn makePure(arena: *Arena, val: u32) !u32 {
|
||||
const tag = try codecs.ofNumber(arena, 0);
|
||||
return try arena.alloc(.{ .fork = .{ .left = tag, .right = val } });
|
||||
}
|
||||
|
||||
pub fn makeOkResult(arena: *Arena, val: u32) !u32 {
|
||||
const ok_tag = try arena.alloc(.{ .stem = .{ .child = try arena.alloc(.leaf) } });
|
||||
const val_pair = try arena.alloc(.{ .fork = .{ .left = val, .right = try arena.alloc(.leaf) } });
|
||||
return try arena.alloc(.{ .fork = .{ .left = ok_tag, .right = val_pair } });
|
||||
}
|
||||
|
||||
pub fn makeErrResult(arena: *Arena, code: u64) !u32 {
|
||||
const code_tree = try codecs.ofNumber(arena, code);
|
||||
const code_pair = try arena.alloc(.{ .fork = .{ .left = code_tree, .right = try arena.alloc(.leaf) } });
|
||||
return try arena.alloc(.{ .fork = .{ .left = try arena.alloc(.leaf), .right = code_pair } });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Frame stack and runtime
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Frame = union(enum) {
|
||||
bind: u32, // continuation k
|
||||
local: u32, // old env
|
||||
};
|
||||
|
||||
const Runtime = struct {
|
||||
env: u32,
|
||||
state: u32,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: reduce a term in a scratch arena and copy the result back
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn reduceInScratch(gpa: std.mem.Allocator, arena: *Arena, term: u32) !u32 {
|
||||
var scratch = Arena.init(gpa);
|
||||
defer scratch.deinit();
|
||||
const scratch_root = try tree.copyTree(arena.nodes.items, &scratch, term);
|
||||
const scratch_result = try reduce.reduce(scratch_root, &scratch, std.math.maxInt(u64));
|
||||
return try tree.copyTree(scratch.nodes.items, arena, scratch_result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Task = struct {
|
||||
id: u64,
|
||||
parent: ?*Task,
|
||||
frames: std.ArrayList(Frame),
|
||||
runtime: Runtime,
|
||||
current: u32,
|
||||
status: enum { runnable, blocked, completed },
|
||||
result: ?u32,
|
||||
waiting_for: ?u64,
|
||||
|
||||
fn init(gpa: std.mem.Allocator, id: u64, parent: ?*Task, env: u32, state: u32, current: u32) !*Task {
|
||||
const task = try gpa.create(Task);
|
||||
task.* = .{
|
||||
.id = id,
|
||||
.parent = parent,
|
||||
.frames = std.ArrayList(Frame).empty,
|
||||
.runtime = .{ .env = env, .state = state },
|
||||
.current = current,
|
||||
.status = .runnable,
|
||||
.result = null,
|
||||
.waiting_for = null,
|
||||
};
|
||||
return task;
|
||||
}
|
||||
|
||||
fn deinit(self: *Task, gpa: std.mem.Allocator) void {
|
||||
self.frames.deinit(gpa);
|
||||
gpa.destroy(self);
|
||||
}
|
||||
|
||||
// finishValue processes a value through the frame stack.
|
||||
// Returns true if the task has completed (no more frames).
|
||||
fn finishValue(self: *Task, arena: *Arena, value: u32) !bool {
|
||||
if (self.frames.pop()) |frame| {
|
||||
switch (frame) {
|
||||
.bind => |k| {
|
||||
self.current = try arena.alloc(.{ .app = .{ .func = k, .arg = value } });
|
||||
return false;
|
||||
},
|
||||
.local => |old_env| {
|
||||
self.runtime.env = old_env;
|
||||
self.current = try makePure(arena, value);
|
||||
return false;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
self.current = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Scheduler = struct {
|
||||
gpa: std.mem.Allocator,
|
||||
loop: *c.uv_loop_t,
|
||||
arena: *Arena,
|
||||
tasks: std.ArrayList(*Task),
|
||||
runnable: std.ArrayList(*Task),
|
||||
next_id: u64,
|
||||
perms: IOPerms,
|
||||
|
||||
fn init(gpa: std.mem.Allocator, loop: *c.uv_loop_t, arena: *Arena, perms: IOPerms) !Scheduler {
|
||||
const sched = Scheduler{
|
||||
.gpa = gpa,
|
||||
.loop = loop,
|
||||
.arena = arena,
|
||||
.tasks = std.ArrayList(*Task).empty,
|
||||
.runnable = std.ArrayList(*Task).empty,
|
||||
.next_id = 1,
|
||||
.perms = perms,
|
||||
};
|
||||
return sched;
|
||||
}
|
||||
|
||||
fn deinit(self: *Scheduler) void {
|
||||
for (self.tasks.items) |task| {
|
||||
task.deinit(self.gpa);
|
||||
}
|
||||
self.tasks.deinit(self.gpa);
|
||||
self.runnable.deinit(self.gpa);
|
||||
}
|
||||
|
||||
fn createTask(self: *Scheduler, parent: ?*Task, env: u32, state: u32, current: u32) !*Task {
|
||||
const id = self.next_id;
|
||||
self.next_id += 1;
|
||||
const task = try Task.init(self.gpa, id, parent, env, state, current);
|
||||
try self.tasks.append(self.gpa, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
fn run(self: *Scheduler) !void {
|
||||
while (true) {
|
||||
if (self.runnable.items.len > 0) {
|
||||
const task = self.runnable.orderedRemove(0);
|
||||
try self.stepTask(task);
|
||||
} else if (self.hasPendingHandles()) {
|
||||
_ = c.uv_run(self.loop, c.UV_RUN_ONCE);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hasPendingHandles(self: *Scheduler) bool {
|
||||
return c.uv_loop_alive(self.loop) != 0;
|
||||
}
|
||||
|
||||
fn completeTask(self: *Scheduler, task: *Task) !void {
|
||||
task.status = .completed;
|
||||
task.result = task.current;
|
||||
// Unblock any tasks waiting for this one
|
||||
for (self.tasks.items) |t| {
|
||||
if (t.status == .blocked and t.waiting_for == task.id) {
|
||||
t.status = .runnable;
|
||||
t.waiting_for = null;
|
||||
t.current = try makePure(self.arena, task.result.?);
|
||||
try self.runnable.append(self.gpa, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stepTask(self: *Scheduler, task: *Task) !void {
|
||||
const reduced = try reduceInScratch(self.gpa, self.arena, task.current);
|
||||
|
||||
const decoded = try decodeAction(self.arena, reduced);
|
||||
if (decoded == null) {
|
||||
// Not a recognized action — if no frames, it's the final result.
|
||||
// Otherwise treat as invalid.
|
||||
if (task.frames.items.len == 0) {
|
||||
task.current = reduced;
|
||||
try self.completeTask(task);
|
||||
return;
|
||||
}
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (decoded.?) {
|
||||
.pure => |val| {
|
||||
if (try task.finishValue(self.arena, val)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.bind => |b| {
|
||||
try task.frames.append(self.gpa, .{ .bind = b.k });
|
||||
task.current = b.left;
|
||||
try self.runnable.append(self.gpa, task);
|
||||
},
|
||||
|
||||
.putStr => |str_tree| {
|
||||
const str = try codecs.toString(self.arena, str_tree) orelse {
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
};
|
||||
defer self.gpa.free(str);
|
||||
_ = std.c.write(1, str.ptr, str.len);
|
||||
const leaf = try self.arena.alloc(.leaf);
|
||||
if (try task.finishValue(self.arena, leaf)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.getLine => {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var len: usize = 0;
|
||||
while (len < buf.len) {
|
||||
const n = std.c.read(0, buf[len..].ptr, 1);
|
||||
if (n <= 0) break;
|
||||
if (buf[len] == '\n') break;
|
||||
len += 1;
|
||||
}
|
||||
const line = buf[0..len];
|
||||
const str_tree = try codecs.ofString(self.arena, line);
|
||||
if (try task.finishValue(self.arena, str_tree)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.readFile => |path_tree| {
|
||||
const path = try codecs.toString(self.arena, path_tree) orelse {
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if (!self.perms.allow_read_all) {
|
||||
self.arena.allocator.free(path);
|
||||
const err = try makeErrResult(self.arena, ERR_POLICY_DENY);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = try self.gpa.create(FileReadCtx);
|
||||
ctx.* = .{
|
||||
.scheduler = self,
|
||||
.task = task,
|
||||
.arena = self.arena,
|
||||
.gpa = self.gpa,
|
||||
.fd = -1,
|
||||
.buf = std.ArrayList(u8).empty,
|
||||
.path = path,
|
||||
.req = undefined,
|
||||
.read_buf = null,
|
||||
};
|
||||
ctx.req.data = ctx;
|
||||
_ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, c.O_RDONLY, 0, file_open_cb);
|
||||
},
|
||||
|
||||
.writeFile => |wf| {
|
||||
const path = try codecs.toString(self.arena, wf.path) orelse {
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const contents = try codecs.toString(self.arena, wf.contents) orelse {
|
||||
self.arena.allocator.free(path);
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_STRING);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if (!self.perms.allow_write_all) {
|
||||
self.arena.allocator.free(path);
|
||||
self.arena.allocator.free(contents);
|
||||
const err = try makeErrResult(self.arena, ERR_POLICY_DENY);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = try self.gpa.create(FileWriteCtx);
|
||||
ctx.* = .{
|
||||
.scheduler = self,
|
||||
.task = task,
|
||||
.arena = self.arena,
|
||||
.gpa = self.gpa,
|
||||
.fd = -1,
|
||||
.path = path,
|
||||
.contents = contents,
|
||||
.written = false,
|
||||
.req = undefined,
|
||||
};
|
||||
ctx.req.data = ctx;
|
||||
const flags = c.O_WRONLY | c.O_CREAT | c.O_TRUNC;
|
||||
_ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, flags, 0o644, file_write_open_cb);
|
||||
},
|
||||
|
||||
.ask => {
|
||||
if (try task.finishValue(self.arena, task.runtime.env)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.local => |loc| {
|
||||
const new_env = try reduceInScratch(self.gpa, self.arena, try self.arena.alloc(.{ .app = .{ .func = loc.f, .arg = task.runtime.env } }));
|
||||
try task.frames.append(self.gpa, .{ .local = task.runtime.env });
|
||||
task.runtime.env = new_env;
|
||||
task.current = loc.action;
|
||||
try self.runnable.append(self.gpa, task);
|
||||
},
|
||||
|
||||
.get => {
|
||||
if (try task.finishValue(self.arena, task.runtime.state)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.put => |new_state| {
|
||||
task.runtime.state = new_state;
|
||||
const leaf = try self.arena.alloc(.leaf);
|
||||
if (try task.finishValue(self.arena, leaf)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.fork => |action| {
|
||||
const child = try self.createTask(task, task.runtime.env, task.runtime.state, action);
|
||||
try self.runnable.append(self.gpa, child);
|
||||
const handle = try codecs.ofNumber(self.arena, child.id);
|
||||
if (try task.finishValue(self.arena, handle)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.await => |handle_tree| {
|
||||
const handle = try codecs.toNumber(self.arena, handle_tree) orelse {
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
};
|
||||
var found: ?*Task = null;
|
||||
for (self.tasks.items) |t| {
|
||||
if (t.id == handle) {
|
||||
found = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == null) {
|
||||
const err = try makeErrResult(self.arena, ERR_INVALID_ACTION);
|
||||
if (try task.finishValue(self.arena, err)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (found.?.status == .completed) {
|
||||
const result = found.?.result.?;
|
||||
if (try task.finishValue(self.arena, result)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
} else {
|
||||
task.status = .blocked;
|
||||
task.waiting_for = handle;
|
||||
// Task remains out of runnable until child completes
|
||||
}
|
||||
},
|
||||
|
||||
.yield => {
|
||||
const leaf = try self.arena.alloc(.leaf);
|
||||
if (try task.finishValue(self.arena, leaf)) {
|
||||
try self.completeTask(task);
|
||||
} else {
|
||||
try self.runnable.append(self.gpa, task);
|
||||
}
|
||||
},
|
||||
|
||||
.sleep => |ms_tree| {
|
||||
const ms = try codecs.toNumber(self.arena, ms_tree) orelse 0;
|
||||
const ctx = try self.gpa.create(SleepCtx);
|
||||
ctx.* = .{
|
||||
.scheduler = self,
|
||||
.task = task,
|
||||
.arena = self.arena,
|
||||
.timer = undefined,
|
||||
};
|
||||
ctx.timer.data = ctx;
|
||||
_ = c.uv_timer_init(self.loop, &ctx.timer);
|
||||
_ = c.uv_timer_start(&ctx.timer, sleep_cb, @intCast(ms), 0);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Async file read
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const FileReadCtx = struct {
|
||||
scheduler: *Scheduler,
|
||||
task: *Task,
|
||||
arena: *Arena,
|
||||
gpa: std.mem.Allocator,
|
||||
fd: c_int,
|
||||
buf: std.ArrayList(u8),
|
||||
path: []const u8,
|
||||
req: c.uv_fs_t,
|
||||
read_buf: ?[]u8,
|
||||
};
|
||||
|
||||
fn mapUvErr(uv_err: c_int) u64 {
|
||||
return switch (uv_err) {
|
||||
c.UV_ENOENT => ERR_DOES_NOT_EXIST,
|
||||
c.UV_EACCES => ERR_PERMISSION,
|
||||
c.UV_EEXIST => ERR_ALREADY_EXISTS,
|
||||
else => ERR_IO_OTHER,
|
||||
};
|
||||
}
|
||||
|
||||
fn file_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||
const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data)));
|
||||
const result = req.*.result;
|
||||
c.uv_fs_req_cleanup(req);
|
||||
if (result < 0) {
|
||||
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-result))) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
ctx.buf.deinit(ctx.gpa);
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
}
|
||||
ctx.fd = @intCast(result);
|
||||
const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable;
|
||||
ctx.read_buf = read_buf;
|
||||
var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len));
|
||||
_ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb);
|
||||
}
|
||||
|
||||
fn file_read_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||
const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data)));
|
||||
const nread = req.*.result;
|
||||
c.uv_fs_req_cleanup(req);
|
||||
if (nread < 0) {
|
||||
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
|
||||
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-nread))) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
if (ctx.read_buf) |b| ctx.gpa.free(b);
|
||||
ctx.buf.deinit(ctx.gpa);
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
}
|
||||
if (nread == 0) {
|
||||
// EOF
|
||||
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
|
||||
const bytes_tree = codecs.ofBytes(ctx.arena, ctx.buf.items) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
const ok = makeOkResult(ctx.arena, bytes_tree) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, ok) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
if (ctx.read_buf) |b| ctx.gpa.free(b);
|
||||
ctx.buf.deinit(ctx.gpa);
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
}
|
||||
const data = ctx.read_buf.?[0..@intCast(nread)];
|
||||
ctx.buf.appendSlice(ctx.gpa, data) catch unreachable;
|
||||
const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable;
|
||||
ctx.read_buf = read_buf;
|
||||
var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len));
|
||||
_ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Async file write
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const FileWriteCtx = struct {
|
||||
scheduler: *Scheduler,
|
||||
task: *Task,
|
||||
arena: *Arena,
|
||||
gpa: std.mem.Allocator,
|
||||
fd: c_int,
|
||||
path: []const u8,
|
||||
contents: []const u8,
|
||||
written: bool,
|
||||
req: c.uv_fs_t,
|
||||
};
|
||||
|
||||
fn file_write_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
|
||||
const result = req.*.result;
|
||||
c.uv_fs_req_cleanup(req);
|
||||
if (result < 0) {
|
||||
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-result))) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.free(ctx.contents);
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
}
|
||||
ctx.fd = @intCast(result);
|
||||
var uv_buf = c.uv_buf_init(@ptrCast(@constCast(ctx.contents.ptr)), @intCast(ctx.contents.len));
|
||||
_ = c.uv_fs_write(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, 0, file_write_cb);
|
||||
}
|
||||
|
||||
fn file_write_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
|
||||
const nwrite = req.*.result;
|
||||
c.uv_fs_req_cleanup(req);
|
||||
if (nwrite < 0) {
|
||||
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null);
|
||||
const err = makeErrResult(ctx.arena, mapUvErr(@intCast(-nwrite))) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, err) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.free(ctx.contents);
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
}
|
||||
_ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, file_write_close_cb);
|
||||
}
|
||||
|
||||
fn file_write_close_cb(req: [*c]c.uv_fs_t) callconv(.c) void {
|
||||
const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data)));
|
||||
c.uv_fs_req_cleanup(req);
|
||||
const leaf = ctx.arena.alloc(.leaf) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
const ok = makeOkResult(ctx.arena, leaf) catch {
|
||||
ctx.gpa.destroy(ctx);
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, ok) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
ctx.gpa.free(ctx.path);
|
||||
ctx.gpa.free(ctx.contents);
|
||||
ctx.gpa.destroy(ctx);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Async sleep
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SleepCtx = struct {
|
||||
scheduler: *Scheduler,
|
||||
task: *Task,
|
||||
arena: *Arena,
|
||||
timer: c.uv_timer_t,
|
||||
};
|
||||
|
||||
fn sleep_cb(handle: [*c]c.uv_timer_t) callconv(.c) void {
|
||||
const ctx = @as(*SleepCtx, @ptrCast(@alignCast(handle.*.data)));
|
||||
defer ctx.scheduler.gpa.destroy(ctx);
|
||||
const leaf = ctx.arena.alloc(.leaf) catch {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
return;
|
||||
};
|
||||
if (ctx.task.finishValue(ctx.arena, leaf) catch false) {
|
||||
ctx.scheduler.completeTask(ctx.task) catch {};
|
||||
} else {
|
||||
ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn runIO(gpa: std.mem.Allocator, arena: *Arena, program: u32, perms: IOPerms) !u32 {
|
||||
const action_tree = try isIOSentinel(arena, program) orelse {
|
||||
return error.InvalidIOSentinel;
|
||||
};
|
||||
|
||||
var loop: c.uv_loop_t = undefined;
|
||||
const rc = c.uv_loop_init(&loop);
|
||||
if (rc != 0) return error.LoopInitFailed;
|
||||
defer _ = c.uv_loop_close(&loop);
|
||||
|
||||
var scheduler = try Scheduler.init(gpa, &loop, arena, perms);
|
||||
defer scheduler.deinit();
|
||||
|
||||
const main_task = try scheduler.createTask(null, try arena.alloc(.leaf), try arena.alloc(.leaf), action_tree);
|
||||
try scheduler.runnable.append(gpa, main_task);
|
||||
|
||||
try scheduler.run();
|
||||
|
||||
// Return the main task's result
|
||||
return main_task.result orelse program;
|
||||
}
|
||||
@@ -5,6 +5,47 @@ const reduce = @import("reduce.zig");
|
||||
const codecs = @import("codecs.zig");
|
||||
const kernel = @import("kernel.zig");
|
||||
const bundle = @import("bundle.zig");
|
||||
const io_driver = @import("io_driver.zig");
|
||||
|
||||
fn printNode(arena: *Arena, tag: u64, node: u32, io: std.Io) !void {
|
||||
var stdout_buf: [4096]u8 = undefined;
|
||||
var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
|
||||
|
||||
switch (tag) {
|
||||
codecs.HOST_STRING_TAG => {
|
||||
const s = try codecs.toString(arena, node) orelse {
|
||||
try stdout.interface.writeAll("Error: failed to decode string result\n");
|
||||
try stdout.flush();
|
||||
return error.DecodeFailed;
|
||||
};
|
||||
defer arena.allocator.free(s);
|
||||
try stdout.interface.writeAll(s);
|
||||
try stdout.interface.writeAll("\n");
|
||||
},
|
||||
codecs.HOST_NUMBER_TAG => {
|
||||
const n = try codecs.toNumber(arena, node) orelse 0;
|
||||
try stdout.interface.print("{d}\n", .{n});
|
||||
},
|
||||
codecs.HOST_BOOL_TAG => {
|
||||
const b = try codecs.toBool(arena, node) orelse {
|
||||
try stdout.interface.writeAll("Error: failed to decode bool result\n");
|
||||
try stdout.flush();
|
||||
return error.DecodeFailed;
|
||||
};
|
||||
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
|
||||
},
|
||||
codecs.HOST_TREE_TAG => {
|
||||
try tree.formatTree(&stdout.interface, arena, node, 0);
|
||||
try stdout.interface.writeAll("\n");
|
||||
},
|
||||
else => {
|
||||
try stdout.interface.print("(tag={d}, payload=", .{tag});
|
||||
try tree.formatTree(&stdout.interface, arena, node, 0);
|
||||
try stdout.interface.writeAll(")\n");
|
||||
},
|
||||
}
|
||||
try stdout.flush();
|
||||
}
|
||||
|
||||
fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void {
|
||||
const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
|
||||
@@ -16,44 +57,29 @@ fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []cons
|
||||
}
|
||||
|
||||
const result = try reduce.reduce(current, arena, fuel);
|
||||
try printNode(arena, tag, result, io);
|
||||
}
|
||||
|
||||
var stdout_buf: [4096]u8 = undefined;
|
||||
var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
|
||||
fn runIO(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, perms: io_driver.IOPerms, io: std.Io) !void {
|
||||
const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
|
||||
|
||||
switch (tag) {
|
||||
codecs.HOST_STRING_TAG => {
|
||||
const s = try codecs.toString(arena, result) orelse {
|
||||
try stdout.interface.writeAll("Error: failed to decode string result\n");
|
||||
try stdout.flush();
|
||||
return error.DecodeFailed;
|
||||
};
|
||||
defer arena.allocator.free(s);
|
||||
try stdout.interface.writeAll(s);
|
||||
try stdout.interface.writeAll("\n");
|
||||
},
|
||||
codecs.HOST_NUMBER_TAG => {
|
||||
const n = try codecs.toNumber(arena, result) orelse 0;
|
||||
try stdout.interface.print("{d}\n", .{n});
|
||||
},
|
||||
codecs.HOST_BOOL_TAG => {
|
||||
const b = try codecs.toBool(arena, result) orelse {
|
||||
try stdout.interface.writeAll("Error: failed to decode bool result\n");
|
||||
try stdout.flush();
|
||||
return error.DecodeFailed;
|
||||
};
|
||||
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
|
||||
},
|
||||
codecs.HOST_TREE_TAG => {
|
||||
try tree.formatTree(&stdout.interface, arena, result, 0);
|
||||
try stdout.interface.writeAll("\n");
|
||||
},
|
||||
else => {
|
||||
try stdout.interface.print("(tag={d}, payload=", .{tag});
|
||||
try tree.formatTree(&stdout.interface, arena, result, 0);
|
||||
try stdout.interface.writeAll(")\n");
|
||||
},
|
||||
var current = term;
|
||||
for (args_raw) |arg| {
|
||||
const arg_tree = try parseArg(arena, io, arg);
|
||||
current = try arena.alloc(.{ .app = .{ .func = current, .arg = arg_tree } });
|
||||
}
|
||||
try stdout.flush();
|
||||
|
||||
const reduced = try reduce.reduce(current, arena, fuel);
|
||||
|
||||
if (try io_driver.isIOSentinel(arena, reduced) == null) {
|
||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||
try stderr.interface.writeAll("Error: reduced term is not a valid IO program\n");
|
||||
try stderr.flush();
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
const result = try io_driver.runIO(arena.allocator, arena, reduced, perms);
|
||||
try printNode(arena, tag, result, io);
|
||||
}
|
||||
|
||||
fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void {
|
||||
@@ -98,43 +124,7 @@ fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []cons
|
||||
return error.InvalidHostValue;
|
||||
};
|
||||
|
||||
var stdout_buf: [4096]u8 = undefined;
|
||||
var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
|
||||
|
||||
switch (hv.tag) {
|
||||
codecs.HOST_STRING_TAG => {
|
||||
const s = try codecs.toString(arena, hv.payload) orelse {
|
||||
try stdout.interface.writeAll("Error: failed to decode string payload\n");
|
||||
try stdout.flush();
|
||||
return error.DecodeFailed;
|
||||
};
|
||||
defer arena.allocator.free(s);
|
||||
try stdout.interface.writeAll(s);
|
||||
try stdout.interface.writeAll("\n");
|
||||
},
|
||||
codecs.HOST_NUMBER_TAG => {
|
||||
const n = try codecs.toNumber(arena, hv.payload) orelse 0;
|
||||
try stdout.interface.print("{d}\n", .{n});
|
||||
},
|
||||
codecs.HOST_BOOL_TAG => {
|
||||
const b = try codecs.toBool(arena, hv.payload) orelse {
|
||||
try stdout.interface.writeAll("Error: failed to decode bool payload\n");
|
||||
try stdout.flush();
|
||||
return error.DecodeFailed;
|
||||
};
|
||||
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
|
||||
},
|
||||
codecs.HOST_TREE_TAG => {
|
||||
try tree.formatTree(&stdout.interface, arena, hv.payload, 0);
|
||||
try stdout.interface.writeAll("\n");
|
||||
},
|
||||
else => {
|
||||
try stdout.interface.print("(tag={d}, payload=", .{hv.tag});
|
||||
try tree.formatTree(&stdout.interface, arena, hv.payload, 0);
|
||||
try stdout.interface.writeAll(")\n");
|
||||
},
|
||||
}
|
||||
try stdout.flush();
|
||||
try printNode(arena, hv.tag, hv.payload, io);
|
||||
}
|
||||
|
||||
fn parseArg(arena: *Arena, io: std.Io, s: []const u8) !u32 {
|
||||
@@ -162,7 +152,7 @@ pub fn main(init: std.process.Init) !void {
|
||||
const args = try init.minimal.args.toSlice(init.arena.allocator());
|
||||
if (args.len < 2) {
|
||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
|
||||
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--io] [--unsafe-io] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
|
||||
try stderr.flush();
|
||||
std.process.exit(1);
|
||||
}
|
||||
@@ -173,6 +163,8 @@ pub fn main(init: std.process.Init) !void {
|
||||
var arg_start: usize = 2;
|
||||
|
||||
var use_kernel = false;
|
||||
var use_io = false;
|
||||
var io_perms = io_driver.IOPerms{};
|
||||
var fuel: u64 = std.math.maxInt(u64);
|
||||
|
||||
var i: usize = 1;
|
||||
@@ -180,7 +172,7 @@ pub fn main(init: std.process.Init) !void {
|
||||
if (std.mem.eql(u8, args[i], "--type")) {
|
||||
if (i + 1 >= args.len) {
|
||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||
try stderr.interface.writeAll("Usage: tricu-zig --type <tree|number|bool|string|list|bytes> [--fuel N] <bundle> [args...]\n");
|
||||
try stderr.interface.writeAll("Usage: tricu-zig --type <tree|number|bool|string|list|bytes> [--io] [--unsafe-io] [--fuel N] <bundle> [args...]\n");
|
||||
try stderr.flush();
|
||||
std.process.exit(1);
|
||||
}
|
||||
@@ -201,10 +193,15 @@ pub fn main(init: std.process.Init) !void {
|
||||
i += 1;
|
||||
} else if (std.mem.eql(u8, args[i], "--kernel")) {
|
||||
use_kernel = true;
|
||||
} else if (std.mem.eql(u8, args[i], "--io")) {
|
||||
use_io = true;
|
||||
} else if (std.mem.eql(u8, args[i], "--unsafe-io")) {
|
||||
io_perms.allow_read_all = true;
|
||||
io_perms.allow_write_all = true;
|
||||
} else if (std.mem.eql(u8, args[i], "--fuel")) {
|
||||
if (i + 1 >= args.len) {
|
||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||
try stderr.interface.writeAll("Usage: tricu-zig --fuel <N> <bundle> [args...]\n");
|
||||
try stderr.interface.writeAll("Usage: tricu-zig --fuel <N> [--io] [--unsafe-io] <bundle> [args...]\n");
|
||||
try stderr.flush();
|
||||
std.process.exit(1);
|
||||
}
|
||||
@@ -225,7 +222,7 @@ pub fn main(init: std.process.Init) !void {
|
||||
|
||||
if (bundle_idx >= args.len) {
|
||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
|
||||
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--io] [--unsafe-io] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
|
||||
try stderr.flush();
|
||||
std.process.exit(1);
|
||||
}
|
||||
@@ -239,7 +236,14 @@ pub fn main(init: std.process.Init) !void {
|
||||
|
||||
const call_args = if (arg_start < args.len) args[arg_start..] else &[_][]const u8{};
|
||||
|
||||
if (use_kernel) {
|
||||
if (use_io) {
|
||||
runIO(&arena, tag, bundle_bytes, call_args, fuel, io_perms, io) catch |err| {
|
||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||
try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
|
||||
try stderr.flush();
|
||||
std.process.exit(1);
|
||||
};
|
||||
} else if (use_kernel) {
|
||||
runBundle(&arena, tag, bundle_bytes, call_args, fuel, io) catch |err| {
|
||||
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
|
||||
try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
|
||||
|
||||
@@ -51,6 +51,68 @@ int main(void) {
|
||||
}
|
||||
printf("PASS: kernel loaded (root=%u)\n", kernel_root);
|
||||
|
||||
/* Test: tree inspection primitives */
|
||||
uint32_t l = arb_leaf(ctx);
|
||||
uint32_t s = arb_stem(ctx, l);
|
||||
uint32_t f = arb_fork(ctx, s, l);
|
||||
uint32_t a = arb_app(ctx, f, s);
|
||||
|
||||
if (!arb_is_leaf(ctx, l)) {
|
||||
fprintf(stderr, "FAIL: is_leaf on leaf\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
if (arb_is_leaf(ctx, s)) {
|
||||
fprintf(stderr, "FAIL: is_leaf on stem should be false\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
if (!arb_is_stem(ctx, s)) {
|
||||
fprintf(stderr, "FAIL: is_stem on stem\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
if (!arb_is_fork(ctx, f)) {
|
||||
fprintf(stderr, "FAIL: is_fork on fork\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
if (!arb_is_app(ctx, a)) {
|
||||
fprintf(stderr, "FAIL: is_app on app\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t child;
|
||||
if (!arb_get_stem_child(ctx, s, &child) || child != l) {
|
||||
fprintf(stderr, "FAIL: get_stem_child\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t left, right;
|
||||
if (!arb_get_fork_children(ctx, f, &left, &right) || left != s || right != l) {
|
||||
fprintf(stderr, "FAIL: get_fork_children\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t func, arg;
|
||||
if (!arb_get_app_func_arg(ctx, a, &func, &arg) || func != f || arg != s) {
|
||||
fprintf(stderr, "FAIL: get_app_func_arg\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Invalid index should return 0 */
|
||||
if (arb_is_leaf(ctx, 999999)) {
|
||||
fprintf(stderr, "FAIL: is_leaf on invalid index should be false\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("PASS: tree inspection primitives\n");
|
||||
|
||||
arboricx_free(ctx);
|
||||
printf("\nAll C ABI tests passed.\n");
|
||||
return 0;
|
||||
|
||||
223
ext/zig/tests/io_protocol_test.c
Normal file
223
ext/zig/tests/io_protocol_test.c
Normal file
@@ -0,0 +1,223 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "arboricx.h"
|
||||
|
||||
int main(void) {
|
||||
arb_ctx_t* ctx = arboricx_init();
|
||||
if (!ctx) {
|
||||
fprintf(stderr, "Failed to initialize Arboricx context\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Test: construct and verify pure action = Fork 0 Leaf */
|
||||
uint32_t leaf = arb_leaf(ctx);
|
||||
uint32_t zero = arb_of_number(ctx, 0);
|
||||
uint32_t pure_action = arb_fork(ctx, zero, leaf);
|
||||
|
||||
if (!arb_is_fork(ctx, pure_action)) {
|
||||
fprintf(stderr, "FAIL: pure action should be fork\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t tag, payload;
|
||||
if (!arb_get_fork_children(ctx, pure_action, &tag, &payload) ||
|
||||
tag != zero || payload != leaf) {
|
||||
fprintf(stderr, "FAIL: pure action children mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint64_t tag_num;
|
||||
if (!arb_to_number(ctx, tag, &tag_num) || tag_num != 0) {
|
||||
fprintf(stderr, "FAIL: pure action tag should be 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: pure action shape\n");
|
||||
|
||||
/* Test: construct and verify bind action = Fork 1 (Fork left k) */
|
||||
uint32_t one = arb_of_number(ctx, 1);
|
||||
uint32_t left = arb_fork(ctx, zero, leaf); /* pure Leaf */
|
||||
uint32_t k = arb_fork(ctx, leaf, leaf); /* identity as Fork Leaf Leaf */
|
||||
uint32_t bind_pair = arb_fork(ctx, left, k);
|
||||
uint32_t bind_action = arb_fork(ctx, one, bind_pair);
|
||||
|
||||
if (!arb_get_fork_children(ctx, bind_action, &tag, &payload) ||
|
||||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 1) {
|
||||
fprintf(stderr, "FAIL: bind action tag should be 1\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t bind_left, bind_k;
|
||||
if (!arb_get_fork_children(ctx, payload, &bind_left, &bind_k) ||
|
||||
bind_left != left || bind_k != k) {
|
||||
fprintf(stderr, "FAIL: bind payload should be Fork left k\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: bind action shape\n");
|
||||
|
||||
/* Test: construct and verify IO sentinel = Fork "tricuIO" (Fork 1 action) */
|
||||
uint32_t sentinel_str = arb_of_string(ctx, "tricuIO");
|
||||
uint32_t version = arb_of_number(ctx, 1);
|
||||
uint32_t version_action_pair = arb_fork(ctx, version, pure_action);
|
||||
uint32_t io_sentinel = arb_fork(ctx, sentinel_str, version_action_pair);
|
||||
|
||||
if (!arb_is_fork(ctx, io_sentinel)) {
|
||||
fprintf(stderr, "FAIL: IO sentinel should be fork\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t sent_left, sent_right;
|
||||
if (!arb_get_fork_children(ctx, io_sentinel, &sent_left, &sent_right)) {
|
||||
fprintf(stderr, "FAIL: get_fork_children on IO sentinel\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Verify sentinel string */
|
||||
uint8_t* decoded_sentinel;
|
||||
size_t decoded_len;
|
||||
if (!arb_to_string(ctx, sent_left, &decoded_sentinel, &decoded_len) ||
|
||||
decoded_len != 7 || memcmp(decoded_sentinel, "tricuIO", 7) != 0) {
|
||||
fprintf(stderr, "FAIL: IO sentinel string mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
arboricx_free_buf(ctx, decoded_sentinel, decoded_len);
|
||||
|
||||
/* Verify version = 1 and action = pure */
|
||||
uint32_t ver, act;
|
||||
if (!arb_get_fork_children(ctx, sent_right, &ver, &act) ||
|
||||
!arb_to_number(ctx, ver, &tag_num) || tag_num != 1 ||
|
||||
act != pure_action) {
|
||||
fprintf(stderr, "FAIL: IO sentinel version/action mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: IO sentinel shape\n");
|
||||
|
||||
/* Test: putStr action = Fork 10 string */
|
||||
uint32_t ten = arb_of_number(ctx, 10);
|
||||
uint32_t msg = arb_of_string(ctx, "hello");
|
||||
uint32_t putStr_action = arb_fork(ctx, ten, msg);
|
||||
|
||||
if (!arb_get_fork_children(ctx, putStr_action, &tag, &payload) ||
|
||||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 10) {
|
||||
fprintf(stderr, "FAIL: putStr tag should be 10\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: putStr action shape\n");
|
||||
|
||||
/* Test: getLine action = Fork 11 Leaf */
|
||||
uint32_t eleven = arb_of_number(ctx, 11);
|
||||
uint32_t getLine_action = arb_fork(ctx, eleven, leaf);
|
||||
|
||||
if (!arb_get_fork_children(ctx, getLine_action, &tag, &payload) ||
|
||||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 11 ||
|
||||
payload != leaf) {
|
||||
fprintf(stderr, "FAIL: getLine tag should be 11 with Leaf payload\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: getLine action shape\n");
|
||||
|
||||
/* Test: readFile action = Fork 20 path */
|
||||
uint32_t twenty = arb_of_number(ctx, 20);
|
||||
uint32_t path = arb_of_string(ctx, "/tmp/test.txt");
|
||||
uint32_t readFile_action = arb_fork(ctx, twenty, path);
|
||||
|
||||
if (!arb_get_fork_children(ctx, readFile_action, &tag, &payload) ||
|
||||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 20) {
|
||||
fprintf(stderr, "FAIL: readFile tag should be 20\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: readFile action shape\n");
|
||||
|
||||
/* Test: writeFile action = Fork 21 (Fork path contents) */
|
||||
uint32_t twenty_one = arb_of_number(ctx, 21);
|
||||
uint32_t contents = arb_of_string(ctx, "data");
|
||||
uint32_t write_pair = arb_fork(ctx, path, contents);
|
||||
uint32_t writeFile_action = arb_fork(ctx, twenty_one, write_pair);
|
||||
|
||||
if (!arb_get_fork_children(ctx, writeFile_action, &tag, &payload) ||
|
||||
!arb_to_number(ctx, tag, &tag_num) || tag_num != 21) {
|
||||
fprintf(stderr, "FAIL: writeFile tag should be 21\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t wf_path, wf_contents;
|
||||
if (!arb_get_fork_children(ctx, payload, &wf_path, &wf_contents) ||
|
||||
wf_path != path || wf_contents != contents) {
|
||||
fprintf(stderr, "FAIL: writeFile payload should be Fork path contents\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: writeFile action shape\n");
|
||||
|
||||
/* Test: ok result = Fork (Stem Leaf) (Fork val Leaf) */
|
||||
uint32_t stem_leaf = arb_stem(ctx, leaf);
|
||||
uint32_t val_pair = arb_fork(ctx, msg, leaf);
|
||||
uint32_t ok_result = arb_fork(ctx, stem_leaf, val_pair);
|
||||
|
||||
if (!arb_is_fork(ctx, ok_result)) {
|
||||
fprintf(stderr, "FAIL: ok result should be fork\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t ok_tag, ok_rest;
|
||||
if (!arb_get_fork_children(ctx, ok_result, &ok_tag, &ok_rest) ||
|
||||
!arb_is_stem(ctx, ok_tag)) {
|
||||
fprintf(stderr, "FAIL: ok result left should be stem\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t ok_val, ok_leaf;
|
||||
if (!arb_get_fork_children(ctx, ok_rest, &ok_val, &ok_leaf) ||
|
||||
ok_val != msg || ok_leaf != leaf) {
|
||||
fprintf(stderr, "FAIL: ok result right should be Fork val Leaf\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: ok result shape\n");
|
||||
|
||||
/* Test: err result = Fork Leaf (Fork code Leaf) */
|
||||
uint32_t err_code = arb_of_number(ctx, 42);
|
||||
uint32_t err_pair = arb_fork(ctx, err_code, leaf);
|
||||
uint32_t err_result = arb_fork(ctx, leaf, err_pair);
|
||||
|
||||
if (!arb_is_fork(ctx, err_result)) {
|
||||
fprintf(stderr, "FAIL: err result should be fork\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t err_tag, err_rest;
|
||||
if (!arb_get_fork_children(ctx, err_result, &err_tag, &err_rest) ||
|
||||
!arb_is_leaf(ctx, err_tag)) {
|
||||
fprintf(stderr, "FAIL: err result left should be leaf\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t err_c, err_l;
|
||||
if (!arb_get_fork_children(ctx, err_rest, &err_c, &err_l) ||
|
||||
err_c != err_code || err_l != leaf) {
|
||||
fprintf(stderr, "FAIL: err result right should be Fork code Leaf\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: err result shape\n");
|
||||
|
||||
arboricx_free(ctx);
|
||||
printf("\nAll IO protocol tests passed.\n");
|
||||
return 0;
|
||||
}
|
||||
217
ext/zig/tests/io_run_test.c
Normal file
217
ext/zig/tests/io_run_test.c
Normal file
@@ -0,0 +1,217 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "arboricx.h"
|
||||
|
||||
static uint32_t make_pure(arb_ctx_t* ctx, uint32_t val) {
|
||||
uint32_t zero = arb_of_number(ctx, 0);
|
||||
return arb_fork(ctx, zero, val);
|
||||
}
|
||||
|
||||
static uint32_t make_io_sentinel(arb_ctx_t* ctx, uint32_t action) {
|
||||
uint32_t sentinel = arb_of_string(ctx, "tricuIO");
|
||||
uint32_t version = arb_of_number(ctx, 1);
|
||||
uint32_t version_action = arb_fork(ctx, version, action);
|
||||
return arb_fork(ctx, sentinel, version_action);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
arb_ctx_t* ctx = arboricx_init();
|
||||
if (!ctx) {
|
||||
fprintf(stderr, "Failed to initialize Arboricx context\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
arb_io_perms_t perms = { 0, 0 };
|
||||
|
||||
/* Test 1: pure "hello" wrapped in IO sentinel */
|
||||
{
|
||||
uint32_t hello = arb_of_string(ctx, "hello");
|
||||
uint32_t pure_hello = make_pure(ctx, hello);
|
||||
uint32_t program = make_io_sentinel(ctx, pure_hello);
|
||||
|
||||
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||
if (result == 0) {
|
||||
fprintf(stderr, "FAIL: pure hello returned 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
if (!arb_to_string(ctx, result, &decoded, &decoded_len) ||
|
||||
decoded_len != 5 || memcmp(decoded, "hello", 5) != 0) {
|
||||
fprintf(stderr, "FAIL: pure hello result mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
arboricx_free_buf(ctx, decoded, decoded_len);
|
||||
printf("PASS: pure hello\n");
|
||||
}
|
||||
|
||||
/* Test 2: bind (pure "a") (\_ : pure "done") */
|
||||
{
|
||||
uint32_t a = arb_of_string(ctx, "a");
|
||||
uint32_t done = arb_of_string(ctx, "done");
|
||||
uint32_t pure_a = make_pure(ctx, a);
|
||||
uint32_t pure_done = make_pure(ctx, done);
|
||||
|
||||
/* K pure_done = Fork Leaf pure_done */
|
||||
uint32_t k = arb_fork(ctx, arb_leaf(ctx), pure_done);
|
||||
uint32_t bind_pair = arb_fork(ctx, pure_a, k);
|
||||
uint32_t one = arb_of_number(ctx, 1);
|
||||
uint32_t bind_action = arb_fork(ctx, one, bind_pair);
|
||||
uint32_t program = make_io_sentinel(ctx, bind_action);
|
||||
|
||||
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||
if (result == 0) {
|
||||
fprintf(stderr, "FAIL: bind returned 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
if (!arb_to_string(ctx, result, &decoded, &decoded_len) ||
|
||||
decoded_len != 4 || memcmp(decoded, "done", 4) != 0) {
|
||||
fprintf(stderr, "FAIL: bind result mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
arboricx_free_buf(ctx, decoded, decoded_len);
|
||||
printf("PASS: bind pure\n");
|
||||
}
|
||||
|
||||
/* Test 3: putStr "test" (no permissions needed) */
|
||||
{
|
||||
uint32_t test = arb_of_string(ctx, "test");
|
||||
uint32_t ten = arb_of_number(ctx, 10);
|
||||
uint32_t putStr_action = arb_fork(ctx, ten, test);
|
||||
uint32_t program = make_io_sentinel(ctx, putStr_action);
|
||||
|
||||
printf("EXPECT: test\n");
|
||||
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||
if (result == 0) {
|
||||
fprintf(stderr, "FAIL: putStr returned 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
if (!arb_is_leaf(ctx, result)) {
|
||||
fprintf(stderr, "FAIL: putStr should return Leaf\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: putStr\n");
|
||||
}
|
||||
|
||||
/* Test 4: readFile without permission returns err */
|
||||
{
|
||||
uint32_t path = arb_of_string(ctx, "/etc/passwd");
|
||||
uint32_t twenty = arb_of_number(ctx, 20);
|
||||
uint32_t readFile_action = arb_fork(ctx, twenty, path);
|
||||
uint32_t program = make_io_sentinel(ctx, readFile_action);
|
||||
|
||||
uint32_t result = arb_run_io(ctx, program, &perms);
|
||||
if (result == 0) {
|
||||
fprintf(stderr, "FAIL: readFile denied returned 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Should be an err result: Fork Leaf (Fork code Leaf) */
|
||||
uint32_t left, right;
|
||||
if (!arb_get_fork_children(ctx, result, &left, &right) ||
|
||||
!arb_is_leaf(ctx, left)) {
|
||||
fprintf(stderr, "FAIL: readFile denied should be err result\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t code, rest;
|
||||
if (!arb_get_fork_children(ctx, right, &code, &rest) ||
|
||||
!arb_is_leaf(ctx, rest)) {
|
||||
fprintf(stderr, "FAIL: readFile denied err shape mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint64_t code_num;
|
||||
if (!arb_to_number(ctx, code, &code_num) || code_num != 20) {
|
||||
fprintf(stderr, "FAIL: readFile denied code should be 20, got %llu\n",
|
||||
(unsigned long long)code_num);
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: readFile denied\n");
|
||||
}
|
||||
|
||||
/* Test 5: readFile with permission succeeds */
|
||||
{
|
||||
/* Create a temp file first */
|
||||
const char* tmp = "/tmp/tricu_io_test.txt";
|
||||
FILE* f = fopen(tmp, "w");
|
||||
if (!f) {
|
||||
fprintf(stderr, "FAIL: could not create temp file\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
fprintf(f, "hi");
|
||||
fclose(f);
|
||||
|
||||
arb_io_perms_t unsafe_perms = { 1, 0 };
|
||||
uint32_t path = arb_of_string(ctx, tmp);
|
||||
uint32_t twenty = arb_of_number(ctx, 20);
|
||||
uint32_t readFile_action = arb_fork(ctx, twenty, path);
|
||||
uint32_t program = make_io_sentinel(ctx, readFile_action);
|
||||
|
||||
uint32_t result = arb_run_io(ctx, program, &unsafe_perms);
|
||||
if (result == 0) {
|
||||
fprintf(stderr, "FAIL: readFile allowed returned 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Should be ok result: Fork (Stem Leaf) (Fork val Leaf) */
|
||||
uint32_t ok_tag, ok_rest;
|
||||
if (!arb_get_fork_children(ctx, result, &ok_tag, &ok_rest) ||
|
||||
!arb_is_stem(ctx, ok_tag)) {
|
||||
fprintf(stderr, "FAIL: readFile allowed should be ok result\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t val, leaf;
|
||||
if (!arb_get_fork_children(ctx, ok_rest, &val, &leaf) ||
|
||||
!arb_is_leaf(ctx, leaf)) {
|
||||
fprintf(stderr, "FAIL: readFile allowed ok shape mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
if (!arb_to_string(ctx, val, &decoded, &decoded_len) ||
|
||||
decoded_len != 2 || memcmp(decoded, "hi", 2) != 0) {
|
||||
fprintf(stderr, "FAIL: readFile allowed contents mismatch\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
arboricx_free_buf(ctx, decoded, decoded_len);
|
||||
printf("PASS: readFile allowed\n");
|
||||
}
|
||||
|
||||
/* Test 6: invalid sentinel returns 0 */
|
||||
{
|
||||
uint32_t bad = arb_fork(ctx, arb_leaf(ctx), arb_leaf(ctx));
|
||||
uint32_t result = arb_run_io(ctx, bad, &perms);
|
||||
if (result != 0) {
|
||||
fprintf(stderr, "FAIL: invalid sentinel should return 0\n");
|
||||
arboricx_free(ctx);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: invalid sentinel\n");
|
||||
}
|
||||
|
||||
arboricx_free(ctx);
|
||||
printf("\nAll IO run tests passed.\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user