ink/src/Story.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);
}