1028 lines
32 KiB
Zig
1028 lines
32 KiB
Zig
//! Virtual machine state for story execution.
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
|
|
const Ast = @import("Ast.zig");
|
|
const AstGen = @import("AstGen.zig");
|
|
const Compilation = @import("compile.zig").Compilation;
|
|
pub const Loader = @import("Story/Loader.zig");
|
|
pub const Object = @import("Story/Object.zig");
|
|
pub const Dumper = @import("Story/Dumper.zig");
|
|
const ink = @import("root.zig");
|
|
const Story = @This();
|
|
|
|
gpa: std.mem.Allocator,
|
|
arena: std.heap.ArenaAllocator,
|
|
|
|
// Flags
|
|
is_exited: bool = false,
|
|
can_advance: bool = false,
|
|
|
|
stack_top: usize = 0,
|
|
call_stack_top: usize = 0,
|
|
output_marker: usize = 0,
|
|
choice_selected: ?Choice = null,
|
|
output_buffer: std.ArrayListUnmanaged(OutputCommand) = .empty,
|
|
output_scratch: std.ArrayListUnmanaged(u8) = .empty,
|
|
current_choices: std.ArrayListUnmanaged(Choice) = .empty,
|
|
variable_observers: std.StringHashMapUnmanaged(VariableObserver) = .empty,
|
|
|
|
globals: std.StringHashMapUnmanaged(Value) = .empty,
|
|
stack: []Value = &.{},
|
|
call_stack: []CallFrame = &.{},
|
|
code_chunks: std.ArrayListUnmanaged(*Object.Code) = .empty,
|
|
/// Linked list of all tracked runtime objects.
|
|
/// We don't currently have a garbage collector, though this will necessary.
|
|
gc_objects: std.SinglyLinkedList = .{},
|
|
/// Global constants pool.
|
|
constants_pool: []const Value = &.{},
|
|
/// Interned string bytes.
|
|
string_bytes: []const u8 = &.{},
|
|
dump_writer: ?*std.Io.Writer = null,
|
|
internal_counter: usize = 0,
|
|
|
|
pub const default_knot_name: [:0]const u8 = "$__main__$";
|
|
|
|
pub const Opcode = enum(u8) {
|
|
/// Exit the VM normally.
|
|
exit,
|
|
done,
|
|
ret,
|
|
nil,
|
|
/// Pop a value off the stack, discarding it.
|
|
pop,
|
|
/// Push an object representing the boolean value of "true" to the stack.
|
|
true,
|
|
/// Push an object representing the boolean value of "false" to the stack.
|
|
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,
|
|
not,
|
|
cmp_eq,
|
|
cmp_neq,
|
|
cmp_lt,
|
|
cmp_gt,
|
|
cmp_lte,
|
|
cmp_gte,
|
|
/// Jump unconditionally to the target address.
|
|
jmp,
|
|
/// Jump conditionally to the target address if the boolean value at the
|
|
/// top of the stack is true.
|
|
jmp_t,
|
|
/// Jump conditionally to the target address if the boolean value at the
|
|
/// top of the stack is false.
|
|
jmp_f,
|
|
call,
|
|
divert,
|
|
load_const,
|
|
load,
|
|
store,
|
|
load_global,
|
|
store_global,
|
|
load_attr,
|
|
store_attr,
|
|
/// Pop a value off the stack and write it to the content stream.
|
|
stream_push,
|
|
stream_line,
|
|
stream_glue,
|
|
stream_mark,
|
|
br_push,
|
|
br_table,
|
|
br_dispatch,
|
|
br_select_index,
|
|
string_builder,
|
|
string_append,
|
|
string_freeze,
|
|
_,
|
|
};
|
|
|
|
pub const VariableObserver = struct {
|
|
callback: Callback,
|
|
context: Context,
|
|
|
|
pub const Callback = *const fn (Value, Context) anyerror!void;
|
|
|
|
pub const Context = struct {
|
|
ptr: *anyopaque,
|
|
};
|
|
};
|
|
|
|
pub const CallFrame = struct {
|
|
/// Pointer to the knot that initiated the call.
|
|
callee: *Object.Knot,
|
|
/// Instruction pointer.
|
|
ip: usize,
|
|
bp: usize,
|
|
};
|
|
|
|
pub const ValueType = enum(u8) {
|
|
nil,
|
|
bool,
|
|
int,
|
|
float,
|
|
object,
|
|
};
|
|
|
|
pub const Value = union(ValueType) {
|
|
nil,
|
|
bool: bool,
|
|
int: i64,
|
|
float: f64,
|
|
object: *Object,
|
|
|
|
pub fn tagBytes(v: Value) []const u8 {
|
|
return switch (v) {
|
|
.nil => "Nil",
|
|
.bool => "Bool",
|
|
.int => "Int",
|
|
.float => "Float",
|
|
.object => "Object",
|
|
};
|
|
}
|
|
|
|
pub fn castObject(v: Value, comptime T: type) *T {
|
|
return switch (v) {
|
|
.object => |object| return @ptrCast(object),
|
|
else => unreachable,
|
|
};
|
|
}
|
|
|
|
pub fn isNumeric(v: Value) bool {
|
|
return v == .int or v == .float;
|
|
}
|
|
|
|
pub fn coerce(v: Value) ?Value {
|
|
return switch (v) {
|
|
.int => v,
|
|
.float => v,
|
|
.bool => |boolean| .{ .int = if (boolean) 1 else 0 },
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
pub fn isTruthy(v: Value) bool {
|
|
return switch (v) {
|
|
.nil => false,
|
|
.bool => |b| b,
|
|
.int => |i| i != 0,
|
|
.float => |f| f != 0.0,
|
|
.object => true,
|
|
};
|
|
}
|
|
|
|
pub fn toFloat(v: Value) f64 {
|
|
return switch (v) {
|
|
.int => |i| @floatFromInt(i),
|
|
.float => |f| f,
|
|
else => unreachable,
|
|
};
|
|
}
|
|
|
|
pub fn add(lhs: Value, rhs: Value, story: *Story) !Value {
|
|
return arith(lhs, rhs, .add, story);
|
|
}
|
|
|
|
pub fn arith(lhs: Value, rhs: Value, op: Opcode, story: *Story) !Value {
|
|
if (lhs.coerce()) |l| {
|
|
if (rhs.coerce()) |r| {
|
|
return numericArith(l, r, op);
|
|
}
|
|
}
|
|
if (op == .add) {
|
|
if (lhs == .object and lhs.object.tag == .string)
|
|
return concat(lhs, rhs, story);
|
|
if (rhs == .object and rhs.object.tag == .string)
|
|
return concat(lhs, rhs, story);
|
|
}
|
|
return error.TypeError;
|
|
}
|
|
|
|
pub fn numericArith(lhs: Value, rhs: Value, op: Story.Opcode) !Value {
|
|
if (!lhs.isNumeric() or !rhs.isNumeric()) return error.TypeError;
|
|
|
|
if (lhs == .int and rhs == .int) {
|
|
switch (op) {
|
|
.add => return .{ .int = lhs.int +% rhs.int },
|
|
.sub => return .{ .int = lhs.int -% rhs.int },
|
|
.mul => return .{ .int = lhs.int *% rhs.int },
|
|
.div => {
|
|
if (rhs.int == 0)
|
|
return error.DivisionByZero;
|
|
return .{ .int = @divTrunc(lhs.int, rhs.int) };
|
|
},
|
|
.mod => if (rhs.int == 0)
|
|
return error.DivisionByZero
|
|
else
|
|
return .{ .int = @mod(lhs.int, rhs.int) },
|
|
else => unreachable,
|
|
}
|
|
}
|
|
|
|
const l = lhs.toFloat();
|
|
const r = rhs.toFloat();
|
|
switch (op) {
|
|
.add => return .{ .float = l + r },
|
|
.sub => return .{ .float = l - r },
|
|
.mul => return .{ .float = l * r },
|
|
.div => if (r == 0.0)
|
|
return error.DivisionByZero
|
|
else
|
|
return .{ .float = l / r },
|
|
.mod => if (r == 0.0)
|
|
return error.DivisionByZero
|
|
else
|
|
return .{ .float = @mod(l, r) },
|
|
else => unreachable,
|
|
}
|
|
}
|
|
|
|
pub fn eql(lhs: Value, rhs: Value) bool {
|
|
return switch (lhs) {
|
|
.nil => rhs == .nil,
|
|
.bool => |l| switch (rhs) {
|
|
.bool => |r| l == r,
|
|
else => false,
|
|
},
|
|
.int => |l| switch (rhs) {
|
|
.int => |r| l == r,
|
|
.float => |r| @as(f64, @floatFromInt(l)) == r,
|
|
else => false,
|
|
},
|
|
.float => |l| switch (rhs) {
|
|
.int => |r| l == @as(f64, @floatFromInt(r)),
|
|
.float => |r| l == r,
|
|
else => false,
|
|
},
|
|
.object => |lobj| switch (rhs) {
|
|
.object => |robj| Object.eql(lobj, robj),
|
|
else => false,
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn compare(lhs: Value, rhs: Value, op: Story.Opcode) !Value {
|
|
return switch (op) {
|
|
.cmp_eq => .{ .bool = lhs.eql(rhs) },
|
|
.cmp_neq => .{ .bool = !lhs.eql(rhs) },
|
|
.cmp_lt, .cmp_gt, .cmp_lte, .cmp_gte => blk: {
|
|
if (!lhs.isNumeric() or !rhs.isNumeric()) return error.TypeError;
|
|
const l = lhs.toFloat();
|
|
const r = rhs.toFloat();
|
|
break :blk .{
|
|
.bool = switch (op) {
|
|
.cmp_lt => l < r,
|
|
.cmp_gt => l > r,
|
|
.cmp_lte => l <= r,
|
|
.cmp_gte => l >= r,
|
|
else => unreachable,
|
|
},
|
|
};
|
|
},
|
|
else => unreachable,
|
|
};
|
|
}
|
|
|
|
pub fn logicalNot(lhs: Value) Value {
|
|
return .{ .bool = !lhs.isTruthy() };
|
|
}
|
|
|
|
pub fn concat(lhs: Value, rhs: Value, story: *Story) !Value {
|
|
const lhs_object = try Object.String.fromValue(story, lhs);
|
|
const rhs_object = try Object.String.fromValue(story, rhs);
|
|
const str_object = try Object.String.concat(story, lhs_object, rhs_object);
|
|
return .{ .object = &str_object.base };
|
|
}
|
|
|
|
pub fn negate(lhs: Value) !Value {
|
|
switch (lhs) {
|
|
.nil, .bool, .object => return error.TypeError,
|
|
.int => |int| return .{ .int = -int },
|
|
.float => |float| return .{ .float = -float },
|
|
}
|
|
}
|
|
|
|
pub fn format(value: Value, writer: *std.Io.Writer) error{WriteFailed}!void {
|
|
var scratch_buffer: [64]u8 = undefined;
|
|
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| {
|
|
if (std.math.isNan(float))
|
|
return writer.writeAll("NaN");
|
|
|
|
if (std.math.isInf(float))
|
|
return writer.writeAll(if (float > 0) "Inf" else "-Inf");
|
|
|
|
var str = std.fmt.bufPrint(&scratch_buffer, "{d:.7}", .{float}) catch |err| switch (err) {
|
|
error.NoSpaceLeft => unreachable,
|
|
else => |e| return e,
|
|
};
|
|
if (std.mem.indexOfScalar(u8, str, '.')) |dot| {
|
|
var end = str.len;
|
|
while (end > dot + 2 and str[end - 1] == '0') end -= 1;
|
|
str = str[0..end];
|
|
}
|
|
return writer.writeAll(str);
|
|
},
|
|
.object => |object| switch (object.tag) {
|
|
.string => {
|
|
const typed: *const Object.String = @ptrCast(object);
|
|
return writer.writeAll(typed.toSlice());
|
|
},
|
|
else => return writer.print("<{s} {*}>", .{ object.tag.tagBytes(), object }),
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const OutputCommand = union(enum) {
|
|
line,
|
|
glue,
|
|
value: Value,
|
|
};
|
|
|
|
pub const Choice = struct {
|
|
content: []const u8,
|
|
dest_offset: u16,
|
|
};
|
|
|
|
pub fn deinit(story: *Story) void {
|
|
const gpa = story.gpa;
|
|
var next = story.gc_objects.first;
|
|
while (next) |node| {
|
|
const object: *Object = @alignCast(@fieldParentPtr("node", node));
|
|
next = node.next;
|
|
object.destroy(story);
|
|
}
|
|
|
|
story.arena.deinit();
|
|
story.globals.deinit(gpa);
|
|
story.variable_observers.deinit(gpa);
|
|
story.current_choices.deinit(gpa);
|
|
story.output_scratch.deinit(gpa);
|
|
story.output_buffer.deinit(gpa);
|
|
|
|
gpa.free(story.string_bytes);
|
|
gpa.free(story.constants_pool);
|
|
gpa.free(story.stack);
|
|
gpa.free(story.call_stack);
|
|
story.* = undefined;
|
|
}
|
|
|
|
pub fn currentFrame(vm: *Story) *CallFrame {
|
|
assert(vm.call_stack_top > 0);
|
|
return &vm.call_stack[vm.call_stack_top - 1];
|
|
}
|
|
|
|
pub fn peekStack(vm: *Story, offset: usize) ?Value {
|
|
if (vm.stack_top <= offset) return null;
|
|
return vm.stack[vm.stack_top - offset - 1];
|
|
}
|
|
|
|
pub fn pushStack(vm: *Story, value: Value) !void {
|
|
if (vm.stack_top >= vm.stack.len) return error.StackOverflow;
|
|
vm.stack[vm.stack_top] = value;
|
|
vm.stack_top += 1;
|
|
}
|
|
|
|
pub fn popStack(vm: *Story) ?Value {
|
|
if (vm.stack_top == 0) return null;
|
|
|
|
const stack_top = vm.stack_top;
|
|
vm.stack_top -= 1;
|
|
return vm.stack[stack_top - 1];
|
|
}
|
|
|
|
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[constant_index];
|
|
}
|
|
|
|
fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?Value {
|
|
return vm.stack[frame.bp + offset + 1];
|
|
}
|
|
|
|
fn setLocal(vm: *Story, frame: *CallFrame, offset: u8, value: Value) void {
|
|
vm.stack[frame.bp + offset + 1] = value;
|
|
}
|
|
|
|
// TODO: This should probably check the constants table first.
|
|
fn getGlobal(vm: *Story, key: Value) !Value {
|
|
switch (key) {
|
|
.object => |object| switch (object.tag) {
|
|
.string => {
|
|
const str_object: *Object.String = @ptrCast(object);
|
|
if (vm.globals.get(str_object.toSlice())) |value| {
|
|
return value;
|
|
}
|
|
return error.InvalidVariable;
|
|
},
|
|
else => return error.TypeError,
|
|
},
|
|
else => return error.TypeError,
|
|
}
|
|
}
|
|
|
|
// TODO: This should probably check the constants table first.
|
|
fn setGlobal(vm: *Story, key: Value, value: Value) !void {
|
|
switch (key) {
|
|
.object => |object| switch (object.tag) {
|
|
.string => {
|
|
const str_object: *Object.String = @ptrCast(object);
|
|
return vm.globals.putAssumeCapacity(str_object.toSlice(), value);
|
|
},
|
|
else => return error.TypeError,
|
|
},
|
|
else => return error.TypeError,
|
|
}
|
|
}
|
|
|
|
pub fn getKnot(vm: *Story, name: []const u8) ?*Object.Knot {
|
|
const knot: ?*Object.Knot = blk: {
|
|
if (vm.globals.get(name)) |value| {
|
|
// TODO: Do a check here.
|
|
break :blk @ptrCast(value.object);
|
|
}
|
|
break :blk null;
|
|
};
|
|
return knot;
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
vm.call_stack[vm.call_stack_top] = .{
|
|
.callee = knot,
|
|
.ip = 0,
|
|
.bp = vm.stack_top - args_count - 1,
|
|
};
|
|
vm.call_stack_top += 1;
|
|
vm.stack_top += knot.code.stack_size;
|
|
}
|
|
|
|
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.
|
|
pub 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,
|
|
};
|
|
|
|
vm.stack_top += knot.code.locals_count;
|
|
}
|
|
|
|
fn readAddress(code: []const Story.Opcode, offset: usize) u16 {
|
|
const arg_offset = std.mem.bytesToValue(u16, code[offset + 1 ..][0..2]);
|
|
return std.mem.bigToNative(u16, arg_offset);
|
|
}
|
|
|
|
const StepSignal = union(enum) {
|
|
exit,
|
|
done,
|
|
choices_ready,
|
|
};
|
|
|
|
fn step(vm: *Story, variables: *VariablesState) !StepSignal {
|
|
assert(vm.call_stack_top > 0);
|
|
assert(vm.can_advance == true);
|
|
|
|
const gpa = vm.gpa;
|
|
const arena = vm.arena.allocator();
|
|
|
|
var frame = vm.currentFrame();
|
|
while (true) {
|
|
const code = std.mem.bytesAsSlice(Opcode, frame.callee.code.bytecode);
|
|
if (vm.dump_writer) |w| {
|
|
Dumper.trace(vm, w, frame) catch {};
|
|
}
|
|
switch (code[frame.ip]) {
|
|
.exit => return .exit,
|
|
.done => return .done,
|
|
.nil => {
|
|
try pushStack(vm, .nil);
|
|
frame.ip += 1;
|
|
},
|
|
.ret => {
|
|
if (vm.call_stack_top == 0) return error.UnexpectedReturn;
|
|
vm.call_stack_top -= 1;
|
|
|
|
const value = popStack(vm).?;
|
|
|
|
vm.stack_top = frame.bp;
|
|
vm.stack[vm.stack_top] = value;
|
|
vm.stack_top += 1;
|
|
frame = &vm.call_stack[vm.call_stack_top - 1];
|
|
},
|
|
.pop => {
|
|
if (vm.popStack()) |_| {} else return error.InvalidArgument;
|
|
frame.ip += 1;
|
|
},
|
|
.true => {
|
|
try vm.pushStack(.{ .bool = true });
|
|
frame.ip += 1;
|
|
},
|
|
.false => {
|
|
try vm.pushStack(.{ .bool = false });
|
|
frame.ip += 1;
|
|
},
|
|
.add => {
|
|
const lhs = vm.peekStack(1) orelse return error.Bugged;
|
|
const rhs = vm.peekStack(0) orelse return error.Bugged;
|
|
const value = try Value.add(lhs, rhs, vm);
|
|
|
|
_ = vm.popStack();
|
|
_ = vm.popStack();
|
|
try vm.pushStack(value);
|
|
frame.ip += 1;
|
|
},
|
|
.sub, .mul, .div, .mod => |op| {
|
|
const lhs = vm.peekStack(1) orelse return error.Bugged;
|
|
const rhs = vm.peekStack(0) orelse return error.Bugged;
|
|
const value = try Value.arith(lhs, rhs, op, vm);
|
|
|
|
_ = vm.popStack();
|
|
_ = vm.popStack();
|
|
try vm.pushStack(value);
|
|
frame.ip += 1;
|
|
},
|
|
.neg => {
|
|
if (vm.popStack()) |arg| {
|
|
const value = try Value.negate(arg);
|
|
try vm.pushStack(value);
|
|
} else {
|
|
return error.InvalidArgument;
|
|
}
|
|
frame.ip += 1;
|
|
},
|
|
.not => {
|
|
if (vm.popStack()) |arg| {
|
|
const value = Value.logicalNot(arg);
|
|
try vm.pushStack(value);
|
|
} else {
|
|
return error.StackOverflow;
|
|
}
|
|
frame.ip += 1;
|
|
},
|
|
.cmp_eq => {
|
|
const lhs = vm.peekStack(1) orelse return error.Bugged;
|
|
const rhs = vm.peekStack(0) orelse return error.Bugged;
|
|
const value = try Value.compare(lhs, rhs, .cmp_eq);
|
|
_ = vm.popStack();
|
|
_ = vm.popStack();
|
|
try vm.pushStack(value);
|
|
|
|
frame.ip += 1;
|
|
},
|
|
.cmp_lt, .cmp_gt, .cmp_lte, .cmp_gte => |op| {
|
|
const lhs = vm.peekStack(1) orelse return error.Bugged;
|
|
const rhs = vm.peekStack(0) orelse return error.Bugged;
|
|
const value = try Value.compare(lhs, rhs, op);
|
|
_ = vm.popStack();
|
|
_ = vm.popStack();
|
|
try vm.pushStack(value);
|
|
|
|
frame.ip += 1;
|
|
},
|
|
.jmp => {
|
|
const arg_offset = readAddress(code, frame.ip);
|
|
frame.ip += 3 + arg_offset;
|
|
},
|
|
.jmp_t => {
|
|
const arg_offset = readAddress(code, frame.ip);
|
|
frame.ip += 3;
|
|
|
|
if (vm.peekStack(0)) |condition| {
|
|
if (condition.isTruthy()) {
|
|
frame.ip += arg_offset;
|
|
}
|
|
} else {
|
|
return error.InvalidArgument;
|
|
}
|
|
},
|
|
.jmp_f => {
|
|
const arg_offset = readAddress(code, frame.ip);
|
|
frame.ip += 3;
|
|
|
|
if (vm.peekStack(0)) |condition| {
|
|
if (!condition.isTruthy()) {
|
|
frame.ip += arg_offset;
|
|
}
|
|
} else {
|
|
return error.InvalidArgument;
|
|
}
|
|
},
|
|
.call => {
|
|
const args_count: u8 = @intFromEnum(code[frame.ip + 1]);
|
|
frame.ip += 2;
|
|
|
|
if (peekStack(vm, args_count)) |value| {
|
|
try callValue(vm, value, args_count);
|
|
} else {
|
|
return error.InvalidArgument;
|
|
}
|
|
|
|
frame = &vm.call_stack[vm.call_stack_top - 1];
|
|
},
|
|
.divert => {
|
|
const args_count: u8 = @intFromEnum(code[frame.ip + 1]);
|
|
frame.ip += 2;
|
|
|
|
if (peekStack(vm, args_count)) |value| {
|
|
try divertValue(vm, value, args_count);
|
|
} 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);
|
|
try vm.pushStack(value);
|
|
frame.ip += 2;
|
|
},
|
|
.load => {
|
|
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
|
|
const value = vm.getLocal(frame, arg_offset) orelse return error.Bugged;
|
|
try vm.pushStack(value);
|
|
frame.ip += 2;
|
|
},
|
|
.store => {
|
|
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
|
|
if (peekStack(vm, 0)) |arg| {
|
|
vm.setLocal(frame, arg_offset, arg);
|
|
} else {
|
|
return error.InvalidArgument;
|
|
}
|
|
frame.ip += 2;
|
|
},
|
|
.load_attr => {
|
|
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
|
|
|
|
if (peekStack(vm, 0)) |value| {
|
|
const knot_object: *Object.Knot = @ptrCast(value.object);
|
|
const arg_value = try vm.getConstant(frame, arg_offset);
|
|
const knot_attr: *Object.String = @ptrCast(arg_value.object);
|
|
|
|
_ = popStack(vm);
|
|
|
|
if (knot_object.members.get(knot_attr.toSlice())) |attr_object| {
|
|
try vm.pushStack(.{ .object = attr_object });
|
|
} else {
|
|
return error.InvalidArgument;
|
|
}
|
|
} else {
|
|
return error.InvalidArgument;
|
|
}
|
|
frame.ip += 2;
|
|
},
|
|
// TODO: store_attr
|
|
.load_global => {
|
|
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
|
|
const global_name = try vm.getConstant(frame, arg_offset);
|
|
const global_value = try vm.getGlobal(global_name);
|
|
try vm.pushStack(global_value);
|
|
frame.ip += 2;
|
|
},
|
|
.store_global => {
|
|
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
|
|
const global_name = try getConstant(vm, frame, arg_offset);
|
|
|
|
if (peekStack(vm, 0)) |arg| {
|
|
try setGlobal(vm, global_name, arg);
|
|
_ = popStack(vm);
|
|
try pushStack(vm, arg);
|
|
try variables.update(gpa, global_name);
|
|
} else {
|
|
return error.InvalidArgument;
|
|
}
|
|
frame.ip += 2;
|
|
},
|
|
.stream_push => {
|
|
// TODO: Make this more strict.
|
|
if (popStack(vm)) |value| {
|
|
switch (value) {
|
|
.nil => {},
|
|
else => try vm.output_buffer.append(gpa, .{ .value = value }),
|
|
}
|
|
}
|
|
frame.ip += 1;
|
|
},
|
|
.stream_line => {
|
|
try vm.output_buffer.append(gpa, .line);
|
|
frame.ip += 1;
|
|
},
|
|
.stream_glue => {
|
|
try vm.output_buffer.append(gpa, .glue);
|
|
frame.ip += 1;
|
|
},
|
|
.stream_mark => {
|
|
vm.output_marker = vm.output_buffer.items.len;
|
|
frame.ip += 1;
|
|
},
|
|
.br_push => {
|
|
const marker = vm.output_marker;
|
|
const arg_offset = readAddress(code, frame.ip);
|
|
const output_stream = vm.output_buffer.items[marker..];
|
|
const choice_display = try resolveOutputStream(vm, arena, output_stream);
|
|
defer vm.output_buffer.shrinkRetainingCapacity(marker);
|
|
|
|
try vm.current_choices.append(gpa, .{
|
|
.content = choice_display,
|
|
.dest_offset = arg_offset,
|
|
});
|
|
|
|
frame.ip += 3;
|
|
},
|
|
.br_table => {
|
|
frame.ip += 1;
|
|
},
|
|
.br_select_index => {
|
|
frame.ip += 1;
|
|
return .choices_ready;
|
|
},
|
|
.br_dispatch => {
|
|
if (vm.choice_selected) |choice| {
|
|
frame.ip = choice.dest_offset;
|
|
} else {
|
|
return error.InvalidArgument;
|
|
}
|
|
},
|
|
.string_builder => {
|
|
const builder_object = try Object.StringBuilder.create(vm);
|
|
try pushStack(vm, .{ .object = &builder_object.base });
|
|
frame.ip += 1;
|
|
},
|
|
.string_append => {
|
|
if (popStack(vm)) |value| {
|
|
if (peekStack(vm, 0)) |builder| {
|
|
const string_builder = builder.castObject(Object.StringBuilder);
|
|
try string_builder.append(value);
|
|
frame.ip += 1;
|
|
continue;
|
|
}
|
|
}
|
|
return error.InvalidArgument;
|
|
},
|
|
.string_freeze => {
|
|
if (popStack(vm)) |value| {
|
|
const string_builder = value.castObject(Object.StringBuilder);
|
|
const frozen = try string_builder.freeze(vm);
|
|
try pushStack(vm, .{ .object = &frozen.base });
|
|
frame.ip += 1;
|
|
continue;
|
|
}
|
|
return error.InvalidArgument;
|
|
},
|
|
else => return error.InvalidInstruction,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn resolveOutputStream(
|
|
story: *Story,
|
|
gpa: std.mem.Allocator,
|
|
stream: []const OutputCommand,
|
|
) ![]const u8 {
|
|
var pending_newline = false;
|
|
var pending_glue = false;
|
|
var result: std.ArrayListUnmanaged(u8) = .empty;
|
|
defer result.deinit(gpa);
|
|
|
|
for (stream) |cmd| {
|
|
switch (cmd) {
|
|
.value => |value| {
|
|
if (pending_newline) {
|
|
try result.append(gpa, '\n');
|
|
pending_newline = false;
|
|
}
|
|
pending_glue = false;
|
|
switch (value) {
|
|
.nil => {},
|
|
else => {
|
|
const str = try Object.String.fromValue(story, value);
|
|
try result.appendSlice(gpa, str.toSlice());
|
|
},
|
|
}
|
|
},
|
|
.line => {
|
|
if (!pending_glue) pending_newline = true;
|
|
},
|
|
.glue => {
|
|
pending_newline = false;
|
|
pending_glue = true;
|
|
},
|
|
}
|
|
}
|
|
return result.toOwnedSlice(gpa);
|
|
}
|
|
|
|
const VariablesState = struct {
|
|
changed: std.StringHashMapUnmanaged(void) = .empty,
|
|
|
|
pub fn update(
|
|
state: *VariablesState,
|
|
gpa: std.mem.Allocator,
|
|
value: Value,
|
|
) !void {
|
|
switch (value) {
|
|
.object => |object| switch (object.tag) {
|
|
.string => {
|
|
const str_object: *Object.String = @ptrCast(object);
|
|
try state.changed.put(gpa, str_object.toSlice(), undefined);
|
|
},
|
|
else => unreachable,
|
|
},
|
|
else => unreachable,
|
|
}
|
|
}
|
|
};
|
|
|
|
pub fn advance(story: *Story) !?[]const u8 {
|
|
const arena = story.arena.allocator();
|
|
const output_buffer = &story.output_buffer;
|
|
const output_scratch = &story.output_scratch;
|
|
|
|
output_buffer.clearRetainingCapacity();
|
|
output_scratch.clearRetainingCapacity();
|
|
|
|
if (!story.can_advance) return null;
|
|
defer story.can_advance = false;
|
|
|
|
var variables_state: VariablesState = .{};
|
|
defer variables_state.changed.deinit(story.gpa);
|
|
|
|
const signal = try story.step(&variables_state);
|
|
switch (signal) {
|
|
.exit => story.is_exited = true,
|
|
.done, .choices_ready => {},
|
|
}
|
|
|
|
var state_iter = variables_state.changed.keyIterator();
|
|
while (state_iter.next()) |key| {
|
|
if (story.variable_observers.get(key.*)) |observer| {
|
|
// If there's an observer, we can assume that a global exists.
|
|
const value = story.globals.get(key.*).?;
|
|
try observer.callback(value, observer.context);
|
|
}
|
|
}
|
|
|
|
if (output_buffer.items.len == 0) return null;
|
|
return try resolveOutputStream(story, arena, output_buffer.items[0..]);
|
|
}
|
|
|
|
pub fn selectChoiceIndex(story: *Story, index: usize) !void {
|
|
const choices = &story.current_choices;
|
|
if (index >= choices.items.len)
|
|
return error.InvalidChoice;
|
|
|
|
story.choice_selected = choices.items[index];
|
|
story.can_advance = true;
|
|
story.output_marker = 0;
|
|
story.current_choices.clearRetainingCapacity();
|
|
_ = story.arena.reset(.retain_capacity);
|
|
}
|
|
|
|
pub fn observeVariable(
|
|
story: *Story,
|
|
name: []const u8,
|
|
callback: VariableObserver.Callback,
|
|
context: VariableObserver.Context,
|
|
) !void {
|
|
const gpa = story.gpa;
|
|
if (story.globals.get(name) == null) return error.VariableNotFound;
|
|
|
|
try story.variable_observers.put(gpa, name, .{
|
|
.callback = callback,
|
|
.context = context,
|
|
});
|
|
}
|
|
|
|
pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
|
|
return Dumper.dump(story, writer);
|
|
}
|
|
|
|
pub const LoadFileOptions = struct {
|
|
error_writer: *std.Io.Writer,
|
|
};
|
|
|
|
pub fn readSourceFile(
|
|
gpa: std.mem.Allocator,
|
|
filename: []const u8,
|
|
options: LoadFileOptions,
|
|
) !Story {
|
|
var read_buffer: [4096]u8 align(std.heap.page_size_min) = undefined;
|
|
// FIXME: Temporary until 0.16.x
|
|
var arena_allocator = std.heap.ArenaAllocator.init(gpa);
|
|
defer arena_allocator.deinit();
|
|
|
|
const arena = arena_allocator.allocator();
|
|
const source_bytes: [:0]const u8 = s: {
|
|
var f = try std.fs.cwd().openFile(filename, .{});
|
|
defer f.close();
|
|
|
|
var file_reader: std.fs.File.Reader = f.reader(&read_buffer);
|
|
break :s try ink.readSourceFileToEndAlloc(arena, &file_reader);
|
|
};
|
|
return Story.fromSourceBytes(gpa, source_bytes, .{
|
|
.filename = filename,
|
|
.error_writer = options.error_writer,
|
|
.dump_writer = null,
|
|
.dump_use_color = false,
|
|
.dump_ast = false,
|
|
.dump_ir = false,
|
|
});
|
|
}
|
|
|
|
pub const LoadOptions = struct {
|
|
filename: [:0]const u8,
|
|
errors: *std.ArrayListUnmanaged(Compilation.Error),
|
|
stack_size: usize = 128,
|
|
};
|
|
|
|
pub fn fromSourceBytes(
|
|
gpa: std.mem.Allocator,
|
|
source_bytes: [:0]const u8,
|
|
options: LoadOptions,
|
|
) !Story {
|
|
var arena_allocator = std.heap.ArenaAllocator.init(gpa);
|
|
defer arena_allocator.deinit();
|
|
const arena = arena_allocator.allocator();
|
|
|
|
var tree = try Ast.parse(gpa, arena, source_bytes, options.filename, 0);
|
|
defer tree.deinit(gpa);
|
|
|
|
var ir = try AstGen.generate(gpa, &tree);
|
|
defer ir.deinit(gpa);
|
|
|
|
var cu = Compilation.build(gpa, tree, ir, options.errors) catch |err| switch (err) {
|
|
else => |e| return e,
|
|
};
|
|
defer cu.deinit();
|
|
|
|
if (cu.hasCompileErrors()) {
|
|
return error.CompilationError;
|
|
}
|
|
return .fromCompilation(gpa, &cu, .{
|
|
.stack_size = options.stack_size,
|
|
});
|
|
}
|
|
|
|
pub fn fromCompilation(
|
|
gpa: std.mem.Allocator,
|
|
cu: *Compilation,
|
|
options: Loader.Options,
|
|
) !Story {
|
|
return Loader.fromCompilation(gpa, cu, options);
|
|
}
|
|
|
|
pub fn fromCachedCompilation(
|
|
gpa: std.mem.Allocator,
|
|
bytes: []const u8,
|
|
options: Loader.Options,
|
|
) !Story {
|
|
return Loader.fromCachedCompilation(gpa, bytes, options);
|
|
}
|