Repository navigation
Refuse an uninterpretable admit_callers entry at the entry, instead of silently emptying the roster - #8962
Conversation
…empty the roster
An admit_callers entry that is not an interpretable decl_ref(...) call was DROPPED
during collection: N authored entries became fewer than N, and when every entry was
uninterpretable, ZERO. fn_admit_callers then answered Present-and-empty -- present,
and empty -- and the admission wall refused every caller with a diagnostic naming the
CALL SITE and the CALLEE while never mentioning the roster expression, which was the
only thing wrong in the file. An author hits that and goes looking at a site with
nothing wrong with it.
DESIGN names the class: bottom-as-answer conflated with bottom-as-ignorance, the
empty-observation narrow. "I could not interpret any entry" was rendered as "there
are zero permitted callers". It fails closed by accident -- an empty roster refuses
everyone -- while being just as uncomputed as the widen DESIGN already forbids.
WHY THE PARSER GUARD DID NOT COVER IT, which is what made the trap credible: parse
refuses a syntactically empty list with "admit_callers requires at least one decl_ref
caller", so an author meeting that guard reasonably concludes an empty roster is
unrepresentable. It is not. The guard counts list CHILDREN before interpretation and
cannot see a list that EMPTIES during collection. A guard that is wrong about its own
subject is worse than an absent one.
THE REPAIR IS CONSTRUCTION, NOT VALIDATION. Collection now maps every authored entry
to a total per-entry outcome, so the result length equals the authored length and a
shrunk roster has no spelling at that seam. A post-collection non-empty check would
have been strictly weaker AND wrong on the merits: three entries with one malformed
still collect to a nonempty list and pass any threshold while silently dropping a
caller the author explicitly admitted. Only a per-entry refusal reaches that case.
MEASURED, gen-2, all eleven arms in one dispatch:
h (a DEFINED String in the roster -- no undefined name, no type error, the entry
being a non-decl_ref is the file's ONLY defect) flipped from
"permitted callers: []" to "admit_callers entry on 'probe_seal'"
e (well-formed roster admitting its real caller) ACCEPTED, unchanged
a, b, d, i (uses-clause arms, unrelated subject) ACCEPTED, unchanged
c (unresolved type) REFUSED, unchanged
The unchanged arms are the load-bearing half: a gen-2 compiler that refused
everything would flip h identically, and a control whose subject is unrelated to the
change cannot be satisfied by the change succeeding or by it failing loudly.
WHAT THIS DOES NOT CLOSE, declared rather than left for a reviewer. A WELL-FORMED
decl_ref naming a module that does not exist is interpretable, survives collection
untouched, and can never match any caller's coordinates -- so the effective roster
still differs from the authored roster by a second route. Measured here: arm k
refuses while LISTING that entry (roster nonempty, entry dead), and arm j compiles
with nothing anywhere naming the missing module. Nothing in this diff changes either.
Closing it requires a declared decision -- resolve admission targets against the
module graph, or state that they are deliberately unresolved coordinates because a
roster may legitimately name a caller outside the current compile's closure. The
witness carries an arm asserting this route is UNCHANGED, as a receipt, not a claim
that the class is closed.
NOT ADDED, deliberately: a diagnostic distinguishing "roster emptied by dropping"
from "roster intact but inert". Pre-repair those two states are indistinguishable to
an author (k refuses naming one entry, h refuses naming zero, same message). Under
per-entry refusal the zero-entry state is unwritable, so a message separating them
would restate what construction already carries.
The mirrors are the bytes from the dispatch that ran the arms above, transported and
verified by sha256 in both directions. v1_compiler_emit_rust.rs is deliberately NOT
included: it drifts on main independently of this change and is gunbc#8953's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b
The failing check is the inherited main drift, and this PR's own mirrors are cleanRegen reached the comparison and reported it: Pre-registered before this run started, both outcomes named in advance:
If the drift had named either of my two mirrors, that would have been mine and a finding in its own right: it would mean a change to the compiler's diagnostic surface perturbs its own emitted mirror, which is the same shape as the drift #8953 repairs. How pre-registration 2 is established, stated preciselyThe log names only divergent files, never the full compared population — so "my files are absent from the drift list" is by itself an argument from silence, which is not evidence. The positive form is two steps:
So the comparison covered them and did not report them as drifted. That is an inference over a confirmed non-empty population, not a missing-negative read — and it is weaker than a log line naming them, which is why it is written out rather than asserted. Also from this runThe witness floor passed: Not rebasing, not re-running, not regenerating that mirror. This clears when #8953 lands. — sent from quiet-boar-696 |
The defect
An
admit_callersentry that is not an interpretabledecl_ref(...)call was dropped during collection. N authored entries became fewer than N — and when every entry was uninterpretable, zero.fn_admit_callersthen answeredPresent-and-empty, and the admission wall refused every caller with a diagnostic naming the call site and the callee, never mentioning the roster expression that was the only thing wrong in the file.An author hits that and goes looking at a site with nothing wrong with it. A typed, located diagnostic pointing at the wrong location is not a lesser version of a good one — it actively spends the reader's time in the wrong file.
DESIGN names the class: the empty-observation narrow — ⊥-as-answer conflated with ⊥-as-ignorance. "I could not interpret any entry" was rendered as "there are zero permitted callers". It fails closed by accident (an empty roster refuses everyone) while being exactly as uncomputed as the widen DESIGN already forbids.
Why the existing guard did not cover it
Parse refuses a syntactically empty list with
admit_callers requires at least one decl_ref caller. An author who meets that guard reasonably concludes an empty roster is unrepresentable. It is not — the guard counts list children before interpretation, and nothing re-checked after collection had dropped all of them. Non-empty in, empty out, no diagnostic. A guard that is wrong about its own subject is worse than an absent one, for the same reason an inert lens is worse than no lens.The repair is construction, not validation
Collection now maps every authored entry to a total per-entry outcome, so the result length equals the authored length and a shrunk roster has no spelling at that seam.
A post-collection non-empty check would have been strictly weaker and wrong on the merits: three entries with one malformed still collect to a nonempty list, pass any threshold, and silently drop a caller the author explicitly admitted. Only a per-entry refusal reaches that case — so the partial arm in the witness is not a bonus test, it is the arm a count check would fail.
Measured — gen-2, eleven arms, one dispatch
h— a definedStringin the rosterpermitted callers: []admit_callers entry on 'probe_seal'e— well-formed roster admitting its callera,b,d,i— uses-clause, unrelated subjectc— unresolved typehis the attributing arm: no undefined name for resolution to reject, no type error for inference to raise — the entry being a non-decl_refis the file's only defect.The unchanged arms are the load-bearing half. A gen-2 compiler that refused everything would flip
hidentically. A control whose subject is unrelated to the change cannot be satisfied by the change succeeding or by it failing loudly.What this does NOT close — declared, not left for review
A well-formed
decl_refnaming a nonexistent module is interpretable, survives collection untouched, and can never match any caller's coordinates. The effective roster still differs from the authored roster by a second route. Measured: armkrefuses while listing that entry (roster nonempty, entry dead); armjcompiles with nothing anywhere naming the missing module.Nothing here changes either. Closing it needs a declared decision — resolve admission targets against the module graph, or state they are deliberately unresolved coordinates because a roster may legitimately name a caller outside the current compile's closure. I lean to the second being defensible, and it turns that route from a defect into a boundary. The witness carries an arm asserting this route is unchanged, as a receipt — not a claim the class is closed.
Deliberately not added
A diagnostic distinguishing "roster emptied by dropping" from "roster intact but inert". Pre-repair those are indistinguishable to an author (
krefuses naming one entry,hnaming zero, same message). Under per-entry refusal the zero-entry state is unwritable, so a message separating them would restate what construction already carries.Mirrors
v1_std_core.rsandv1_compiler_infer.rsare the bytes from the same dispatch that ran the arms above — transported and sha256-verified in both directions, so the receipt attaches to the artifact rather than to a later re-emit.v1_compiler_emit_rust.rsis deliberately excluded: it drifts on main independently of this change and belongs to #8953. Files were installed by name, never "everything that differed".🤖 Generated with Claude Code
https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b