feat: naive variable observers
This commit is contained in:
parent
48b0e3a806
commit
faac9b54d8
3 changed files with 115 additions and 22 deletions
|
|
@ -1134,12 +1134,13 @@ fn assign(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void {
|
||||||
const expr_node = node.data.bin.rhs.?;
|
const expr_node = node.data.bin.rhs.?;
|
||||||
const name_str = try astgen.strFromNode(identifier_node);
|
const name_str = try astgen.strFromNode(identifier_node);
|
||||||
|
|
||||||
if (scope.lookup(name_str.index)) |decl| {
|
const lhs = if (scope.lookup(name_str.index)) |decl| blk: {
|
||||||
const expr_result = try expr(gi, scope, expr_node);
|
break :blk decl.inst_index.toRef();
|
||||||
_ = try gi.addBinaryNode(.store, decl.inst_index.toRef(), expr_result);
|
} else blk: {
|
||||||
return;
|
break :blk try gi.addStrTok(.decl_ref, name_str.index, node.loc.start);
|
||||||
}
|
};
|
||||||
_ = try gi.addStrTok(.decl_ref, name_str.index, node.loc.start);
|
const rhs = try expr(gi, scope, expr_node);
|
||||||
|
_ = try gi.addBinaryNode(.store, lhs, rhs);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assignOp(
|
fn assignOp(
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ choice_selected: ?Choice = null,
|
||||||
output_buffer: std.ArrayListUnmanaged(OutputCommand) = .empty,
|
output_buffer: std.ArrayListUnmanaged(OutputCommand) = .empty,
|
||||||
output_scratch: std.ArrayListUnmanaged(u8) = .empty,
|
output_scratch: std.ArrayListUnmanaged(u8) = .empty,
|
||||||
current_choices: std.ArrayListUnmanaged(Choice) = .empty,
|
current_choices: std.ArrayListUnmanaged(Choice) = .empty,
|
||||||
|
variable_observers: std.StringHashMapUnmanaged(VariableObserver) = .empty,
|
||||||
|
|
||||||
globals: std.StringHashMapUnmanaged(Value) = .empty,
|
globals: std.StringHashMapUnmanaged(Value) = .empty,
|
||||||
stack: []Value = &.{},
|
stack: []Value = &.{},
|
||||||
call_stack: []CallFrame = &.{},
|
call_stack: []CallFrame = &.{},
|
||||||
|
|
@ -38,6 +40,17 @@ dump_writer: ?*std.Io.Writer = null,
|
||||||
|
|
||||||
pub const default_knot_name: [:0]const u8 = "$__main__$";
|
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) {
|
pub const Opcode = enum(u8) {
|
||||||
/// Exit the VM normally.
|
/// Exit the VM normally.
|
||||||
exit,
|
exit,
|
||||||
|
|
@ -337,6 +350,7 @@ pub fn deinit(story: *Story) void {
|
||||||
|
|
||||||
story.arena.deinit();
|
story.arena.deinit();
|
||||||
story.globals.deinit(gpa);
|
story.globals.deinit(gpa);
|
||||||
|
story.variable_observers.deinit(gpa);
|
||||||
story.current_choices.deinit(gpa);
|
story.current_choices.deinit(gpa);
|
||||||
story.output_scratch.deinit(gpa);
|
story.output_scratch.deinit(gpa);
|
||||||
story.output_buffer.deinit(gpa);
|
story.output_buffer.deinit(gpa);
|
||||||
|
|
@ -491,7 +505,7 @@ const StepSignal = union(enum) {
|
||||||
choices_ready,
|
choices_ready,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn step(vm: *Story) !StepSignal {
|
fn step(vm: *Story, variables: *VariablesState) !StepSignal {
|
||||||
assert(vm.call_stack_top > 0);
|
assert(vm.call_stack_top > 0);
|
||||||
assert(vm.can_advance == true);
|
assert(vm.can_advance == true);
|
||||||
|
|
||||||
|
|
@ -695,12 +709,13 @@ fn step(vm: *Story) !StepSignal {
|
||||||
},
|
},
|
||||||
.store_global => {
|
.store_global => {
|
||||||
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
|
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| {
|
if (peekStack(vm, 0)) |arg| {
|
||||||
try vm.setGlobal(global_name, arg);
|
try setGlobal(vm, global_name, arg);
|
||||||
_ = vm.popStack();
|
_ = popStack(vm);
|
||||||
try vm.pushStack(arg);
|
try pushStack(vm, arg);
|
||||||
|
try variables.update(gpa, global_name);
|
||||||
} else {
|
} else {
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
}
|
}
|
||||||
|
|
@ -799,6 +814,27 @@ fn resolveOutputStream(
|
||||||
return result.toOwnedSlice(gpa);
|
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 {
|
pub fn advance(story: *Story) !?[]const u8 {
|
||||||
const arena = story.arena.allocator();
|
const arena = story.arena.allocator();
|
||||||
const output_buffer = &story.output_buffer;
|
const output_buffer = &story.output_buffer;
|
||||||
|
|
@ -808,17 +844,26 @@ pub fn advance(story: *Story) !?[]const u8 {
|
||||||
output_scratch.clearRetainingCapacity();
|
output_scratch.clearRetainingCapacity();
|
||||||
|
|
||||||
if (!story.can_advance) return null;
|
if (!story.can_advance) return null;
|
||||||
while (story.can_advance) {
|
defer story.can_advance = false;
|
||||||
const signal = try story.step();
|
|
||||||
switch (signal) {
|
var variables_state: VariablesState = .{};
|
||||||
.exit => {
|
defer variables_state.changed.deinit(story.gpa);
|
||||||
story.is_exited = true;
|
|
||||||
break;
|
const signal = try story.step(&variables_state);
|
||||||
},
|
switch (signal) {
|
||||||
.done, .choices_ready => break,
|
.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;
|
if (output_buffer.items.len == 0) return null;
|
||||||
return try resolveOutputStream(story, arena, output_buffer.items[0..]);
|
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);
|
_ = 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 {
|
pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
|
||||||
return Dumper.dump(story, writer);
|
return Dumper.dump(story, writer);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const fatal = std.process.fatal;
|
const fatal = std.process.fatal;
|
||||||
const ink = @import("../root.zig");
|
const ink = @import("../root.zig");
|
||||||
|
const Story = ink.Story;
|
||||||
|
|
||||||
test "fixture - variable arithmetic" {
|
test "fixture - variable arithmetic" {
|
||||||
try testRuntimeFixture("variable-arithmetic");
|
try testRuntimeFixture("variable-arithmetic");
|
||||||
|
|
@ -256,6 +257,37 @@ test "fixture - I135 (Bools can be coerced)" {
|
||||||
try testRuntimeFixture("I135");
|
try testRuntimeFixture("I135");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "variable observer" {
|
||||||
|
const gpa = std.testing.allocator;
|
||||||
|
var io_w = std.Io.Writer.Allocating.init(gpa);
|
||||||
|
defer io_w.deinit();
|
||||||
|
|
||||||
|
var story = try ink.Story.fromSourceBytes(gpa,
|
||||||
|
\\VAR foo = 1
|
||||||
|
\\~foo = 10
|
||||||
|
, .{
|
||||||
|
.filename = "<STDIN>",
|
||||||
|
.error_writer = &io_w.writer,
|
||||||
|
});
|
||||||
|
defer story.deinit();
|
||||||
|
|
||||||
|
const TestState = struct { score: i64 };
|
||||||
|
var test_state = TestState{ .score = 0 };
|
||||||
|
|
||||||
|
try story.observeVariable("foo", struct {
|
||||||
|
fn observe(
|
||||||
|
value: Story.Value,
|
||||||
|
ctx: Story.VariableObserver.Context,
|
||||||
|
) anyerror!void {
|
||||||
|
const local_test_state: *TestState = @ptrCast(@alignCast(ctx.ptr));
|
||||||
|
local_test_state.score += value.int;
|
||||||
|
}
|
||||||
|
}.observe, .{ .ptr = &test_state });
|
||||||
|
|
||||||
|
_ = try story.advance();
|
||||||
|
return std.testing.expectEqual(10, test_state.score);
|
||||||
|
}
|
||||||
|
|
||||||
const Options = struct {
|
const Options = struct {
|
||||||
input_reader: *std.Io.Reader,
|
input_reader: *std.Io.Reader,
|
||||||
error_writer: *std.Io.Writer,
|
error_writer: *std.Io.Writer,
|
||||||
|
|
@ -265,7 +297,7 @@ const Options = struct {
|
||||||
fn testRunner(gpa: std.mem.Allocator, source_bytes: [:0]const u8, options: Options) !void {
|
fn testRunner(gpa: std.mem.Allocator, source_bytes: [:0]const u8, options: Options) !void {
|
||||||
const io_r = options.input_reader;
|
const io_r = options.input_reader;
|
||||||
const io_w = options.transcript_writer;
|
const io_w = options.transcript_writer;
|
||||||
var story = try ink.Story.fromSourceBytes(gpa, source_bytes, .{
|
var story = try Story.fromSourceBytes(gpa, source_bytes, .{
|
||||||
.filename = "<STDIN>",
|
.filename = "<STDIN>",
|
||||||
.error_writer = options.error_writer,
|
.error_writer = options.error_writer,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue