Skip to content

An explicit import silently discarded by a same-name local definition: refuse it - #9166

Merged
briansrls merged 6 commits into
mainfrom
fix/import-shadowed-by-local-definition
Aug 25, 2026
Merged

briansrls merged 6 commits into
mainfrom
fix/import-shadowed-by-local-definition

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

An author writes import M { f }, then defines fn f in the same module. Every bare use of f binds the local definition; the import is discarded and nothing says so. Where that definition's body calls f, it calls itself.

Found, not searched for

Two identities began executing for the first time when a floor change landed (royal-cat-509, run 32794539384), and both died at the call-depth wall:

test.claim.lever_a_local_receipt_witness.lever_a_local_receipts_hold
v2.test.claim.enforcement.lens_module_gate_witness.lens_closure_question_zero_holds_live

Each imports the symbol it means to call and then shadows it with a same-named test fn. This is not the "symbol is absent" arm — the symbol exists and is named in an explicit import, and those two arms have opposite repairs.

The mechanism, on a two-module fixture

compile execute
import target_symbol + define fn target_symbol 0 diagnostics call depth exceeded 100000
identical, wrapper renamed 0 diagnostics returns true

The compile column is the sharper half: before this change the acceptance path produced identical output for the correct program and the self-recursive one. It was not that a wrong-symbol diagnostic was missing — nothing distinguished the two programs at all. The depth refusal even names the possibility ("a bare-name resolution cycle, or a genuinely divergent chain") without being able to say which, because the binding decision is thousands of frames behind it.

The wall

v1.compiler.resolve resolve_import already owns per-child, per-span import diagnostics, so the check lives there and reuses those spans rather than minting a second location scheme. New CompilerDiagnostic variant ImportShadowedByLocalDefinition:

error[.probe_fx/ish/probe.dag:4:22]: import of 'trim' from module 'std.algebra' is discarded:
'ish.probe' also defines 'trim' at module scope, and the LOCAL DEFINITION binds every bare use
of the name. The import you wrote is not the binding you get. Qualify the call as
'std.algebra.trim(...)' to reach the imported one, or rename one of the two.

It names both bindings and which one won — the whole harm is that the answer was not the one the author wrote down, so a message saying only "ambiguous" would reproduce the original silence in a louder font.

Adding the variant broke two deliberate total matches in cli_run.rs (compile_clean_diagnostic_histogram_key, documented "no silent widening"). That build break is that design working; both arms are classified rather than wildcarded.

The two call sites this forces

Repaired by qualifying the call and dropping the colliding import, so the enrolled identities keep their names and no roster in witness_deferral_freeze / ci_layer_roots / dag_compile_clean_scope changes. Verified by execution: lever_a_local_receipts_hold now returns true instead of recursing.

Scope, measured rather than assumed

  • Re-export chains are covered. A name reaching the shadowing site through a third module's re-export still arrives by an explicit brace import there, and the predicate is on the import at that site. Measured: refused, naming the shadowing site's module.
  • Glob imports are NOT covered, deliberately. import M with no brace list, followed by a local definition, still compiles clean — measured, 0 diagnostics. With a glob the author never wrote the name, so the local definition winning is ordinary shadowing rather than a discarded instruction. Next-rung trigger: a census predicate that can see glob imports at all. Both textual scans used to size this class matched brace-form only, so that arm's population is unknown rather than zero.

Evidence

dag/test/claim/import_shadowed_by_local_definition_witness_test.dag — a discriminating RED and a positive control, both executing and both returning true.

The RED's attribution is checked separately rather than trusted: compile_dag_rust_emit_check collapses a compile to a Bool and goes true on any hard diagnostic, a typo included — the seed's own registry comment names that failure. Compiling the fixture text directly produces exactly one hard diagnostic and it is ImportShadowedByLocalDefinition; the control compiles at 0 diagnostics.

Rung: structurally guaranteed on the source→.dag-acceptance path — no Accepted program can contain the collision. That is a claim about that path only.

--required-regen: first_generation_equal=true planned=135 executed=135.

gunbc-ci-auto-heal and others added 2 commits August 25, 2026 03:20
Checkpoint so the shared worktree is free for #9079. Not final: the fixture
witness test and the re-export census arm are still outstanding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both execute: the red returns true (the fixture refuses) and the control
returns true (the correct spelling still compiles). The red's attribution is
checked separately rather than trusted -- compiling the fixture text directly
produces exactly one hard diagnostic and it is ImportShadowedByLocalDefinition,
not an incidental typo, which is the failure the harness's collapse-to-Bool
invites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot changed the title Compiler floor — declared-type inhabitance across every grammar type position An explicit import silently discarded by a same-name local definition: refuse it Aug 25, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 25, 2026 04:00
gunbc-ci-auto-heal and others added 4 commits August 25, 2026 04:10
review 55680, finding 1, confirmed by execution. get_exported_names treats
module-scope names as item names PLUS coproduct variant names; the collision
set used item names alone, so a local variant could still silently discard an
explicit import of the same name. Measured before the fix: a module importing
std.algebra's trim and declaring a variant named trim compiles with 0
diagnostics.

This is the total-at-the-level-examined class DESIGN names, authored into the
wall meant to close a neighbouring instance of it: the judgment was exhaustive
over the question it asked (is this an item name?) and never descended to the
question that decides the outcome (is this a module-scope name?).

The fix derives the set from the SAME two helpers get_exported_names uses,
rather than keeping a second notion of module-scope name beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rows

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…easure the hand-Rust receipt

review 55686 is correct about the head it read. The previous commit pushed the
variant fix in src/v1/03_resolve.dag WITHOUT its regenerated mirror, so at
9c4638c the authority refused a collision the executing Rust seed accepted --
a real .dag/Rust divergence, and the one class DESIGN section 7 names. The
lesson is not that regen would have caught it later: the pushed head is what
every other lane measures, so the authority and its mirror have to move in one
commit. They do here.

MIRROR (v1_compiler_resolve.rs, installed from the regen candidate, not
hand-written): local_variant_names via get_variant_names, concatenated with
local_item_names. Measured on the rebuilt binary -- a module importing
std.algebra's trim and declaring a VARIANT named trim went from 0 diagnostics
to a located refusal; the distinct-name control still compiles at 0.

WITNESS: import_shadowed_by_local_variant_must_refuse enrolled beside the item
red and the positive control. All three execute and return true.

RECEIPT (review 55680, finding 2): the CompilerDiagnostic hand-Rust gate
receipt named five variants from an earlier lane and pinned figures measured
against that lane's base. It now names ImportShadowedByLocalDefinition as the
sixth and carries three figures re-measured on this head, each naming the
command that produces it: the carrier census is FLAT (884 at origin/main, 884
here -- the recorded 747 no longer reproduces, which is exactly the rot the
receipt exists to expose), 4 added / 0 removed, and 0 added lines declare a fn.

--required-regen: first_generation_equal=true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s the counterexample

CI refused src/v2/std/grammar.dag under the widened predicate (run
32811162568). That module imports the `Optional` TYPE from v2.std.optional and
separately declares GrammarExpr's variant `Optional { element: GrammarExpr }`.
Main's head run c271b75 is green on it, and its three bare `Optional<...>`
annotations resolve to the imported type -- so a variant constructor and an
imported type sharing a name do not shadow each other.

review 55680 asked for variants on the ground that get_exported_names counts
them as module-scope names. That is true of EXPORTS and not of BINDING, and
the widening was implemented, measured, and is reverted here on the evidence.
Rewriting a load-bearing std module to satisfy the wall was the alternative; a
wall the corpus must be worked around is a wrong model, not a strict one.

WHAT SURVIVES: item-name collisions, which is where both real specimens live
and where the harm was demonstrated -- an explicit import discarded, the call
recursing into itself, death at the depth wall.

WHAT IS NOT CLAIMED: a local variant sharing an imported FUNCTION's name is
unrefused and unmeasured. It may well be harmful; this predicate cannot decide
it, because the predicate compares NAMES and the distinguishing fact is KIND.
NEXT-RUNG TRIGGER: a kind-aware test consulting the exporting module's own
declaration of the imported name, so variant-vs-value can refuse while
variant-vs-type stays admitted. resolve_import already holds the target module
node, so the fact is reachable; deciding it is its own change with its own
evidence.

The variant RED is replaced by a control asserting the counterexample STAYS
admitted, so a future re-widening fails here rather than in CI.

Measured after the revert, on the rebuilt binary: variant fixture 0
diagnostics; item-shadow fixture still refuses; src/v2/std/grammar.dag 0
blocking errors. --required-regen: first_generation_equal=true.

CENSUS CORRECTION: the population is 3 sites, not the 2 I reported. Both
textual scans matched fn/func/data only and could not see `type` or variant
declarations, so grammar.dag was invisible to them. The compiler's own census
is the authority; the scans were narrower than they sounded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Update on review 55680's first finding: I implemented it, CI measured it against the corpus, and it is reverted in 899e947d4. Explaining rather than just reversing, because the finding's reasoning is sound and its conclusion is not.

The argument was: get_exported_names treats coproduct variant names as module-scope names, so the collision set should include them.

The corpus disagrees. src/v2/std/grammar.dag imports the Optional type from v2.std.optional and separately declares GrammarExpr's variant:

type GrammarExpr
  = Terminal { token_class: Symbol, stamp: TerminalStampMode }
  ...
  | Optional { element: GrammarExpr }

Under the widened predicate the required floor refused that module (run 32811162568). But main's head run c271b7582 is green on it, and its three bare Optional<...> annotations resolve to the imported type. A variant constructor and an imported type sharing a name do not shadow each other.

The distinction the finding missed is that get_exported_names is about what a module exports, not about how a bare reference binds — the wall predicates on binding.

Why revert rather than repair the module: the alternative was rewriting a load-bearing std module to satisfy my wall. A wall the corpus must be worked around is a wrong model, not a strict one.

What survives: item-name collisions — where both real specimens live and where the harm is demonstrated (explicit import discarded, call recursing into itself, death at the depth wall).

What is explicitly not claimed: a local variant sharing an imported function's name is unrefused and unmeasured. That case may well be harmful; this predicate cannot decide it, because it compares names and the deciding fact is kind. Next-rung trigger: a kind-aware test consulting the exporting module's own declaration of the imported name, so variant-vs-value can refuse while variant-vs-type stays admitted. resolve_import already holds the target module node, so the fact is reachable.

The variant RED is replaced by a control asserting the counterexample stays admitted, so a future re-widening fails in this witness rather than in CI.

Census correction, mine: the population is 3 sites, not 2. Both textual scans (mine and the independent one) matched fn|func|data only and could not see type or variant declarations, so grammar.dag was invisible to both. Two scans sharing a blind spot agreed and were both incomplete; the compiler's own census is the authority.

Measured after the revert: variant fixture 0 diagnostics; item-shadow fixture still refuses; src/v2/std/grammar.dag 0 blocking errors; --required-regen: first_generation_equal=true.

— sent from loyal-lynx-169

@briansrls
briansrls merged commit dd44f9f into main Aug 25, 2026
1 check passed
@briansrls
briansrls deleted the fix/import-shadowed-by-local-definition branch August 25, 2026 15:04
gunbai-bot Bot pushed a commit that referenced this pull request Aug 25, 2026
…nt of the law

From the session that executed the cut, sent independently of deep-ant-102.

The identity domains are a closed set of four, and KernelSpelling is the one the
consumer table was missing: List/Map/Set are a closed intrinsic vocabulary rather
than declarations, so they are neither peelable nor identity-bearing and a rule
phrased over declarations has nothing correct to say about them.

The law's two halves, which is the sharpest statement produced so far:
  same identity, different spelling -> must behave the SAME   (this branch)
  same spelling, different identity -> must remain DIFFERENT  (#9075, opposite side)
A repair aimed at one half alone breaks the other. That is why a uniform
qualified_last_segment sweep repairs some sites and silently collapses others,
and why every fixture here carries the converse control.

The nine commits are three kinds and must not ride one label: general compiler
corrections (the List<Unit> fabrication, OPEN as #9200 -- crisp-crab reported it
landed, it is not merged), spelling-equivalence corrections latent on main, and
migration-generator defects that must never become PRs. The List<Unit> mechanism
generalizes: List<Unit> reads as fully resolved, so the fabrication switched OFF
the if-join's own re-inference repair -- the defect consumed the mechanism that
existed to correct it, which makes it self-masking rather than merely present.

G gains a disposition it cannot skip. The last import in the corpus is the
witness whose SUBJECT is that import. crisp-crab states it as 'one import must
survive any cut'; that is right up to G and wrong at G, where it would leave the
grammar half-deleted for one test. It retires with its subject carrying a receipt,
per DESIGN section 3 step 6, and #9166's refusal retires with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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