diff options
Diffstat (limited to 'bridge/bridge.zig')
| -rw-r--r-- | bridge/bridge.zig | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/bridge/bridge.zig b/bridge/bridge.zig new file mode 100644 index 0000000..136cfa9 --- /dev/null +++ b/bridge/bridge.zig @@ -0,0 +1,232 @@ +// so the thing is right, revo isn't really that low level and we need to have a +// thin bridge to translate shit over. Its just to setup the VM, serial ports, +// heap shit like that. + +const std = @import("std"); +const revo = @import("revo"); + +// all the VM's output goes here, boot injects this at runtime so the +// bridge doesn't need to pull a serial port from its ass +pub const Sink = *const fn ([]const u8) void; +fn noopSink(_: []const u8) void {} +var sink: Sink = noopSink; + +// fan a multi-slice write payload out to the sink. mirrors the shape of std.Io's +// file_write_streaming request: an optional header, then `data`, whose LAST slice +// is repeated `splat` times. returns the total byte count written. +fn writeToSink( + header: []const u8, + data: []const []const u8, + splat: usize, +) std.Io.Operation.FileWriteStreaming.Result { + if (header.len > 0) sink(header); + for (data[0 .. data.len -| 1]) |slice| { + if (slice.len > 0) sink(slice); + } + const last = data[data.len -| 1]; + var i: usize = 0; + while (i < splat) : (i += 1) { + if (last.len > 0) sink(last); + } + + var total: usize = header.len; + for (data[0 .. data.len -| 1]) |slice| total += slice.len; + total += last.len * splat; + return total; +} + +// every file write (stdout, stderr, whatever) routes to the serial port; we +// don't have a filesystem or multiple sinks yet, so there's nowhere else to go. +fn ioOperate(_: ?*anyopaque, operation: std.Io.Operation) std.Io.Cancelable!std.Io.Operation.Result { + return switch (operation) { + .file_write_streaming => |req| .{ + .file_write_streaming = writeToSink(req.header, req.data, req.splat), + }, + .file_read_streaming => .{ .file_read_streaming = error.InputOutput }, + .device_io_control => .{ .device_io_control = -1 }, + .net_receive => .{ .net_receive = .{ error.NetworkDown, 0 } }, + }; +} + +// no timer wired up yet, so time stands still. fine until we have the PIT/TSC. +fn ioNow(_: ?*anyopaque, _: std.Io.Clock) std.Io.Timestamp { + return .{ .nanoseconds = 0 }; +} + +fn ioClockResolution(_: ?*anyopaque, _: std.Io.Clock) std.Io.Clock.ResolutionError!std.Io.Duration { + return .{ .nanoseconds = 1_000_000 }; +} + +// the scheduler parks fibers itself, it never needs us to actually block. +fn ioSleep(_: ?*anyopaque, _: std.Io.Timeout) std.Io.Cancelable!void {} + +fn ioCrashHandler(_: ?*anyopaque) void {} + +// eval captures its own errors into a buffer, so this is never reached; return +// Canceled rather than recursing into a stderr lock we can't provide. +fn ioLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr { + return error.Canceled; +} + +fn ioUnlockStderr(_: ?*anyopaque) void {} + +// start from std's all-failing vtable and override only what the VM touches. +const io_vtable: std.Io.VTable = blk: { + var v = std.Io.failing.vtable.*; + v.operate = ioOperate; + v.now = ioNow; + v.clockResolution = ioClockResolution; + v.sleep = ioSleep; + v.crashHandler = ioCrashHandler; + v.lockStderr = ioLockStderr; + v.unlockStderr = ioUnlockStderr; + break :blk v; +}; + +const serial_io: std.Io = .{ .userdata = null, .vtable = &io_vtable }; + +// A second IO used only as std's debug IO (for std.debug.print, which revo hits +// on a few error paths). std's default debug IO is a threaded stderr writer that +// doesn't exist on freestanding, and std.Io.failing's stderr path is literally +// `unreachable` -- so we give std a real one that drains to the same sink. Unlike +// serial_io above, its lockStderr must return an actual File.Writer (std turns a +// Canceled lock into a panic), and swapCancelProtection must not be unreachable. +var dbg_fw: std.Io.File.Writer = undefined; +var dbg_fw_buf: [256]u8 = undefined; + +fn dbgLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr { + // File.stderr() reads posix.STDERR_FILENO, which doesn't exist on + // freestanding. Build the File by hand instead; ioOperate ignores the handle + // and routes every write to the sink regardless. + const stderr_file: std.Io.File = .{ .handle = undefined, .flags = .{ .nonblocking = false } }; + dbg_fw = stderr_file.writerStreaming(debug_io, &dbg_fw_buf); + return .{ .file_writer = &dbg_fw, .terminal_mode = .no_color }; +} +fn dbgUnlockStderr(_: ?*anyopaque) void { + dbg_fw.interface.flush() catch {}; +} +fn dbgSwapCancel(_: ?*anyopaque, new: std.Io.CancelProtection) std.Io.CancelProtection { + return new; +} + +const debug_io_vtable: std.Io.VTable = blk: { + var v = std.Io.failing.vtable.*; + v.operate = ioOperate; + v.now = ioNow; + v.clockResolution = ioClockResolution; + v.sleep = ioSleep; + v.crashHandler = ioCrashHandler; + v.lockStderr = dbgLockStderr; + v.unlockStderr = dbgUnlockStderr; + v.swapCancelProtection = dbgSwapCancel; + break :blk v; +}; + +pub const debug_io: std.Io = .{ .userdata = null, .vtable = &debug_io_vtable }; + +// an unbuffered std.Io.Writer that drains straight to the sink, so we can render +// revo's own error reports (which write to a std.Io.Writer) onto the console. +const SinkWriter = struct { + interface: std.Io.Writer, + + fn init() SinkWriter { + return .{ .interface = .{ .buffer = &.{}, .vtable = &.{ .drain = drain, .flush = flush }, .end = 0 } }; + } + + fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { + _ = w; + var total: usize = 0; + for (data[0 .. data.len -| 1]) |s| { + if (s.len > 0) sink(s); + total += s.len; + } + const last = data[data.len -| 1]; + var i: usize = 0; + while (i < splat) : (i += 1) { + if (last.len > 0) sink(last); + } + total += last.len * splat; + return total; + } + + fn flush(_: *std.Io.Writer) std.Io.Writer.Error!void {} +}; + +// bring up the VM on the kernel heap and run a revo program. this is the "hello +// world" that proves the whole stack: revo compiled for bare metal, our +// allocator feeding it, its stdout coming out our serial port. +pub fn run(alloc: std.mem.Allocator, out: Sink) void { + sink = out; + + // NOTE: we drive the VM directly (compile -> run) instead of the high-level + // Runtime.eval wrapper. that wrapper has a latent type error in revo (it + // names module.EvalResult, which doesn't exist) that only trips when it's + // actually analyzed -- we're the first caller. this is the same low-level + // path revo's own wasm entry uses, so it's the well-trodden one anyway. + const VM = revo.VM; + const vm = alloc.create(VM) catch { + out("bridge: OOM creating VM\r\n"); + return; + }; + defer alloc.destroy(vm); + vm.* = VM.init(.{ + .alloc = alloc, + .io = serial_io, + .argv = &.{}, + .diag_alloc = alloc, + }) catch { + out("bridge: VM.init failed\r\n"); + return; + }; + defer vm.deinit(); + out("bridge: revo VM up\r\n"); + + // a proper workout, not just a print: recursion (call frames + soft-float + // arithmetic), string methods (real stdlib), tables, tuples, destructuring. + const src = + \\fn fib(n) do + \\ if n < 2 n + \\ else fib(n - 1) + fib(n - 2) + \\end + \\print("hello from revo") + \\print("fib(10) =", fib(10)) + \\print("arith =", 1 + 2 * 3, 10 / 4) + \\print("string =", "revo":upper(), "KERNEL":lower()) + \\const arr = {10, 20, 30} + \\print("table =", arr, arr[0]) + \\const (a, b) = (3, 4) + \\print("tuple =", a + b) + ; + const build_result = revo.lang.build(vm, .{ .name = "kernel", .text = src }, .{}) catch { + out("bridge: compile threw\r\n"); + return; + }; + const artifact = switch (build_result) { + .ok => |art| art, + .err => { + out("bridge: compile error\r\n"); + return; + }, + }; + defer alloc.free(artifact.instructions); + defer alloc.free(artifact.spans); + + vm.setProgramDebugInfo(artifact.spans, "", "kernel") catch { + out("bridge: setProgramDebugInfo failed\r\n"); + return; + }; + + const eval_result = revo.module.runCompiledModuleReport(vm, "kernel", artifact.instructions) catch { + out("bridge: eval threw\r\n"); + return; + }; + switch (eval_result) { + .ok => out("\r\nbridge: eval ok\r\n"), + .err => |failure| { + out("\r\nbridge: runtime error: "); + var sw = SinkWriter.init(); + failure.render(alloc, &sw.interface, src) catch {}; + out("\r\n"); + }, + } +} |
