hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/bridge/bridge.zig
blob: 136cfa96c3c738330c6a626ffbb89b054c680d13 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
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");
        },
    }
}