//! 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); }