feat: function calls

This commit is contained in:
Brett Broadhurst 2026-03-29 15:52:34 -06:00
parent 11d99fba38
commit d08e753664
Failed to generate hash of commit
99 changed files with 881 additions and 148 deletions

View file

@ -14,13 +14,16 @@ dump_writer: ?*std.Io.Writer = null,
is_exited: bool = false,
can_advance: bool = false,
choice_index: usize = 0,
stack_top: usize = 0,
call_stack_top: usize = 0,
current_choices: std.ArrayListUnmanaged(Choice) = .empty,
code_chunks: std.ArrayListUnmanaged(*Object.Code) = .empty,
constants_pool: std.ArrayListUnmanaged(Value) = .empty,
globals: std.StringHashMapUnmanaged(?Value) = .empty,
stack: std.ArrayListUnmanaged(?Value) = .empty,
call_stack: std.ArrayListUnmanaged(CallFrame) = .empty,
stack_max: usize = 128,
stack: []Value = &.{},
call_stack: []CallFrame = &.{},
/// Global constants pool.
constants_pool: []const Value = &.{},
/// Linked list of all tracked runtime objects.
gc_objects: std.SinglyLinkedList = .{},
// FIXME: This was a hack to keep string bytes alive.
string_bytes: []const u8 = &.{},
@ -28,6 +31,7 @@ string_bytes: []const u8 = &.{},
pub const default_knot_name: [:0]const u8 = "$__main__$";
pub const Value = union(enum) {
nil,
bool: bool,
int: i64,
float: f64,
@ -35,6 +39,7 @@ pub const Value = union(enum) {
pub fn tagBytes(v: Value) []const u8 {
return switch (v) {
.nil => "Nil",
.bool => "Bool",
.int => "Int",
.float => "Float",
@ -57,7 +62,7 @@ pub const Value = union(enum) {
pub fn isTruthy(v: Value) bool {
return switch (v) {
//.nil => false,
.nil => false,
.bool => |b| b,
.int => |i| i != 0,
.float => |f| f != 0.0,
@ -133,6 +138,7 @@ pub const Value = union(enum) {
pub fn eql(lhs: Value, rhs: Value) bool {
return switch (lhs) {
.nil => rhs == .nil,
.bool => |l| rhs == .bool and l == rhs.bool,
.int => |l| switch (rhs) {
.int => |r| l == r,
@ -183,15 +189,15 @@ pub const Value = union(enum) {
pub fn negate(lhs: Value) !Value {
switch (lhs) {
.bool => return error.TypeError,
.nil, .bool, .object => return error.TypeError,
.int => |int| return .{ .int = -int },
.float => |float| return .{ .float = -float },
.object => return error.TypeError,
}
}
pub fn format(value: Value, writer: *std.Io.Writer) error{WriteFailed}!void {
switch (value) {
.nil => try writer.writeAll(value.tagBytes()),
.bool => |boolean| try writer.writeAll(if (boolean) "true" else "false"),
.int => |int| try writer.print("{d}", .{int}),
.float => |float| {
@ -222,9 +228,10 @@ pub const Value = union(enum) {
};
pub const CallFrame = struct {
callee: *Object.Knot,
caller_top: usize,
ip: usize,
sp: usize,
callee: *Object.Knot,
};
pub const Choice = struct {
@ -304,62 +311,52 @@ pub fn deinit(story: *Story) void {
}
story.current_choices.deinit(gpa);
story.constants_pool.deinit(gpa);
story.globals.deinit(gpa);
story.stack.deinit(gpa);
story.call_stack.deinit(gpa);
gpa.free(story.string_bytes);
}
fn isCallStackEmpty(vm: *const Story) bool {
return vm.call_stack.items.len == 0;
gpa.free(story.string_bytes);
gpa.free(story.constants_pool);
gpa.free(story.stack);
gpa.free(story.call_stack);
}
fn currentFrame(vm: *Story) *CallFrame {
return &vm.call_stack.items[vm.call_stack.items.len - 1];
assert(vm.call_stack_top > 0);
return &vm.call_stack[vm.call_stack_top - 1];
}
fn peekStack(vm: *Story, offset: usize) ?Value {
const stack_top = vm.stack.items.len;
if (stack_top <= offset) return null;
return vm.stack.items[stack_top - offset - 1];
if (vm.stack_top <= offset) return null;
return vm.stack[vm.stack_top - offset - 1];
}
fn pushStack(vm: *Story, value: Value) !void {
const gpa = vm.allocator;
const stack_top = vm.stack.items.len;
const max_stack_top = vm.stack_max;
if (stack_top >= max_stack_top) return error.StackOverflow;
return vm.stack.append(gpa, value);
if (vm.stack_top >= vm.stack.len) return error.StackOverflow;
vm.stack[vm.stack_top] = value;
vm.stack_top += 1;
}
fn popStack(vm: *Story) ?Value {
return vm.stack.pop() orelse unreachable;
if (vm.stack_top == 0) return null;
const stack_top = vm.stack_top;
vm.stack_top -= 1;
return vm.stack[stack_top];
}
fn getConstant(story: *Story, frame: *CallFrame, offset: u8) !Value {
if (offset >= frame.callee.code.constants.len) return error.InvalidArgument;
const constant_index = frame.callee.code.constants[offset];
return story.constants_pool.items[constant_index];
return story.constants_pool[constant_index];
}
fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?Value {
const stack_top = vm.stack.items.len;
const stack_offset = frame.sp + offset;
assert(stack_top > stack_offset);
return vm.stack.items[stack_offset];
assert(vm.stack_top > frame.sp + offset);
return vm.stack[frame.sp + offset];
}
fn setLocal(vm: *Story, frame: *CallFrame, offset: u8, value: Value) void {
const stack_top = vm.stack.items.len;
const stack_offset = frame.sp + offset;
assert(stack_top > stack_offset);
vm.stack.items[stack_offset] = value;
assert(vm.stack_top > frame.sp + offset);
vm.stack[frame.sp + offset] = value;
}
// TODO: This should probably check the constants table first.
@ -396,15 +393,14 @@ fn setGlobal(vm: *Story, key: Value, value: Value) !void {
fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
const gpa = vm.allocator;
if (vm.call_stack_top == 0) return .empty;
errdefer vm.can_advance = false;
if (vm.isCallStackEmpty()) return .empty;
var stream_writer = std.Io.Writer.Allocating.init(gpa);
defer stream_writer.deinit();
var frame = vm.currentFrame();
while (true) {
const frame = vm.currentFrame();
const code = std.mem.bytesAsSlice(Opcode, frame.callee.code.bytecode);
if (vm.dump_writer) |w| {
Dumper.trace(vm, w, frame) catch {};
@ -419,14 +415,27 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
vm.can_advance = false;
return .empty;
},
.ret => {
const return_value = vm.stack[vm.stack_top - 1];
vm.call_stack_top -= 1;
const completed_frame = vm.call_stack[vm.call_stack_top];
vm.stack_top = completed_frame.caller_top;
vm.stack[vm.stack_top] = return_value;
vm.stack_top += 1;
if (vm.call_stack_top == 0) return error.UnexpectedReturn;
frame = &vm.call_stack[vm.call_stack_top - 1];
},
.true => {
const value: Value = .{ .bool = true };
try vm.pushStack(value);
try vm.pushStack(.{ .bool = true });
frame.ip += 1;
},
.false => {
const value: Value = .{ .bool = false };
try vm.pushStack(value);
try vm.pushStack(.{ .bool = false });
frame.ip += 1;
},
.pop => {
@ -519,6 +528,43 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
return error.InvalidArgument;
}
},
.call => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
frame.ip += 2;
if (peekStack(vm, arg_offset)) |value| {
switch (value) {
.object => |object| switch (object.tag) {
.knot => try call(vm, @ptrCast(object)),
else => unreachable,
},
else => unreachable,
}
} else {
return error.InvalidArgument;
}
// Re-fetch we're now in the callee's frame
frame = &vm.call_stack[vm.call_stack_top - 1];
},
.divert => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
frame.ip += 2;
if (peekStack(vm, arg_offset)) |value| {
switch (value) {
.object => |object| switch (object.tag) {
.knot => try divert(vm, @ptrCast(object)),
else => unreachable,
},
else => unreachable,
}
} else {
return error.InvalidArgument;
}
frame = &vm.call_stack[vm.call_stack_top - 1];
},
.load_const => {
const index: u8 = @intFromEnum(code[frame.ip + 1]);
const value = try vm.getConstant(frame, index);
@ -611,16 +657,6 @@ 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)) |value| {
try divertToValue(vm, value);
} else {
return error.InvalidArgument;
}
},
.load_attr => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
frame.ip += 2;
@ -667,38 +703,62 @@ pub fn getKnot(vm: *Story, name: []const u8) ?*Object.Knot {
return knot;
}
// TODO(Brett): Add arguments?
fn divertToKnot(vm: *Story, knot: *Object.Knot) !void {
const gpa = vm.allocator;
const stack_ptr = vm.stack.items.len - knot.code.args_count;
const stack_needed = knot.code.stack_size;
fn call(vm: *Story, knot: *Object.Knot) !void {
if (vm.call_stack_top >= vm.call_stack.len)
return error.CallStackOverflow;
try vm.stack.ensureUnusedCapacity(gpa, stack_needed);
try vm.call_stack.ensureUnusedCapacity(gpa, 1);
const locals_count = knot.code.locals_count;
const args_count = knot.code.args_count;
const sp = vm.stack_top - args_count;
const caller_top = if (vm.call_stack_top == 0)
sp
else
sp - 1;
vm.call_stack.appendAssumeCapacity(.{
const frame_top = sp + args_count + locals_count;
if (frame_top > vm.stack.len) return error.StackOverflow;
for (vm.stack[sp + args_count .. frame_top]) |*slot| slot.* = .nil;
vm.stack_top = frame_top;
vm.call_stack[vm.call_stack_top] = .{
.callee = knot,
.ip = 0,
.sp = stack_ptr,
});
vm.stack.appendNTimesAssumeCapacity(null, stack_needed);
vm.can_advance = true;
.sp = sp,
.caller_top = caller_top,
};
vm.call_stack_top += 1;
}
fn divertToValue(vm: *Story, value: Value) !void {
switch (value) {
.object => |object| switch (object.tag) {
.knot => try divertToKnot(vm, @ptrCast(object)),
else => return error.TypeError,
},
else => return error.TypeError,
// Diverts are essentially tail calls.
fn divert(vm: *Story, knot: *Object.Knot) !void {
const args_count = knot.code.args_count;
const locals_count = knot.code.locals_count;
if (vm.call_stack_top == 0) return vm.call(knot);
const args_start = vm.stack_top - args_count;
const current_frame = &vm.call_stack[vm.call_stack_top - 1];
const sp = current_frame.sp;
const caller_top = current_frame.caller_top;
if (args_count > 0) {
std.mem.copyForwards(
Value,
vm.stack[sp .. sp + args_count],
vm.stack[args_start .. args_start + args_count],
);
}
}
fn divert(vm: *Story, knot_name: []const u8) !void {
return if (getKnot(vm, knot_name)) |knot| {
return divertToKnot(vm, knot);
} else return error.InvalidPath;
const frame_top = sp + args_count + locals_count;
if (frame_top > vm.stack.len) return error.StackOverflow;
for (vm.stack[sp + args_count .. frame_top]) |*slot| slot.* = .nil;
vm.stack_top = frame_top;
current_frame.* = .{
.callee = knot,
.ip = 0,
.sp = sp,
.caller_top = caller_top,
};
}
pub const LoadOptions = struct {
@ -746,15 +806,25 @@ pub fn loadFromString(
return error.LoadFailed;
}
const stack_size = 128;
const eval_stack_ptr = try gpa.alloc(Value, stack_size);
errdefer gpa.free(eval_stack_ptr);
const call_stack_ptr = try gpa.alloc(CallFrame, stack_size);
errdefer gpa.free(call_stack_ptr);
var story: Story = .{
.allocator = gpa,
.can_advance = false,
.dump_writer = if (options.dump_trace) options.dump_writer else null,
.stack = eval_stack_ptr,
.call_stack = call_stack_ptr,
};
errdefer story.deinit();
try comp.setupStoryRuntime(gpa, &story);
if (story.getKnot(Story.default_knot_name)) |knot| {
try story.divertToKnot(knot);
try story.divert(knot);
story.can_advance = true;
}
return story;