fix: call frame handling, logical short circuiting

This commit is contained in:
Brett Broadhurst 2026-04-01 21:13:36 -06:00
parent 5c133e5fa2
commit 236acc7d60
Failed to generate hash of commit
8 changed files with 301 additions and 159 deletions

View file

@ -100,10 +100,13 @@ pub const Opcode = enum(u8) {
};
pub const CallFrame = struct {
/// Pointer to the knot that initiated the call.
callee: *Object.Knot,
caller_top: usize,
/// Instruction pointer.
ip: usize,
sp: usize,
bp: usize,
/// Output stream base.
output_base: usize,
};
pub const Value = union(enum) {
@ -377,11 +380,11 @@ fn getConstant(story: *Story, frame: *CallFrame, offset: u8) !Value {
}
fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?Value {
return vm.stack[frame.sp + offset];
return vm.stack[frame.bp + offset + 1];
}
fn setLocal(vm: *Story, frame: *CallFrame, offset: u8, value: Value) void {
vm.stack[frame.sp + offset] = value;
vm.stack[frame.bp + offset + 1] = value;
}
// TODO: This should probably check the constants table first.
@ -426,68 +429,58 @@ pub fn getKnot(vm: *Story, name: []const u8) ?*Object.Knot {
return knot;
}
fn call(vm: *Story, knot: *Object.Knot) !void {
if (vm.call_stack_top >= vm.call_stack.len)
return error.CallStackOverflow;
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;
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 = sp,
.caller_top = caller_top,
};
vm.call_stack_top += 1;
fn callValue(vm: *Story, value: Value, args_count: u8) !void {
switch (value) {
.object => |object| switch (object.tag) {
.knot => return call(vm, @ptrCast(object), args_count),
else => return error.InvalidCallTarget,
},
else => return error.InvalidCallTarget,
}
}
// 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;
fn call(vm: *Story, knot: *Object.Knot, args_count: u8) !void {
assert(knot.code.args_count == args_count);
if (vm.call_stack_top >= vm.call_stack.len)
return error.CallStackOverflow;
if (!vm.can_advance)
vm.can_advance = true;
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],
);
}
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.* = .{
vm.call_stack[vm.call_stack_top] = .{
.callee = knot,
.ip = 0,
.sp = sp,
.caller_top = caller_top,
.bp = vm.stack_top - args_count - 1,
.output_base = vm.output_buffer.items.len,
};
vm.call_stack_top += 1;
vm.stack_top += knot.code.locals_count;
}
fn divertValue(vm: *Story, value: Value, args_count: u8) !void {
switch (value) {
.object => |object| switch (object.tag) {
.knot => return divert(vm, @ptrCast(object), args_count),
else => return error.InvalidCallTarget,
},
else => return error.InvalidCallTarget,
}
}
// Diverts are essentially tail calls.
fn divert(vm: *Story, knot: *Object.Knot, args_count: u8) !void {
assert(knot.code.args_count == args_count);
if (vm.call_stack_top == 0)
return vm.call(knot, args_count);
const frame = &vm.call_stack[vm.call_stack_top - 1];
frame.* = .{
.callee = knot,
.ip = 0,
.bp = vm.stack_top - args_count - 1,
.output_base = vm.output_buffer.items.len,
};
vm.stack_top += knot.code.locals_count;
}
fn readAddress(code: []const Story.Opcode, offset: usize) u16 {
@ -518,16 +511,32 @@ fn step(vm: *Story) !StepSignal {
.exit => return .exit,
.done => return .done,
.ret => {
const return_value = vm.stack[vm.stack_top - 1];
if (vm.call_stack_top == 0) return error.UnexpectedReturn;
vm.call_stack_top -= 1;
const completed_frame = vm.call_stack[vm.call_stack_top];
vm.stack_top = completed_frame.caller_top;
const resolved_stream: ?Value = if (vm.output_buffer.items.len > frame.output_base) blk: {
const frame_output = vm.output_buffer.items[frame.output_base..];
defer vm.output_buffer.shrinkRetainingCapacity(frame.output_base);
const str_bytes = try resolveOutputStream(vm, arena, frame_output);
const str_object = try Object.String.create(vm, .{ .bytes = str_bytes });
break :blk .{ .object = &str_object.base };
} else blk: {
break :blk null;
};
const return_value = if (resolved_stream) |stream| blk: {
if (frame.bp + frame.callee.code.stack_size + 1 < vm.stack_top) {
try vm.output_buffer.append(gpa, .{ .value = stream });
break :blk popStack(vm).?;
}
break :blk stream;
} else if (frame.bp + frame.callee.code.stack_size + 1 < vm.stack_top) blk: {
break :blk popStack(vm).?;
} else .nil;
vm.stack_top = frame.bp;
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];
},
.pop => {
@ -629,17 +638,11 @@ fn step(vm: *Story) !StepSignal {
}
},
.call => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
const args_count: 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,
}
if (peekStack(vm, args_count)) |value| {
try callValue(vm, value, args_count);
} else {
return error.InvalidArgument;
}
@ -647,17 +650,11 @@ fn step(vm: *Story) !StepSignal {
frame = &vm.call_stack[vm.call_stack_top - 1];
},
.divert => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
const args_count: 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,
}
if (peekStack(vm, args_count)) |value| {
try divertValue(vm, value, args_count);
} else {
return error.InvalidArgument;
}
@ -831,6 +828,7 @@ pub fn advance(story: *Story) !?[]const u8 {
}
}
story.can_advance = false;
if (output_buffer.items.len == 0) return null;
return try resolveOutputStream(story, arena, output_buffer.items[0..]);
}
@ -906,7 +904,8 @@ pub fn fromSourceBytes(
try comp.setupStoryRuntime(gpa, &story);
if (story.getKnot(Story.default_knot_name)) |knot| {
try story.divert(knot);
try story.pushStack(.{ .object = &knot.base });
try story.divert(knot, 0);
}
return story;
}