Skip to content

observer: export the predicate classifier the AOT had been copying - #1053

Merged
InauguralPhysicist merged 1 commit into
mainfrom
observer-predicate-export
Aug 28, 2026
Merged

observer: export the predicate classifier the AOT had been copying#1053
InauguralPhysicist merged 1 commit into
mainfrom
observer-predicate-export

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

Groundwork for InauguralSystems/ouroboros#119 and #122.

What

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 observer_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:

  • 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#119predicate 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 — 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_used is the single 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 (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 used slot 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 — b and c inherit a's verdicts.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt

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.
@InauguralPhysicist

Copy link
Copy Markdown
Collaborator Author

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 s && s->used && !opaque; observer_predicate_at does opaque → env_obs_slot → used.

Result-equivalent on every input, and that was checked rather than assumed: vm_slot_value_opaque self-guards on !e || idx < 0 || idx >= e->count while env_obs_slot guards obs_cap. The two arrays are independent, so no input can reach one guard through the other, and env_obs_slot is NULL-safe.

The one observable difference: vm_slot_value_opaque takes env_dump_lock(e), and is now reached on SLOT/NAME paths that previously short-circuited before it (an out-of-range slot, or used == 0). Under g_vm_multithreaded that is one extra lock/unlock pair on those paths. No result changes; it is a no-op single-threaded via the env_mt_shared gate.

Recording it because "pure refactor" is a claim about behaviour, and this is the single place the claim is narrower than it sounds.

@InauguralPhysicist
InauguralPhysicist merged commit 0a99967 into main Aug 28, 2026
18 checks passed
@InauguralPhysicist
InauguralPhysicist deleted the observer-predicate-export branch August 28, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant