feat: naive variable observers

This commit is contained in:
Brett Broadhurst 2026-04-03 15:40:49 -06:00
parent 48b0e3a806
commit faac9b54d8
Failed to generate hash of commit
3 changed files with 115 additions and 22 deletions

View file

@ -24,6 +24,8 @@ 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 = &.{},
@ -38,6 +40,17 @@ dump_writer: ?*std.Io.Writer = null,
pub const default_knot_name: [:0]const u8 = "$__main__$";
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 Opcode = enum(u8) {
/// Exit the VM normally.
exit,
@ -337,6 +350,7 @@ pub fn deinit(story: *Story) void {
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);
@ -491,7 +505,7 @@ const StepSignal = union(enum) {
choices_ready,
};
fn step(vm: *Story) !StepSignal {
fn step(vm: *Story, variables: *VariablesState) !StepSignal {
assert(vm.call_stack_top > 0);
assert(vm.can_advance == true);
@ -695,12 +709,13 @@ fn step(vm: *Story) !StepSignal {
},
.store_global => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
const global_name = try vm.getConstant(frame, arg_offset);
const global_name = try getConstant(vm, frame, arg_offset);
if (vm.peekStack(0)) |arg| {
try vm.setGlobal(global_name, arg);
_ = vm.popStack();
try vm.pushStack(arg);
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;
}
@ -799,6 +814,27 @@ fn resolveOutputStream(
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;
@ -808,17 +844,26 @@ pub fn advance(story: *Story) !?[]const u8 {
output_scratch.clearRetainingCapacity();
if (!story.can_advance) return null;
while (story.can_advance) {
const signal = try story.step();
switch (signal) {
.exit => {
story.is_exited = true;
break;
},
.done, .choices_ready => break,
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);
}
}
story.can_advance = false;
if (output_buffer.items.len == 0) return null;
return try resolveOutputStream(story, arena, output_buffer.items[0..]);
}
@ -835,6 +880,21 @@ pub fn selectChoiceIndex(story: *Story, index: usize) !void {
_ = 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);
}