feat: infrastructure to support qualified lookups

This commit is contained in:
Brett Broadhurst 2026-03-25 03:18:24 -06:00
parent e8ad1cd5b5
commit 440ec68481
Failed to generate hash of commit
6 changed files with 1118 additions and 575 deletions

View file

@ -1,10 +1,9 @@
//! Virtual machine state for story execution.
const std = @import("std");
const Compilation = @import("compile.zig").Compilation;
const tokenizer = @import("tokenizer.zig");
const Ast = @import("Ast.zig");
const AstGen = @import("AstGen.zig");
const Sema = @import("Sema.zig");
const Module = @import("compile.zig").Module;
pub const Object = @import("Story/Object.zig");
const Dumper = @import("Story/Dumper.zig");
const assert = std.debug.assert;
@ -16,6 +15,7 @@ is_exited: bool = false,
can_advance: bool = false,
choice_index: usize = 0,
current_choices: std.ArrayListUnmanaged(Choice) = .empty,
code_chunks: std.ArrayListUnmanaged(*Object) = .empty,
constants_pool: std.ArrayListUnmanaged(*Object) = .empty,
globals: std.StringHashMapUnmanaged(?*Object) = .empty,
stack: std.ArrayListUnmanaged(?*Object) = .empty,
@ -30,7 +30,7 @@ pub const default_knot_name: [:0]const u8 = "$__main__$";
pub const CallFrame = struct {
ip: usize,
sp: usize,
callee: *Object.ContentPath,
callee: *Object.Knot,
};
pub const Choice = struct {
@ -83,6 +83,8 @@ pub const Opcode = enum(u8) {
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,
@ -114,11 +116,11 @@ pub fn deinit(story: *Story) void {
}
pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
const story_dumper: Dumper = .{ .story = story, .writer = writer };
var story_dumper: Dumper = .{ .story = story };
try writer.writeAll("=== Constants ===\n");
for (story.constants_pool.items) |global_constant| {
try story_dumper.dumpObject(global_constant);
try story_dumper.dumpObject(writer, global_constant);
try writer.writeAll("\n");
}
@ -138,7 +140,12 @@ pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
while (knots_iter.next()) |entry| {
if (entry.value_ptr.*) |global| {
switch (global.tag) {
.content_path => try story_dumper.dump(@ptrCast(global)),
.knot => {
try writer.writeAll("*");
story_dumper.indent_level += 2;
try story_dumper.dumpKnot(writer, @ptrCast(global));
story_dumper.indent_level -= 2;
},
else => {},
}
}
@ -147,8 +154,7 @@ pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
pub fn trace(story: *Story, writer: *std.Io.Writer, frame: *CallFrame) !void {
try writer.print("\tStack => stack_pointer={d}, objects=[", .{frame.sp});
const story_dumper: Dumper = .{ .story = story, .writer = writer };
var story_dumper: Dumper = .{ .story = story };
const stack = &story.stack;
const stack_top = story.stack.items.len;
if (stack_top > 0) {
@ -156,7 +162,7 @@ pub fn trace(story: *Story, writer: *std.Io.Writer, frame: *CallFrame) !void {
if (stack_top > 1) {
for (stack.items[frame.sp .. stack.items.len - 1]) |slot| {
if (slot) |object| {
try story_dumper.dumpObject(object);
try story_dumper.dumpObject(writer, object);
} else {
try writer.writeAll("null");
}
@ -164,14 +170,14 @@ pub fn trace(story: *Story, writer: *std.Io.Writer, frame: *CallFrame) !void {
}
}
if (stack.items[stack.items.len - 1]) |object| {
try story_dumper.dumpObject(object);
try story_dumper.dumpObject(writer, object);
} else {
try writer.writeAll("null");
}
}
try writer.writeAll("]\n");
_ = try story_dumper.dumpInst(frame.callee, frame.ip, true);
_ = try story_dumper.dumpInst(writer, frame.callee, frame.ip, true);
return writer.flush();
}
@ -204,9 +210,9 @@ fn popStack(vm: *Story) ?*Object {
}
fn getConstant(story: *Story, frame: *CallFrame, offset: u8) !*Object {
if (offset >= frame.callee.const_pool.len) return error.InvalidArgument;
if (offset >= frame.callee.code.constants.len) return error.InvalidArgument;
const constant_index = frame.callee.const_pool[offset];
const constant_index = frame.callee.code.constants[offset];
return story.constants_pool.items[constant_index];
}
@ -251,7 +257,7 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
while (true) {
const frame = vm.currentFrame();
const code = std.mem.bytesAsSlice(Opcode, frame.callee.bytes);
const code = std.mem.bytesAsSlice(Opcode, frame.callee.code.bytecode);
if (vm.dump_writer) |w| {
vm.trace(w, frame) catch {};
}
@ -262,6 +268,7 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
return .empty;
},
.true => {
// TODO: Intern this value.
const true_object = try Object.Number.create(vm, .{
.boolean = true,
});
@ -269,6 +276,7 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
frame.ip += 1;
},
.false => {
// TODO: Intern this value.
const false_object = try Object.Number.create(vm, .{
.boolean = false,
});
@ -472,6 +480,28 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
return error.InvalidArgument;
}
},
.load_attr => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
frame.ip += 2;
if (peekStack(vm, 0)) |obj| {
assert(obj.tag == .knot);
const knot_obj: *Object.Knot = @ptrCast(obj);
const arg_obj = try vm.getConstant(frame, arg_offset);
assert(arg_obj.tag == .string);
const knot_attr: *Object.String = @ptrCast(arg_obj);
_ = popStack(vm);
if (knot_obj.members.get(knot_attr.toSlice())) |attr_obj| {
try vm.pushStack(attr_obj);
} else {
return error.InvalidArgument;
}
} else {
return error.InvalidArgument;
}
},
else => return error.InvalidInstruction,
}
}
@ -487,8 +517,8 @@ pub fn advance(story: *Story, gpa: std.mem.Allocator) ![]const u8 {
return content.toOwnedSlice(gpa);
}
pub fn getKnot(vm: *Story, name: []const u8) ?*Object.ContentPath {
const knot: ?*Object.ContentPath = blk: {
pub fn getKnot(vm: *Story, name: []const u8) ?*Object.Knot {
const knot: ?*Object.Knot = blk: {
if (vm.globals.get(name)) |object| {
break :blk @ptrCast(object);
}
@ -498,10 +528,10 @@ pub fn getKnot(vm: *Story, name: []const u8) ?*Object.ContentPath {
}
// TODO(Brett): Add arguments?
fn divertToKnot(vm: *Story, knot: *Object.ContentPath) !void {
fn divertToKnot(vm: *Story, knot: *Object.Knot) !void {
const gpa = vm.allocator;
const stack_ptr = vm.stack.items.len - knot.arity;
const stack_needed = knot.locals_count;
const stack_ptr = vm.stack.items.len - knot.code.args_count;
const stack_needed = knot.code.stack_size;
try vm.stack.ensureUnusedCapacity(gpa, stack_needed);
try vm.call_stack.ensureUnusedCapacity(gpa, 1);
@ -527,6 +557,7 @@ pub const LoadOptions = struct {
use_color: bool = true,
dump_ast: bool = false,
dump_ir: bool = false,
dump_trace: bool = false,
};
pub fn selectChoiceIndex(story: *Story, index: usize) !void {
@ -540,7 +571,11 @@ pub fn loadFromString(
source_bytes: [:0]const u8,
options: LoadOptions,
) !Story {
var comp = try Compilation.compile(gpa, .{
var arena_allocator = std.heap.ArenaAllocator.init(gpa);
defer arena_allocator.deinit();
const arena = arena_allocator.allocator();
var comp = try Module.compile(gpa, arena, .{
.source_bytes = source_bytes,
.filename = "<STDIN>",
.dump_writer = options.dump_writer,
@ -550,17 +585,17 @@ pub fn loadFromString(
});
defer comp.deinit();
if (comp.errors.len > 0) {
for (comp.errors) |err| {
if (comp.errors.items.len > 0) {
for (comp.errors.items) |err| {
try comp.renderError(options.error_writer, err);
}
return error.Fail;
return error.LoadFailed;
}
var story: Story = .{
.allocator = gpa,
.can_advance = false,
.dump_writer = options.dump_writer,
.dump_writer = if (options.dump_trace) options.dump_writer else null,
};
errdefer story.deinit();
try comp.setupStoryRuntime(gpa, &story);