diff options
| author | eunhhu <[email protected]> | 2026-09-24 17:14:54 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-24 17:14:54 +0100 |
| commit | b1ce1927022d374198441afafeb27199fc8a052a (patch) | |
| tree | 6021f175f4c67ed03b44240eaa2f90d846f12f6f /prototypes/workbench/engine.zig | |
| parent | d34f9426d5317a1948f12f229439946728bc342a (diff) | |
| download | pure-b1ce1927022d374198441afafeb27199fc8a052a.tar.gz pure-b1ce1927022d374198441afafeb27199fc8a052a.zip | |
Establish native micro runtime checkpoint
Diffstat (limited to 'prototypes/workbench/engine.zig')
| -rw-r--r-- | prototypes/workbench/engine.zig | 700 |
1 files changed, 700 insertions, 0 deletions
diff --git a/prototypes/workbench/engine.zig b/prototypes/workbench/engine.zig new file mode 100644 index 0000000..16907ef --- /dev/null +++ b/prototypes/workbench/engine.zig @@ -0,0 +1,700 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +pub const std = @import("std"); +const root = @import("root"); +const fixed_definition = @hasDecl(root, "pure_definition"); +pub const Evaluation = @import("evaluation.zig").Evaluation; +pub const Tables = @import("evaluation.zig").Tables; + +// A fixed native definition may request exact capacities from its generated +// build root. Browser/authoring builds retain the full language limits. +pub const max_states = if (@hasDecl(root, "pure_max_states")) root.pure_max_states else 32; +pub const max_inputs = if (@hasDecl(root, "pure_max_inputs")) root.pure_max_inputs else 8; +pub const max_instructions = if (@hasDecl(root, "pure_max_instructions")) root.pure_max_instructions else 512; +pub const max_handlers = if (@hasDecl(root, "pure_max_handlers")) root.pure_max_handlers else 64; +pub const max_stack = 64; +pub const max_pointer_targets = if (@hasDecl(root, "pure_max_pointer_targets")) root.pure_max_pointer_targets else 24; +comptime { + if (max_states > 32 or max_inputs > 8 or max_instructions > 512 or + max_handlers > 64 or max_pointer_targets > 24) + @compileError("native profile exceeds the language limits"); +} +pub const event_target = 255; +pub const no_pc = std.math.maxInt(u32); +pub const frame_dt: f64 = 1.0 / 60.0; + +pub const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +pub const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +pub const Handler = struct { + event: u32 = 0, + start: u32 = 0, + length: u32 = 0, + parameter_base: u8 = 0, + parameter_count: u8 = 0, +}; +pub const PointerTarget = struct { input: u8, base: u8, count: u8 }; +pub const Program = struct { + pointer_targets: [max_pointer_targets]PointerTarget = undefined, + pointer_target_count: u8 = 0, + routed_inputs: u8 = 0, + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +const shared_program: Program = if (fixed_definition) blk: { + const definition = root.pure_definition; + var program: Program = .{}; + program.state_count = definition.states_count; + program.input_count = definition.inputs_count; + program.instruction_count = definition.instructions_count; + program.handler_count = definition.handlers_count; + program.pointer_target_count = definition.pointer_targets_count; + program.routed_inputs = definition.routed_inputs; + for (definition.initials, 0..) |value, i| program.initials[i] = value; + for (definition.code, 0..) |inst, i| + program.instructions[i] = .{ .value = inst.value, .op = inst.op, .arg = inst.arg }; + for (definition.handlers_data, 0..) |handler, i| + program.handlers[i] = .{ .event = handler.event, .start = handler.start, + .length = handler.length, .parameter_base = handler.base, + .parameter_count = handler.count }; + for (definition.targets_data, 0..) |target, i| + program.pointer_targets[i] = .{ .input = target.input, .base = target.base, .count = target.count }; + break :blk program; +} else .{}; +// Loading a definition/optional restored values and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +pub const Pending = struct { + program: Program = .{}, + values: [max_states]f64 = @splat(0), + value_mask: u32 = 0, +}; +pub const Workspace = union { staging: Pending, reuse: Tables }; +pub const Engine = struct { + active: if (fixed_definition) bool else Program = if (fixed_definition) false else .{}, + workspace: if (fixed_definition) void else Workspace = if (fixed_definition) {} else .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, + + fn program(self: *Engine) *const Program { + return if (comptime fixed_definition) &shared_program else &self.active; + } + + pub fn reuseTables(self: *Engine) ?*Tables { + if (comptime fixed_definition) return null; + return if (self.staging_open) null else &self.workspace.reuse; + } + + pub fn finishStaging(self: *Engine) void { + if (comptime fixed_definition) return; + self.workspace = .{ .reuse = .{} }; + self.staging_open = false; + self.evaluation.forget(null, 3); + } + + pub fn public(_: *Engine, code: Code) u32 { + return @intFromEnum(code); + } + + pub fn stagingFail(self: *Engine, code: Code) u32 { + if (self.staging_error == .ok) self.staging_error = code; + return self.public(code); + } + + pub fn reset_all(self: *Engine) void { + self.* = .{}; + } + + pub fn installFixed(self: *Engine, identity: *const [32]u8, + state_count: u32, input_count: u32, instruction_count: u32, + handler_count: u32, target_count: u32, restored: ?[*]const f64) u32 { + if (comptime !fixed_definition) return self.public(.invalid); + const definition = root.pure_definition; + if (!std.mem.eql(u8, identity, &definition.identity) or + state_count != definition.states_count or input_count != definition.inputs_count or + instruction_count != definition.instructions_count or + handler_count != definition.handlers_count or target_count != definition.pointer_targets_count) + return self.public(.invalid); + var next: [max_states]f64 = @splat(0); + for (0..state_count) |i| { + const value = if (restored) |values| values[i] else shared_program.initials[i]; + if (!std.math.isFinite(value)) return self.public(.non_finite); + next[i] = value; + } + self.active = true; + self.values = next; + self.staging_open = false; + self.staging_error = .ok; + self.evaluation.forget(null, 3); + if (restored != null) { + self.tick = 0; + self.successful_events = 0; + } + self.revision +%= 1; + self.last_error = .ok; + self.error_pc = no_pc; + self.last_ops = 0; + return 0; + } + + pub fn begin_program(self: *Engine, state_count: u32, input_count: u32) u32 { + self.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (self.staging_open) self.finishStaging(); + return self.stagingFail(.capacity); + } + self.workspace = .{ .staging = .{ .program = .{ .state_count = state_count, .input_count = input_count } } }; + self.staging_open = true; + self.evaluation.forget(null, 3); + return 0; + } + + pub fn abort_program(self: *Engine) void { + if (self.staging_open) self.finishStaging(); + } + + pub fn put_state(self: *Engine, id: u32, initial: f64, old_id: i32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + if (id >= self.workspace.staging.program.state_count or old_id < -1) return self.stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= self.program().state_count) return self.stagingFail(.bad_state); + if (self.workspace.staging.program.declared[id]) return self.stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return self.stagingFail(.non_finite); + self.workspace.staging.program.initials[id] = initial; + self.workspace.staging.program.old_ids[id] = old_id; + self.workspace.staging.program.declared[id] = true; + return 0; + } + + pub fn put_instruction(self: *Engine, op: u32, arg: u32, value: f64) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (p.instruction_count == max_instructions) return self.stagingFail(.capacity); + if (op > 26) return self.stagingFail(.invalid); + if (!std.math.isFinite(value)) return self.stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; + } + + pub fn validEvent(_: *Engine, event: u32, inputs: u32) bool { + return event <= inputs * 2 or + (event >= 17 and event - 17 < inputs) or + (event >= 25 and event - 25 < inputs) or + (event >= 33 and event - 33 < inputs); + } + + pub fn put_handler(self: *Engine, event: u32, start: u32, length: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (p.handler_count == max_handlers) return self.stagingFail(.capacity); + if (!self.validEvent(event, p.input_count)) return self.stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; + } + + pub fn bind_handler(self: *Engine, handler: u32, base: u32, count: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (handler >= p.handler_count or base >= p.state_count) return self.stagingFail(.bad_state); + if (count == 0 or count > 8 or count > p.state_count - base) return self.stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return self.stagingFail(.invalid); + h.parameter_base = @intCast(base); + h.parameter_count = @intCast(count); + return 0; + } + + // A route is a static program-local index, never a persistent object identity. + pub fn put_pointer_target(self: *Engine, input: u32, base: u32, count: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (p.pointer_target_count == max_pointer_targets) return self.stagingFail(.capacity); + if (input >= p.input_count) return self.stagingFail(.bad_event); + if (base >= p.state_count or count == 0 or count > 8 or count > p.state_count - base) return self.stagingFail(.bad_state); + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (base < @as(u32, t.base) + t.count and t.base < base + count) return self.stagingFail(.invalid); + if (t.input == input and t.count != count) return self.stagingFail(.bad_state); + } + p.pointer_targets[p.pointer_target_count] = .{ .input = @intCast(input), .base = @intCast(base), .count = @intCast(count) }; + p.pointer_target_count += 1; + p.routed_inputs |= @as(u8, 1) << @as(u3, @intCast(input)); + return 0; + } + + pub fn bind_target_handler(self: *Engine, handler: u32, count: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (handler >= p.handler_count or count == 0 or count > 8) return self.stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return self.stagingFail(.invalid); + h.parameter_base = event_target; + h.parameter_count = @intCast(count); + return 0; + } + + pub fn eventInput(_: *Engine, event: u32) u32 { + return if (event >= 33) event - 33 else if (event >= 25) event - 25 else if (event >= 17) event - 17 else (event - 1) / 2; + } + + pub fn validate(self: *Engine, p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= self.program().state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (!self.validEvent(h.event, p.input_count)) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.parameter_base == event_target) { + if (h.event == 0 or h.event >= 33 or h.parameter_count == 0 or h.parameter_count > 8) return .bad_event; + var found = false; + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (t.input != self.eventInput(h.event)) continue; + if (t.count != h.parameter_count) return .bad_state; + found = true; + } + if (!found) return .bad_event; + } else if (h.parameter_count > 8 or @as(u32, h.parameter_base) + h.parameter_count > p.state_count) return .bad_state; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) { + // Bound rules may share exactly one body, never a partial range. + if (h.parameter_count == 0 or h.parameter_count != other.parameter_count or + h.start != other.start or h.length != other.length) return .invalid; + } + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 26) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op >= 20) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + if ((inst.op == 24 or inst.op == 25) and inst.arg >= h.parameter_count) return .bad_state; + } + } + if (owner == null) return .invalid; + } + } + return .ok; + } + + pub fn put_restored_value(self: *Engine, id: u32, value: f64) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const pending = &self.workspace.staging; + if (id >= pending.program.state_count) return self.stagingFail(.bad_state); + const bit = @as(u32, 1) << @as(u5, @intCast(id)); + if (pending.value_mask & bit != 0) return self.stagingFail(.bad_state); + if (!std.math.isFinite(value)) return self.stagingFail(.non_finite); + pending.values[id] = value; + pending.value_mask |= bit; + return 0; + } + + pub fn commit_program(self: *Engine) u32 { + return self.commit(false); + } + + pub fn commit_restored_program(self: *Engine) u32 { + return self.commit(true); + } + + pub fn commit(self: *Engine, restoring: bool) u32 { + if (!self.staging_open) return self.public(.invalid); + if (self.staging_error != .ok) return self.public(self.staging_error); + const count = self.workspace.staging.program.state_count; + const expected: u32 = if (count == 32) 0xffffffff else (@as(u32, 1) << @as(u5, @intCast(count))) - 1; + if (self.workspace.staging.value_mask != (if (restoring) expected else @as(u32, 0))) + return self.stagingFail(.bad_state); + const code = self.validate(&self.workspace.staging.program); + if (code != .ok) return self.stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..self.workspace.staging.program.state_count) |i| { + const old_id = self.workspace.staging.program.old_ids[i]; + next[i] = if (restoring) self.workspace.staging.values[i] else if (old_id < 0) self.workspace.staging.program.initials[i] else self.values[@intCast(old_id)]; + } + self.active = self.workspace.staging.program; + self.values = next; + self.finishStaging(); + if (restoring) { + self.tick = 0; + self.successful_events = 0; + } + self.revision +%= 1; + self.last_error = .ok; + self.error_pc = no_pc; + self.last_ops = 0; + return 0; + } + + pub fn reset_state(self: *Engine) void { + self.values = if (fixed_definition and !self.active) @splat(0) else self.program().initials; + self.evaluation.forget(self.reuseTables(), 3); + self.tick = 0; + self.successful_events = 0; + self.last_error = .ok; + self.error_pc = no_pc; + self.last_ops = 0; + } + + const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, + }; + + pub fn executionFail(_: *Engine, result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; + } + + const EventContext = struct { dt: f64 = 0, x: f64 = 0, y: f64 = 0, parameter_base: u32 = 0, parameter_count: u32 = 0, point: bool = false, scalar: bool = false, target: ?u32 = null }; + + pub fn execute(self: *Engine, values: *[max_states]f64, start: u32, length: u32, context: EventContext, pure: bool, budget: ?*Evaluation) Execution { + var result: Execution = .{}; + if (comptime fixed_definition) { + if (!self.active) { + if (start != 0 or length != 0) return self.executionFail(result, .invalid, no_pc); + if (pure) return self.executionFail(result, .stack, 0); + return result; + } + } + const p = self.program(); + if (start > p.instruction_count or length > p.instruction_count - start) return self.executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return self.executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op >= 20)) return self.executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2, 22, 23, 24, 26 => 0, + 7, 16, 20, 21, 25 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return self.executionFail(result, .invalid, pc), + }; + if (sp < need) return self.executionFail(result, .stack, pc); + if (inst.op <= 2 or inst.op == 22 or inst.op == 23 or inst.op == 24 or inst.op == 26) { + if (sp == max_stack) return self.executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return self.executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 24 => blk: { + if (inst.arg >= context.parameter_count) return self.executionFail(result, .bad_state, pc); + const state = context.parameter_base + inst.arg; + if (state >= p.state_count) return self.executionFail(result, .bad_state, pc); + break :blk values[state]; + }, + 26 => if (context.scalar) context.x else return self.executionFail(result, .bad_event, pc), + 2 => context.dt, + 22, 23 => if (!context.point) return self.executionFail(result, .bad_event, pc) else if (inst.op == 22) context.x else context.y, + else => unreachable, + }; + if (!std.math.isFinite(value)) return self.executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20 or inst.op == 25) { + if (inst.op == 25 and inst.arg >= context.parameter_count) return self.executionFail(result, .bad_state, pc); + const state = if (inst.op == 25) context.parameter_base + inst.arg else inst.arg; + if (state >= p.state_count) return self.executionFail(result, .bad_state, pc); + sp -= 1; + values[state] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return self.executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return self.executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return self.executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return self.executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return self.executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; + } + + pub fn dispatch(self: *Engine, event: u32, dt: f64) u32 { + return self.dispatchContext(event, .{ .dt = dt }); + } + + pub fn dispatch_point(self: *Engine, event: u32, x: f64, y: f64) u32 { + return self.dispatchContext(event, .{ .x = x, .y = y, .point = true }); + } + + pub fn dispatch_target(self: *Engine, event: u32, target: u32, x: f64, y: f64) u32 { + return self.dispatchContext(event, .{ .x = x, .y = y, .point = true, .target = target }); + } + + pub fn dispatch_value(self: *Engine, event: u32, value: f64) u32 { + return self.dispatchContext(event, .{ .x = value, .scalar = true }); + } + + pub fn dispatchContext(self: *Engine, event: u32, context: EventContext) u32 { + const dt = context.dt; + self.last_error = .ok; + self.error_pc = no_pc; + self.last_ops = 0; + if (comptime fixed_definition) { + if (!self.active) { + if (event != 0 or context.point or context.scalar) { + self.last_error = .bad_event; + return self.public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + self.last_error = .non_finite; + return self.public(.non_finite); + } + if (dt != frame_dt) { + self.last_error = .bad_frame; + return self.public(.bad_frame); + } + self.tick +%= 1; + self.successful_events +%= 1; + return 0; + } + } + if (!self.validEvent(event, self.program().input_count) or + ((context.point or context.scalar) and event == 0) or + (context.scalar != (event >= 33)) or + (!context.point and !context.scalar and event > self.program().input_count * 2)) + { + self.last_error = .bad_event; + return self.public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + self.last_error = .non_finite; + return self.public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + self.last_error = .bad_frame; + return self.public(.bad_frame); + } + var route: ?PointerTarget = null; + const routed = event != 0 and self.program().routed_inputs & (@as(u8, 1) << @as(u3, @intCast(self.eventInput(event)))) != 0; + if (context.target) |target| { + if (!routed or target >= self.program().pointer_target_count or + self.program().pointer_targets[target].input != self.eventInput(event)) + { + self.last_error = .bad_event; + return self.public(.bad_event); + } + route = self.program().pointer_targets[target]; + } else if (routed) { + self.last_error = .bad_event; + return self.public(.bad_event); + } + var pending = self.values; + for (self.program().handlers[0..self.program().handler_count]) |h| { + if (h.event != event) continue; + var bound = context; + bound.parameter_base = if (h.parameter_base == event_target) route.?.base else h.parameter_base; + bound.parameter_count = h.parameter_count; + const result = self.execute(&pending, h.start, h.length, bound, false, null); + self.last_ops += result.ops; + if (result.code != .ok) { + self.last_error = result.code; + self.error_pc = result.pc; + return self.public(result.code); + } + } + if (self.evaluation.cache_count != 0 and !self.staging_open) { + var changed: u32 = 0; + for (0..self.program().state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(self.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + self.evaluation.changed(self.reuseTables(), changed); + } + self.values = pending; + if (event == 0) self.tick +%= 1; + self.successful_events +%= 1; + return 0; + } + + pub fn eval_at(self: *Engine, start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = self.values; + const result = self.execute(&values, start, length, .{}, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); + } + + pub fn configure_evaluation(self: *Engine, cache: u32, history: u32) u32 { + if (comptime fixed_definition) if (cache != 0) return self.public(.capacity); + return if (self.evaluation.configure(self.reuseTables(), cache, history)) 0 else self.public(.capacity); + } + + pub fn forget_evaluation(self: *Engine, flags: u32) void { + self.evaluation.forget(self.reuseTables(), flags); + } + + pub fn begin_evaluation(self: *Engine, limit: u32) void { + self.evaluation.begin(limit); + } + + pub fn eval_bounded(self: *Engine, start: u32, length: u32) f64 { + const e = &self.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(self.reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = self.values; + const result = self.execute(&values, start, length, .{}, true, e); + e.status = self.public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(self.reuseTables(), start, length, result.value, result.dependencies); + return result.value; + } + + pub fn evaluation_info(self: *Engine, id: u32) u32 { + const e = &self.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + if (fixed_definition) @as(u32, 0) else @as(u32, @sizeOf(Tables)), + 7 => @sizeOf(Evaluation), + 8 => if (fixed_definition) 0 else @sizeOf(Tables), + 9 => @intFromBool(!self.staging_open), + 10 => if (fixed_definition) 0 else @sizeOf(Workspace) - @sizeOf(Pending), + else => 0, + }; + } + + pub fn get_state(self: *Engine, id: u32) f64 { + if (comptime fixed_definition) if (!self.active) return std.math.nan(f64); + return if (id < self.program().state_count) self.values[id] else std.math.nan(f64); + } + + pub fn info(self: *Engine, id: u32) u32 { + const loaded = if (fixed_definition) self.active else true; + return switch (id) { + 0 => self.tick, + 1 => self.public(self.last_error), + 2 => self.error_pc, + 3 => self.last_ops, + 4 => self.revision, + 5 => if (loaded) self.program().state_count else 0, + 6 => if (loaded) self.program().instruction_count else 0, + 7 => if (loaded) self.program().handler_count else 0, + 8 => if (loaded) self.program().input_count else 0, + 9 => self.successful_events, + else => 0, + }; + } + + pub fn layout(self: *Engine, id: u32) u32 { + if (comptime fixed_definition) { + if (!self.active and (id == 4 or id == 5 or id == 9)) return 0; + } + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => self.program().instruction_count * @sizeOf(Instruction) + self.program().handler_count * @sizeOf(Handler) + @as(u32, self.program().pointer_target_count) * @sizeOf(PointerTarget), + 5 => self.program().state_count * @sizeOf(f64), + 6 => if (fixed_definition) 0 else @sizeOf(Pending), + 7 => max_stack * @sizeOf(f64), + 8 => @sizeOf(PointerTarget), + 9 => self.program().pointer_target_count, + else => 0, + }; + } +}; |
