feat: function calls
This commit is contained in:
parent
11d99fba38
commit
d08e753664
99 changed files with 881 additions and 148 deletions
232
src/Story.zig
232
src/Story.zig
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue