From 66954c898eb98b9485fa64a378994287e7051919 Mon Sep 17 00:00:00 2001 From: InauguralPhysicist Date: Thu, 27 Aug 2026 19:37:01 -0500 Subject: [PATCH] observer: export the predicate classifier the AOT had been copying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three PREDICATE opcodes each did the same four things inline — the #708 opaque band, the #711 query view, the kind dispatch, and (for the named ops) the `used` check. This lifts that into one exported function and has all three call it. Pure refactor upstream: 4119/4119, the same count as before. WHY EXPORT rather than tidy in place. ouroboros's AOT runtime links this runtime and has been carrying a partial COPY of this logic — a bounds check and a switch straight onto observer_slot_*, with none of the four. Measured consequences, both now filed: - ouroboros#122: a bare predicate inside `unobserved:` returned a confident 1 from generated code where the VM raises and exits 1. A silent wrong answer, in the feature whose entire job is to say "I cannot answer that". - ouroboros#119: `predicate of x` did not compile at all, so no multi-channel program could be AOT-compiled — bare predicates read the last-observed alias, which is the wrong binding as soon as a loop assigns more than one observed variable. "The VM is the byte-exact oracle" only means something if the compiler CALLS the oracle. t27/t28 exercise bare predicates on plain observed numerics, which is exactly where the copy and the original agree, so the differential was green over three unimplemented behaviours. `require_used` is the one genuine difference between the opcodes: the bare op has never tested `used`, the named ops do, and env_obs_slot bounds-checks without testing it. That is PRESERVED as a parameter rather than normalised — this commit changes no behaviour, and deciding which slots answer is a separate call needing its own evidence. The #871 unobserved raise stays in the opcode handlers: it needs the source line, and a binding assigned inside the block has no `used` slot at all, so a classifier-level check is never reached on the path that hangs. --- src/eigenscript.h | 5 +++++ src/vm.c | 57 +++++++++++++++++++++++++++++------------------ 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/eigenscript.h b/src/eigenscript.h index c6fd810f..f199490c 100644 --- a/src/eigenscript.h +++ b/src/eigenscript.h @@ -388,6 +388,11 @@ void env_dump_unlock(struct Env *e); * (called from observer_slot_reset so a torn-down env can't be read stale). */ void vm_obs_slot_dropped(struct Env *e); int observer_slot_converged(const struct ObserverSlot *s); +/* Classify a binding's observer slot as the VM's PREDICATE opcodes do (opaque + * band, query view, kind dispatch). Exported for the AOT runtime, which must + * share this implementation rather than copy it (ouroboros#119/#122). + * require_used: 0 mirrors the bare op, 1 the named ops. */ +int observer_predicate_at(struct Env *e, int idx, int kind, int require_used); int observer_slot_equilibrium(const struct ObserverSlot *s); int observer_slot_improving(const struct ObserverSlot *s); int observer_slot_diverging(const struct ObserverSlot *s); diff --git a/src/vm.c b/src/vm.c index 8a4f38ea..541e4f2a 100644 --- a/src/vm.c +++ b/src/vm.c @@ -346,6 +346,34 @@ static int vm_slot_value_opaque(Env *e, int idx) { return r; } +/* Classify a binding's observer slot exactly as the PREDICATE opcodes do: + * the #708 opaque band, the #711 query view, and the kind dispatch, in one + * place. EXPORTED (eigenscript.h) because the AOT compiler's runtime needs + * the same answer and had been carrying a partial COPY of this logic — + * ouroboros#119/#122, where a bare predicate inside `unobserved:` returned a + * confident `1` from generated code while the VM raised. "The VM is the + * byte-exact oracle" only means something if the compiler CALLS the oracle + * instead of reimplementing a subset of it. + * + * `require_used` is the one place the three opcodes genuinely differ: the + * bare op does not test `used`, the named ops do. That is preserved here + * rather than normalised — this commit is a refactor, and changing which + * slots answer is a separate decision with its own evidence. env_obs_slot + * bounds-checks but does NOT test `used` (its contract covers range only), + * so the distinction is load-bearing on an in-range never-assigned slot. + * + * The #871 unobserved-depth raise stays in the opcode handlers: it needs the + * source line, and a binding assigned inside the block has no `used` slot at + * all, so a classifier-level check is never reached on the path that hangs. */ +int observer_predicate_at(Env *e, int idx, int kind, int require_used) { + if (vm_slot_value_opaque(e, idx)) return 0; + const ObserverSlot *s = env_obs_slot(e, idx); + if (!s) return 0; + if (require_used && !s->used) return 0; + ObserverSlot q = vm_slot_query_view(e, idx, s); + return vm_slot_predicate(&q, (uint16_t)kind); +} + /* Phase 5: VM execution state (g_vm), loop-stall accounting * (g_loop_stall_count, g_loop_iterations, g_loop_exit_reason), * control-flow / error-state globals (g_return_val, g_returning, @@ -5456,16 +5484,11 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, vm_push_slot(slot_null()); DISPATCH(); } - int result = 0; - const ObserverSlot *s = - vm_slot_value_opaque(g_last_obs_slot_env, g_last_obs_slot_idx) - ? NULL /* #708: fn/builtin binding — nothing claimable */ - : env_obs_slot(g_last_obs_slot_env, g_last_obs_slot_idx); - if (s) { - ObserverSlot q = vm_slot_query_view(g_last_obs_slot_env, - g_last_obs_slot_idx, s); /* #711 */ - result = vm_slot_predicate(&q, kind); - } + /* #708 opaque band + #711 query view + kind dispatch, shared with the + * named ops and with the AOT runtime. require_used=0: the bare op has + * never tested `used`, unlike PREDICATE_SLOT/NAME below. */ + int result = observer_predicate_at(g_last_obs_slot_env, + g_last_obs_slot_idx, kind, 0); vm_push(make_num(result ? 1.0 : 0.0)); DISPATCH(); } @@ -5481,12 +5504,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, DISPATCH(); } Env *e = frame->fn_env; - int result = 0; - const ObserverSlot *ps_l = env_obs_slot(e, (int)slot); - if (ps_l && ps_l->used && !vm_slot_value_opaque(e, (int)slot)) { - ObserverSlot q = vm_slot_query_view(e, (int)slot, ps_l); /* #711 */ - result = vm_slot_predicate(&q, kind); /* #708 */ - } + int result = observer_predicate_at(e, (int)slot, kind, 1); vm_push(make_num(result ? 1.0 : 0.0)); DISPATCH(); } @@ -5510,12 +5528,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume, vm_push_slot(slot_null()); DISPATCH(); } - int result = 0; - const ObserverSlot *ps_n = env_obs_slot(oe, oidx); - if (ps_n && ps_n->used && !vm_slot_value_opaque(oe, oidx)) { - ObserverSlot q = vm_slot_query_view(oe, oidx, ps_n); /* #711 */ - result = vm_slot_predicate(&q, kind); /* #708 */ - } + int result = observer_predicate_at(oe, oidx, kind, 1); vm_push(make_num(result ? 1.0 : 0.0)); DISPATCH(); }