feat: code generation and execution for simple choice statements

This commit is contained in:
Brett Broadhurst 2026-03-02 21:16:24 -07:00
parent 55346fcd85
commit 889f678dd8
Failed to generate hash of commit
4 changed files with 389 additions and 179 deletions

View file

@ -12,12 +12,14 @@ allocator: std.mem.Allocator,
dump_writer: ?*std.Io.Writer = null,
is_exited: bool = false,
can_advance: bool = false,
gc_objects: std.SinglyLinkedList = .{},
choice_index: usize = 0,
current_choices: std.ArrayListUnmanaged(Choice) = .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 = .{},
pub const CallFrame = struct {
ip: usize,
@ -25,15 +27,30 @@ pub const CallFrame = struct {
callee: *Object.ContentPath,
};
pub const Choice = struct {
text: std.ArrayListUnmanaged(u8),
dest_offset: u16,
};
pub const Opcode = enum(u8) {
/// Exit the VM normally.
exit,
ret,
/// Pop a value off the stack, discarding it.
pop,
true,
false,
/// Pop two values off the stack and calculate their sum.
/// The result will be pushed to the stack.
add,
/// Pop two values off the stack and calculate their difference.
/// The result will be pushed to the stack.
sub,
/// Pop two values off the stack and calculate their product.
/// The result will be pushed to the stack.
mul,
/// Pop two values off the stack and calculate their quotient.
/// The result will be pushed to the stack.
div,
mod,
neg,
@ -53,12 +70,16 @@ pub const Opcode = enum(u8) {
store,
load_global,
store_global,
load_choice_id,
content,
line,
glue,
choice,
flush,
/// Pop a value off the stack and write it to the content stream.
stream_push,
stream_line,
stream_glue,
/// Flush the content stream to the story consumer.
stream_flush,
br_push,
br_table,
br_dispatch,
br_select_index,
_,
};
@ -71,6 +92,7 @@ pub fn deinit(story: *Story) void {
object.destroy(story);
}
story.current_choices.deinit(gpa);
story.globals.deinit(gpa);
story.paths.deinit(gpa);
story.stack.deinit(gpa);
@ -174,14 +196,17 @@ fn setGlobal(vm: *Story, key: *const Object.String, value: *Object) !void {
return vm.globals.putAssumeCapacity(key_bytes, value);
}
fn execute(vm: *Story, writer: *std.Io.Writer) !void {
fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
const gpa = vm.allocator;
defer {
vm.can_advance = false;
}
if (vm.isCallStackEmpty()) return;
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) {
if (vm.dump_writer) |w| {
@ -190,7 +215,7 @@ fn execute(vm: *Story, writer: *std.Io.Writer) !void {
switch (code[frame.ip]) {
.exit => {
vm.is_exited = true;
return;
return .empty;
},
.pop => {
const object_top = vm.popStack();
@ -224,16 +249,6 @@ fn execute(vm: *Story, writer: *std.Io.Writer) !void {
}
frame.ip += 1;
},
.content => {
const arg_object = vm.popStack();
if (arg_object) |object| {
const string_object = try Object.String.fromObject(vm, object);
const string_bytes = string_object.bytes[0..string_object.length];
try writer.writeAll(string_bytes);
}
frame.ip += 1;
},
.load_const => {
const index: u8 = @intFromEnum(code[frame.ip + 1]);
const value = try vm.getConstant(frame, index);
@ -276,17 +291,53 @@ fn execute(vm: *Story, writer: *std.Io.Writer) !void {
}
frame.ip += 2;
},
.stream_push => {
const arg_object = vm.popStack();
if (arg_object) |object| {
const string_object = try Object.String.fromObject(vm, object);
const string_bytes = string_object.bytes[0..string_object.length];
try stream_writer.writer.writeAll(string_bytes);
}
frame.ip += 1;
},
.stream_flush => {
frame.ip += 1;
return stream_writer.toArrayList();
},
.br_push => {
const arg_offset = std.mem.bytesToValue(u16, code[frame.ip + 1 ..][0..2]);
try vm.current_choices.append(gpa, .{
.text = stream_writer.toArrayList(),
.dest_offset = std.mem.bigToNative(u16, arg_offset),
});
frame.ip += 3;
},
.br_table => {
// No-op currently.
frame.ip += 1;
},
.br_select_index => {
vm.can_advance = false;
frame.ip += 1;
return .empty;
},
.br_dispatch => {
const index = vm.choice_index;
const branch_dispatch = vm.current_choices.items[index];
defer vm.current_choices.clearRetainingCapacity();
frame.ip = branch_dispatch.dest_offset;
},
else => return error.InvalidInstruction,
}
}
}
pub fn advance(story: *Story) ![]const u8 {
const gpa = story.allocator;
var aw = std.Io.Writer.Allocating.init(gpa);
try story.execute(&aw.writer);
return aw.toOwnedSlice();
pub fn advance(story: *Story, gpa: std.mem.Allocator) ![]const u8 {
var content = try story.execute();
return content.toOwnedSlice(gpa);
}
fn divert(vm: *Story, path_name: []const u8) !void {
@ -319,6 +370,12 @@ pub const LoadOptions = struct {
use_color: bool = true,
};
pub fn selectChoiceIndex(story: *Story, index: usize) !void {
if (index >= story.current_choices.items.len) return error.InvalidChoice;
story.choice_index = index;
story.can_advance = true;
}
pub fn loadFromString(
gpa: std.mem.Allocator,
source_bytes: [:0]const u8,