gate: every opcode carries a recorded observer classification (#972) - #1024
Merged
Conversation
The elision behind #972 — stop emitting observer bookkeeping when a program provably never reads observer state — fails silently and totally if its reader set is incomplete. An unlisted reader means a program gates its own bookkeeping off and then reads slots nobody updated: every binding answers "equilibrium" forever, no crash, nothing to fail on. So the reader set must be pinned against the closed opcode list, and this lands that pin ALONE — no elision, no runtime change. The classification is not derived, because it cannot be. Five derivations were tried on #972 and all five produced a confident WRONG answer, in both directions: 1. grep the read-side API -> missed obs_stall_trajectory(), which reads s->used/s->dH/ s->entropy with no observer_slot_* call at all 2. objdump -dr relocations -> missed the same reader: a struct-field read emits no symbol reference 3. scan for struct-field reads -> false-positived on three unrelated functions reading ->n 4. classify by scanning `case OP_X:` -> matched NOTHING (the VM dispatches through CASE(NAME) computed-goto macros) and reported a clean, empty set 5. same scan repaired to CASE(X) -> matched EVERYTHING, including a scope marker and a writer, because the handler terminator did not match and each scan ran on into later handlers The C is the OPEN level: a read can be spelled arbitrarily many ways. The enum is the CLOSED one. So the gate classifies nothing and asks the one mechanically answerable question — HAS EVERY OPCODE BEEN CLASSIFIED BY A HUMAN? A missing marker is a loud unanswered question at the moment an opcode is added, which is exactly when its author knows the answer and nobody else ever will. Same shape as failsoft_classify_check.sh. All 94 opcodes were classified by reading their handlers in vm.c: READS=15 WRITES=10 DIAG=1 NONE=68. Two verdicts contradict the opcode names, and only reading finds them: OP_OBSERVE_ASSIGN is a NO-OP (the slot model observes at OP_OBSERVE_NAME_POST, after the SET), and the bare OP_INTERROGATE reads NO observer state — when/where/why/how on a value operand return constants, since observer state is binding-keyed. obs:DIAG is a fourth marker, added because collapsing it either way is wrong and the choice is a decision rather than a derivation. OP_LOOP_CAP_CHECK is emitted at every plain loop and its only reach into observer state is eigs_observe_safepoint's SIGUSR1 dump. READS would pin bookkeeping on for every loop in every program and delete the entire win (mechanical-gates 9: an unbounded closure marks every loop a reader); NONE would silently drop the obligation that the dump must SAY the gate is closed rather than render every binding as "equilibrium" (mechanical-gates 11). DIAG records that waiver as a marker the elision can enumerate, instead of as prose nobody re-reads. The gate (tools/obs_marker_check.sh, suite [99t]) runs 7 assertions: enum parses at all; no orphan markers (the reverse direction — a marker left behind by a rename); every opcode marked; every marker in the closed vocabulary; the OP_COUNT exemption still fires (present, last, unmarked); an opcode-count floor; and a floor on the READS set, which is the liveness scan's input and whose collapse would elide bookkeeping language-wide while every other check stayed green. Self-test: eleven mutations, each verified to be witnessed by exactly one fixture. That verification found two gaps in the first version — the "OP_COUNT is not last" branch and the EXPECTED_CHECKS pin both survived being neutered, i.e. were advertised guards enforcing nothing — and both now have their own fixture (an opcode appended past the sentinel; a gate-level mutation that deletes an assertion). The first attempt at that meta-mutation was itself invalid: the mutants were copied outside the tree, so `cd $(dirname $0)/..` left them unable to see any header and all of them failed identically. An unstartable mutant is not a caught one. Also fixes a matcher this change falsified in a neighbouring gate: vm_operand_width_check.sh's selftest anchored on `OP_INTERROGATE,[space]* /*`, which the new marker sits between. It failed loudly ("could not remove the comment") rather than passing vacuously, which is the only reason it was noticed. Gates: release 4070/4070; ASan+UBSan detect_leaks=1 4059/4059, leak tally 0. No behaviour change — the markers are comments and the gate is a test. Refs #972 (does NOT close it: the elision, its perf measurement against #915's ceiling, and the three named bypass routes — assembled chunks, the JIT's own reader, and the SIGUSR1 dump's "gate is closed" line — remain open). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
mechanical-gates 63 says a gate whose blindness depends on which awk is installed is a coin flip with a green badge, and the dangerous reading is the one that produces a plausible COUNT while every comparison is false — no floor fires, so it prints OK while measuring nothing. Shadowing awk on PATH: mawk, nawk (one-true-awk, what macOS ships) and busybox awk return the identical clean verdict AND the identical selftest verdict. Recorded in the header, because that verification is not automatic and nobody who trusts the list will repeat it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands the completeness pin for #972's observer-liveness elision — the marker gate only. No elision, no runtime change: the markers are comments and the gate is a test.
Why a marker, not a scan
The elision fails silently and totally when its reader set is incomplete: a program gates its own bookkeeping off, then reads slots nobody updated, and every binding answers
equilibriumforever with no crash and nothing to fail on.That set cannot be derived from the C. Five derivations were tried on #972 and all five gave a confident wrong answer, in both directions:
obs_stall_trajectory()— readss->used/s->dH/s->entropywith noobserver_slot_*call anywhereobjdump -drrelocations->ncase OP_X:handlersCASE(NAME)computed-goto macros — and reported a clean empty setCASE(X)The C is the open level (a read has arbitrarily many spellings); the enum is the closed one. So the gate classifies nothing. It asks the one mechanically answerable question — has every opcode been classified by a human? — and goes red on any that has not. A missing marker is a loud unanswered question at the moment an opcode is added, which is when its author knows the answer and nobody else ever will. Same shape, same reason, as
tools/failsoft_classify_check.sh.What landed
All 94 opcodes read by hand in
src/vm.cand recorded insrc/vm.h:READS=15 WRITES=10 DIAG=1 NONE=68Two verdicts contradict the opcode names, and only reading finds them:
OP_OBSERVE_ASSIGNis a no-op —ip += 2; DISPATCH();. The slot model observes atOP_OBSERVE_NAME_POST, after the SET.OP_INTERROGATEreads no observer state at all —when/where/why/howon a value operand return constants, because observer state is binding-keyed and a bare value has no binding.obs:DIAGis a fourth marker, and a deviation from the three the issue named.OP_LOOP_CAP_CHECKis emitted at every plain loop and its only reach into observer state iseigs_observe_safepoint's SIGUSR1 dump.READSwould pin bookkeeping on for every loop in every program and delete the entire win (mechanical-gates §9 — an unbounded closure marks every loop a reader);NONEwould silently drop the obligation that the dump must say the gate is closed rather than render every binding asequilibrium(§11).DIAGrecords that waiver as a marker the elision can enumerate rather than as prose nobody re-reads.The gate
tools/obs_marker_check.sh, suite section[99t]. Seven assertions, count pinned:obs:READis a typo, not a decision)OP_COUNTexemption still fires — present, last, and unmarked--readsemits the READS set, so the future liveness scan reads the markers rather than keeping a second copy of them.Validation
Eleven mutations in
--selftest, each verified to be witnessed by exactly one fixture by neutering each check in a copy of the gate. That verification earned its keep twice:OP_COUNTis not last" branch and theEXPECTED_CHECKSpin both survived being neutered — advertised guards enforcing nothing. Both now have their own fixture (an opcode appended past the sentinel; a gate-level mutation that deletes a whole assertion).cd "$(dirname "$0")/.."left them unable to see any header and every fixture failed identically. An unstartable mutant is not a caught one — the shipped version passes an absoluteVM_HEADERfor exactly that reason.Also fixes a matcher this change falsified in a neighbouring gate:
vm_operand_width_check.sh's selftest anchored onOP_INTERROGATE,[space]*/*, which the new marker now sits between. It failed loudly ("could not remove the comment") rather than passing vacuously, which is the only reason it was caught.Gates
detect_leaks=14059/4059, 0 failed, leak tally 0src/vm.hverified as markers-only: stripping the 94 markers reproducesHEADbyte-for-byteWhat this does NOT do
OP_CALL/OP_IMPORT/OP_DISPATCH(callee scanned on its own way in),OP_JUMP_BACK's JIT OSR entry (the JIT has its own reader,jit_helper_report_slot), and assembled chunks (vm_run_bytecode/sandbox_run, needing a bytecode twin —chunk_arm_temporalis the precedent) are excluded by name, each with the mechanism that covers it instead.Refs #972 — does not close it. Still open there: the elision itself, its measured before/after against #915's ceiling, and the three bypass routes above.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Aj9b82JBb8WS3b8ExoV5Yt