observer: export the predicate classifier the AOT had been copying - #1053
Conversation
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.
|
One caveat on the "pure refactor" claim, surfaced by blind review and worth stating explicitly rather than leaving to be discovered. The composite reorders the conjuncts. The named arms were Result-equivalent on every input, and that was checked rather than assumed: The one observable difference: Recording it because "pure refactor" is a claim about behaviour, and this is the single place the claim is narrower than it sounds. |
Groundwork for InauguralSystems/ouroboros#119 and #122.
What
The three
PREDICATEopcodes each did the same four things inline — the #708 opaque band, the #711 query view, the kind dispatch, and (for the named ops) theusedcheck. This lifts that into one exportedobserver_predicate_at()and has all three call it.Pure refactor: 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 behaviours. Measured consequences, both filed:unobserved:returned a confident1from 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."predicate of xdid 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/t28exercise bare predicates on plain observed numerics — exactly the region where the copy and the original agree — so the AOT differential was green over three unimplemented behaviours.One thing deliberately not changed
require_usedis the single genuine difference between the opcodes: the bare op has never testedused, the named ops do, andenv_obs_slotbounds-checks without testing it (its contract covers range only). So on an in-range, never-assigned slot the bare and named forms genuinely disagree.That is preserved as a parameter rather than normalised. This commit changes no behaviour; deciding which slots should answer is a separate call that needs 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
usedslot at all, so a classifier-level check is never reached on exactly the path that hangs.Verification
Full suite 4119/4119 (unchanged count, unchanged behaviour). Downstream, with this export in place, the AOT's named form is byte-identical to the VM on a three-channel fixture, and reverting its resolution to last-observed turns that fixture red —
bandcinherita's verdicts.🤖 Generated with Claude Code
https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt