Skip to content

Refuse an uninterpretable admit_callers entry at the entry, instead of silently emptying the roster - #8962

Merged
briansrls merged 2 commits into
mainfrom
session/quiet-boar-696-admit-roster
Aug 23, 2026
Merged

briansrls merged 2 commits into
mainfrom
session/quiet-boar-696-admit-roster

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

The defect

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, 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

arm before after
h — a defined String in the roster permitted callers: [] admit_callers entry on 'probe_seal'
e — well-formed roster admitting its caller ACCEPTED ACCEPTED
a,b,d,i — uses-clause, unrelated subject ACCEPTED ACCEPTED
c — unresolved type REFUSED REFUSED

h is the attributing arm: no undefined name for resolution to reject, no type error for inference to raise — the entry being a non-decl_ref is the file's only defect.

The unchanged arms are the load-bearing half. A gen-2 compiler that refused everything would flip h identically. 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_ref naming 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: arm k refuses while listing that entry (roster nonempty, entry dead); arm j compiles 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 (k refuses naming one entry, h 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.

Mirrors

v1_std_core.rs and v1_compiler_infer.rs are 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.rs is 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

…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
@gunbai-bot

gunbai-bot Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor Author

The failing check is the inherited main drift, and this PR's own mirrors are clean

Regen reached the comparison and reported it:

required-regen: first_generation_equal=false planned=132 executed=132 declared_divergent=1 [main.rs]
required-ci: regen FAIL generated surface drift: v1_compiler_emit_rust.rs

Pre-registered before this run started, both outcomes named in advance:

  1. It will drift on v1_compiler_emit_rust.rs, inherited from main — 1caf8d519f2 is an ancestor of this branch. Held. That file is not in this diff and belongs to Seed fixed point: install the second regen pass #8691 was owed #8953.
  2. This PR's own mirrors will match byte-identically — unlike a docs-only PR, this one ships v1_std_core.rs and v1_compiler_infer.rs. The reasoning was falsifiable: the change touches 00_core.dag and 04_infer.dag and not the emitter, so the function computing the output is the same function in both generations over the same input. Held — see the caveat below on how that is established.

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 precisely

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

  • the comparison ran and reported its population: planned=132 executed=132, so it is not a run that refused before comparing (the third regen outcome, RegenUnproduced, which decides nothing — checked for and absent here);
  • both files are members of that emitted population, established independently: my own verification dispatch copied them out of the candidate emit directory, which is the same emit this comparison performs.

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 run

The witness floor passed: planned=10578 executed=10578 terminal=10578. Only the regen phase failed. The line stops on any failed phase, so a green floor is invisible in the top-level check status — the third instance of that pattern today, and it is the phase-independence property holding rather than a quirk.

Not rebasing, not re-running, not regenerating that mirror. This clears when #8953 lands.

— sent from quiet-boar-696

@briansrls
briansrls merged commit ae31c7c into main Aug 23, 2026
1 of 2 checks passed
@briansrls
briansrls deleted the session/quiet-boar-696-admit-roster branch August 23, 2026 03:29
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