feat: code generation for diverts, wip

This commit is contained in:
Brett Broadhurst 2026-03-17 23:19:54 -06:00
parent ee26be6254
commit 20292bcc6a
Failed to generate hash of commit
5 changed files with 699 additions and 149 deletions

View file

@ -17,11 +17,14 @@ choice_index: usize = 0,
current_choices: std.ArrayListUnmanaged(Choice) = .empty,
constants_pool: std.ArrayListUnmanaged(*Object) = .empty,
globals: std.StringHashMapUnmanaged(?*Object) = .empty,
paths: std.ArrayListUnmanaged(*Object) = .empty,
stack: std.ArrayListUnmanaged(?*Object) = .empty,
call_stack: std.ArrayListUnmanaged(CallFrame) = .empty,
stack_max: usize = 128,
gc_objects: std.SinglyLinkedList = .{},
// FIXME: This was a hack to keep string bytes alive.
string_bytes: []const u8 = &.{},
pub const default_knot_name: [:0]const u8 = "$__main__$";
pub const CallFrame = struct {
ip: usize,
@ -104,9 +107,9 @@ pub fn deinit(story: *Story) void {
story.current_choices.deinit(gpa);
story.constants_pool.deinit(gpa);
story.globals.deinit(gpa);
story.paths.deinit(gpa);
story.stack.deinit(gpa);
story.call_stack.deinit(gpa);
gpa.free(story.string_bytes);
}
pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
@ -129,8 +132,14 @@ pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
try writer.writeAll("\n");
try writer.writeAll("=== Knots ===\n");
for (story.paths.items) |path_object| {
try story_dumper.dump(@ptrCast(path_object));
var knots_iter = story.globals.iterator();
while (knots_iter.next()) |entry| {
if (entry.value_ptr.*) |global| {
switch (global.tag) {
.content_path => try story_dumper.dump(@ptrCast(global)),
else => {},
}
}
}
}
@ -141,16 +150,18 @@ pub fn trace(story: *Story, writer: *std.Io.Writer, frame: *CallFrame) !void {
const stack = &story.stack;
const stack_top = story.stack.items.len;
if (stack_top > 0) {
const last_slot = stack.items[stack.items.len - 1];
for (stack.items[frame.sp .. stack.items.len - 1]) |slot| {
if (slot) |object| {
try story_dumper.dumpObject(object);
} else {
try writer.writeAll("null");
// FIXME: There has to be a better way to do this.
if (stack_top > 1) {
for (stack.items[frame.sp .. stack.items.len - 1]) |slot| {
if (slot) |object| {
try story_dumper.dumpObject(object);
} else {
try writer.writeAll("null");
}
try writer.writeAll(", ");
}
try writer.writeAll(", ");
}
if (last_slot) |object| {
if (stack.items[stack.items.len - 1]) |object| {
try story_dumper.dumpObject(object);
} else {
try writer.writeAll("null");
@ -198,7 +209,7 @@ fn getConstant(story: *Story, frame: *CallFrame, offset: u8) !*Object {
}
fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?*Object {
const stack_top = vm.stack.capacity;
const stack_top = vm.stack.items.len;
const stack_offset = frame.sp + offset;
assert(stack_top > stack_offset);
@ -206,19 +217,21 @@ fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?*Object {
}
fn setLocal(vm: *Story, frame: *CallFrame, offset: u8, value: *Object) void {
const stack_top = vm.stack.capacity;
const stack_top = vm.stack.items.len;
const stack_offset = frame.sp + offset;
assert(stack_top > stack_offset);
vm.stack.items[stack_offset] = value;
}
// TODO: This should probably check the constants table first.
fn getGlobal(vm: *Story, key: *const Object.String) !*Object {
const key_bytes = key.bytes[0..key.length];
const val = vm.globals.get(key_bytes) orelse return error.InvalidVariable;
return val orelse unreachable;
}
// TODO: This should probably check the constants table first.
fn setGlobal(vm: *Story, key: *const Object.String, value: *Object) !void {
const key_bytes = key.bytes[0..key.length];
return vm.globals.putAssumeCapacity(key_bytes, value);
@ -231,12 +244,12 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
}
if (vm.isCallStackEmpty()) return .empty;
const frame = vm.currentFrame();
const code = std.mem.bytesAsSlice(Opcode, frame.callee.bytes);
var stream_writer = std.Io.Writer.Allocating.init(gpa);
defer stream_writer.deinit();
while (true) {
const frame = vm.currentFrame();
const code = std.mem.bytesAsSlice(Opcode, frame.callee.bytes);
if (vm.dump_writer) |w| {
vm.trace(w, frame) catch {};
}
@ -447,6 +460,16 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
frame.ip = branch_dispatch.dest_offset;
},
.divert => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
frame.ip += 2;
if (peekStack(vm, arg_offset)) |knot| {
try divertToKnot(vm, @ptrCast(knot));
} else {
return error.InvalidArgument;
}
},
else => return error.InvalidInstruction,
}
}
@ -462,27 +485,37 @@ pub fn advance(story: *Story, gpa: std.mem.Allocator) ![]const u8 {
return content.toOwnedSlice(gpa);
}
fn divert(vm: *Story, path_name: []const u8) !void {
const gpa = vm.allocator;
const path_object: ?*Object.ContentPath = blk: {
for (vm.paths.items) |object| {
const current_path: *Object.ContentPath = @ptrCast(object);
const current_name = current_path.name;
// TODO(Brett): We probably should create a method for doing this.
const name_bytes = current_name.bytes[0..current_name.length];
if (std.mem.eql(u8, name_bytes, path_name)) break :blk current_path;
pub fn getKnot(vm: *Story, name: []const u8) ?*Object.ContentPath {
const knot: ?*Object.ContentPath = blk: {
if (vm.globals.get(name)) |object| {
break :blk @ptrCast(object);
}
break :blk null;
};
if (path_object) |path| {
// TODO(Brett): Add arguments?
const stack_needed = path.arity + path.locals_count;
const stack_ptr = vm.stack.items.len;
try vm.stack.ensureUnusedCapacity(gpa, stack_needed);
try vm.call_stack.ensureUnusedCapacity(gpa, 1);
return knot;
}
vm.stack.appendNTimesAssumeCapacity(null, stack_needed);
vm.call_stack.appendAssumeCapacity(.{ .ip = 0, .sp = stack_ptr, .callee = path });
// TODO(Brett): Add arguments?
fn divertToKnot(vm: *Story, knot: *Object.ContentPath) !void {
const gpa = vm.allocator;
const stack_ptr = vm.stack.items.len - knot.arity;
const stack_needed = knot.locals_count;
try vm.stack.ensureUnusedCapacity(gpa, stack_needed);
try vm.call_stack.ensureUnusedCapacity(gpa, 1);
vm.call_stack.appendAssumeCapacity(.{
.callee = knot,
.ip = 0,
.sp = stack_ptr,
});
vm.stack.appendNTimesAssumeCapacity(null, stack_needed);
vm.can_advance = true;
}
fn divert(vm: *Story, knot_name: []const u8) !void {
return if (getKnot(vm, knot_name)) |knot| {
return divertToKnot(vm, knot);
} else return error.InvalidPath;
}
@ -535,7 +568,6 @@ pub fn loadFromString(
try options.stderr_writer.flush();
return error.CompilationFailed;
}
if (options.dump_ir) {
if (options.dump_writer) |w| {
try w.writeAll("=== Semantic IR ===\n");
@ -552,8 +584,13 @@ pub fn loadFromString(
.can_advance = false,
.dump_writer = options.dump_writer,
};
try compiled.buildRuntime(gpa, sem_ir, &story);
try story.divert("$__main__$");
story.can_advance = true;
errdefer story.deinit();
try compiled.buildRuntime(gpa, &sem_ir, &story);
if (story.getKnot(Story.default_knot_name)) |knot| {
try story.divertToKnot(knot);
story.can_advance = true;
}
return story;
}