hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/bridge
diff options
context:
space:
mode:
Diffstat (limited to 'bridge')
-rw-r--r--bridge/bridge.zig243
1 files changed, 188 insertions, 55 deletions
diff --git a/bridge/bridge.zig b/bridge/bridge.zig
index 136cfa9..0c934eb 100644
--- a/bridge/bridge.zig
+++ b/bridge/bridge.zig
@@ -11,32 +11,35 @@ 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.
+// fan a multi-slice write payload out to the sink.
+// mirrors the shape of std.Io's file_write_streaming request so
+// an optional header, then the data whose LAST slice is repeated
+// splat times.
+// idk why zig chose these names, they are funny af.
+// NOTE: 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);
+ if (header.len > 0) emit(header);
+ for (data[0..data.len -| 1]) |slice| {
+ if (slice.len > 0) emit(slice);
}
const last = data[data.len -| 1];
var i: usize = 0;
while (i < splat) : (i += 1) {
- if (last.len > 0) sink(last);
+ if (last.len > 0) emit(last);
}
var total: usize = header.len;
- for (data[0 .. data.len -| 1]) |slice| total += slice.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.
+// every file write (stdout, stderr, whatever) routes to the serial port but 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| .{
@@ -48,7 +51,9 @@ fn ioOperate(_: ?*anyopaque, operation: std.Io.Operation) std.Io.Cancelable!std.
};
}
-// no timer wired up yet, so time stands still. fine until we have the PIT/TSC.
+// no timer wired up yet, so time stands still,
+// call me the time bender the way i fuck shit up
+// TODO: fine until we have the PIT/TSC
fn ioNow(_: ?*anyopaque, _: std.Io.Clock) std.Io.Timestamp {
return .{ .nanoseconds = 0 };
}
@@ -57,13 +62,13 @@ fn ioClockResolution(_: ?*anyopaque, _: std.Io.Clock) std.Io.Clock.ResolutionErr
return .{ .nanoseconds = 1_000_000 };
}
-// the scheduler parks fibers itself, it never needs us to actually block.
+// 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.
+// eval captures its own errors into a buffer, so this is never reached lol
+// 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;
}
@@ -87,8 +92,8 @@ 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
+// doesn't exist on freestandin, 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;
@@ -96,8 +101,8 @@ 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.
+ // freestanding so we 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 };
@@ -125,7 +130,7 @@ const debug_io_vtable: std.Io.VTable = blk: {
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.
+// revo's own error reports (which write to a std.Io.Writer) onto the console
const SinkWriter = struct {
interface: std.Io.Writer,
@@ -136,14 +141,14 @@ const SinkWriter = struct {
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);
+ for (data[0..data.len -| 1]) |s| {
+ if (s.len > 0) emit(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);
+ if (last.len > 0) emit(last);
}
total += last.len * splat;
return total;
@@ -152,23 +157,147 @@ const SinkWriter = struct {
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 {
+const HostResult = revo.std_lib.HostResult;
+const Data = revo.Data;
+
+// revo's only number type is f64 which is really fucking annoying
+fn f64ToInt(comptime T: type, n: f64) ?T {
+ if (!std.math.isFinite(n)) return null;
+ // go through @floatFromInt for the bounds: maxInt(u64) isn't representable
+ // as an f64 literal, so comparing against the comptime_int directly won't
+ // even compile bruh
+ const min_f: f64 = @floatFromInt(std.math.minInt(T));
+ const max_f: f64 = @floatFromInt(std.math.maxInt(T));
+ if (n < min_f or n > max_f) return null;
+ return @intFromFloat(n);
+}
+
+// pass shit to revo
+fn hostOutb(args: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ const port = f64ToInt(u16, args[0].asNum().?) orelse return HostResult.other("outb: port out of range");
+ const val = f64ToInt(u8, args[1].asNum().?) orelse return HostResult.other("outb: value out of range");
+ asm volatile ("outb %[v], %[p]"
+ :
+ : [v] "{al}" (val),
+ [p] "N{dx}" (port),
+ );
+ return HostResult.data(Data.new.nil());
+}
+
+// pass shit to revo
+fn hostInb(args: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ const port = f64ToInt(u16, args[0].asNum().?) orelse return HostResult.other("inb: port out of range");
+ const val = asm volatile ("inb %[p], %[r]"
+ : [r] "={al}" (-> u8),
+ : [p] "N{dx}" (port),
+ );
+ return HostResult.data(Data.new.num(val));
+}
+
+// pass shit to revo
+fn hostPuts(args: []const revo.Data, vm: *revo.VM) anyerror!HostResult {
+ const s = args[0].asString() orelse return HostResult.other("puts: not a string");
+ emit(vm.stringValue(s));
+ return HostResult.data(Data.new.nil());
+}
+
+// the actual framebuffer, the data layout feels pretty self explanatory
+pub const Fb = struct {
+ ptr: [*]u8,
+ width: usize,
+ height: usize,
+ pitch: usize,
+};
+var g_fb: ?Fb = null;
+
+fn hostFbWidth(_: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(if (g_fb) |fb| fb.width else 0));
+}
+
+fn hostFbHeight(_: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(if (g_fb) |fb| fb.height else 0));
+}
+
+// color is 0xRRGGBB in the usual Limine 32-bpp layout
+fn hostFillRect(args: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ const fb = g_fb orelse return HostResult.data(Data.new.nil()); // no screen, no-op
+ const x = f64ToInt(usize, args[0].asNum().?) orelse return HostResult.other("fill_rect: bad x");
+ const y = f64ToInt(usize, args[1].asNum().?) orelse return HostResult.other("fill_rect: bad y");
+ const w = f64ToInt(usize, args[2].asNum().?) orelse return HostResult.other("fill_rect: bad w");
+ const h = f64ToInt(usize, args[3].asNum().?) orelse return HostResult.other("fill_rect: bad h");
+ const color = f64ToInt(u32, args[4].asNum().?) orelse return HostResult.other("fill_rect: bad color");
+
+ var yy = y;
+ const y_end = @min(y + h, fb.height);
+ const x_end = @min(x + w, fb.width);
+ while (yy < y_end) : (yy += 1) {
+ const row = fb.ptr + yy * fb.pitch;
+ var xx = x;
+ while (xx < x_end) : (xx += 1) {
+ const px: *align(1) u32 = @ptrCast(row + xx * 4);
+ px.* = color;
+ }
+ }
+ return HostResult.data(Data.new.nil());
+}
+
+// mirror everything to serial
+fn emit(bytes: []const u8) void {
+ sink(bytes);
+}
+
+// shift the whole framebuffer up dy pixels and clear the freed rows
+fn hostFbScroll(args: []const revo.Data, _: *revo.VM) anyerror!HostResult {
+ const fb = g_fb orelse return HostResult.data(Data.new.nil());
+ const dy = f64ToInt(usize, args[0].asNum().?) orelse return HostResult.other("fb_scroll: bad dy");
+ const color = f64ToInt(u32, args[1].asNum().?) orelse return HostResult.other("fb_scroll: bad color");
+ if (dy == 0 or dy >= fb.height) return HostResult.data(Data.new.nil());
+ const move_bytes = dy * fb.pitch;
+ const total = fb.height * fb.pitch;
+ std.mem.copyForwards(u8, fb.ptr[0 .. total - move_bytes], fb.ptr[move_bytes..total]);
+ var y: usize = fb.height - dy;
+ while (y < fb.height) : (y += 1) {
+ const line = fb.ptr + y * fb.pitch;
+ var x: usize = 0;
+ while (x < fb.width) : (x += 1) {
+ const px: *align(1) u32 = @ptrCast(line + x * 4);
+ px.* = color;
+ }
+ }
+ return HostResult.data(Data.new.nil());
+}
+
+// install the primitives as globals the revo program can call by name
+// idk how else we should be doing this, lung said he'll do stdlib stuff
+fn registerPrimitives(vm: *revo.VM) !void {
+ const define = revo.std_lib.define;
+ const T = revo.std_lib.TypeSpec;
+ try vm.registerGlobal("outb", try vm.installHost("outb", define(&[_]T{ .number, .number }, hostOutb)));
+ try vm.registerGlobal("inb", try vm.installHost("inb", define(&[_]T{.number}, hostInb)));
+ try vm.registerGlobal("serial_puts", try vm.installHost("serial_puts", define(&[_]T{.string}, hostPuts)));
+ try vm.registerGlobal("fb_width", try vm.installHost("fb_width", define(&[_]T{}, hostFbWidth)));
+ try vm.registerGlobal("fb_height", try vm.installHost("fb_height", define(&[_]T{}, hostFbHeight)));
+ try vm.registerGlobal("fill_rect", try vm.installHost("fill_rect", define(&[_]T{ .number, .number, .number, .number, .number }, hostFillRect)));
+ try vm.registerGlobal("fb_scroll", try vm.installHost("fb_scroll", define(&[_]T{ .number, .number }, hostFbScroll)));
+}
+
+// the VM outlives boot() now, so we juts make this hoe static
+var g_vm: ?*revo.VM = null;
+
+const init_program = @embedFile("kernel_main");
+
+pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb) void {
sink = out;
+ g_fb = fb;
// 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.
+ // Runtime.eval wrapper, which has a latent type error in revo that only
+ // trips when analyzed. same low-level path revo's own wasm entry uses.
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,
@@ -178,38 +307,29 @@ pub fn run(alloc: std.mem.Allocator, out: Sink) void {
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 {
+ registerPrimitives(vm) catch {
+ out("bridge: registering primitives failed\r\n");
+ return;
+ };
+
+ const build_result = revo.lang.build(vm, .{ .name = "kernel", .text = init_program }, .{}) catch {
out("bridge: compile threw\r\n");
return;
};
const artifact = switch (build_result) {
.ok => |art| art,
- .err => {
- out("bridge: compile error\r\n");
+ .err => |failure| {
+ out("bridge: compile error: ");
+ var sw = SinkWriter.init();
+ revo.lang.renderError(alloc, &sw.interface, .{ .name = "kernel", .text = init_program }, failure) catch {};
+ out("\r\n");
return;
},
};
- defer alloc.free(artifact.instructions);
- defer alloc.free(artifact.spans);
+ // NOTE: on purpose we don't free artifact.instructions/spans cause on_key's
+ // bytecode lives in there and we call it for the life of the kernel
vm.setProgramDebugInfo(artifact.spans, "", "kernel") catch {
out("bridge: setProgramDebugInfo failed\r\n");
@@ -221,12 +341,25 @@ pub fn run(alloc: std.mem.Allocator, out: Sink) void {
return;
};
switch (eval_result) {
- .ok => out("\r\nbridge: eval ok\r\n"),
+ .ok => g_vm = vm, // handler is defined so hand the VM to the event loop
.err => |failure| {
out("\r\nbridge: runtime error: ");
var sw = SinkWriter.init();
- failure.render(alloc, &sw.interface, src) catch {};
+ failure.render(alloc, &sw.interface, init_program) catch {};
out("\r\n");
},
}
}
+
+// called from the boot event loop for each raw scancode
+// hand it to revo's on_key
+// runs in normal context (not the ISR), so allocation is fine here
+// i think
+pub fn onKey(scancode: u8) void {
+ const vm = g_vm orelse return;
+ const cb = vm.getGlobal("on_key") orelse return;
+ if (cb.asFunction() == null) return;
+ _ = vm.callFunctionParts(cb, null, &[_]revo.Data{revo.Data.new.num(scancode)}, null) catch {
+ sink("bridge: on_key threw\r\n");
+ };
+}