ADR-0099 · Constructors are type-namespaced — bare names resolve when unambiguous in scope
- Status: Accepted
- Summary: A data constructor's true identity is
(dataName, ctorName), notctorNamealone (issue #108, operator-ruled option (a) 2026-07-11 — the ML/Rust convention).env.ctorschanges from a flatList (String × CtorInfo)keyed on the bare name to a table that PERMITS multiple entries under one bare name, one per owning type — registration-timeduplicate constructorrefusal is DELETED for the cross-type case (same-type duplicates, e.g. twoNils in onedatadecl, still refuse — that collision is never resolvable). Resolution moves from registration time to USE time: a bare ctor reference (Nil, a patternCons(h,t)) resolves when EXACTLY ONE in-scopedatatype owns that name; two or more owners is a LOUDB012error naming every candidate type and its qualified spelling. The qualified fallback follows the ADR-0093Mod_Typehand-qualification precedent exactly:Type_Ctor(IntList_Nil,List_Cons) — an ordinary identifier, parseable today with ZERO grammar change (ctor intro is already bare application per ADR-0069 decision 4; a pattern name is already a bareString). Migration is mechanical and LOCAL: the only corpus break is same-program cross-type ctor-name collision (empirically:Cons/Nilshared betweenIntListand any injected/userList/List a— census below), fixed by qualifying the ambiguous occurrences, never a program-wide rewrite. This unblocks #105's List-family prelude entries:Prelude.bangcan now declaredata List a = Nil | Cons(a, List a)without breaking any corpus program that separately declares its own list-shaped type, PROVIDED that program's own bareNil/Consuses stay unambiguous (only fires if the program actually MENTIONS a colliding bare name — ADR-0098'sinjectPreludemention-filter means a program that never usesNil/Consnever even sees the preludeListin itsenv.ctors, so most of the corpus is untouched twice over). - Resolves: issue #108 (design probe; implementation is a follow-up slice, not this ADR)
- Depends-on: 0069 (ctor elaboration +
CtorInfo/buildEnv— the exact site restructured), 0079 (generic data —CtorInfo.dataName/monomorphization untouched, resolution is orthogonal to mono vs generic), 0093 (theMod_Type/qualifyNamehand-qualification convention this ADR's qualified form is a direct extension of), 0098 (thePrelude.bangmention-filter that makes the common case pay nothing) - Relates-to: #105 (the blocked List-family prelude entries this unblocks), #97/ADR-0097 §6
(the derive handler — verified namespacing-INDEPENDENT, §6 below),
docs/notes/ stdlib-prelude-survey.md§1 (the universal-prelude functions gated on this landing)
Status
Accepted (2026-07-11) — implementation LANDED on main (merge of feat-ctor-namespacing @ ec6c7f35): resolveCtor + B012 + the Type_Ctor qualified form; all 4 witnesses flipped as designed (w2's B012 message byte-identical to this ADR's prediction); zero corpus regression, confirming the census claim. Formerly Proposed (2026-07-11) — a design probe for #108, not an implementation. Every claim below is
either machine-checked against the real bang binary (witnesses in docs/decisions/ witness-0099/, built from a clean lake build bang on design-ctor-namespacing @ dd60564f) or
explicitly marked as an implementation-time decision this ADR pins but does not build.
- Layer: F (frontend — parser is UNTOUCHED, only the elaborator:
Bang/Frontend/ TypeCheck.lean'sCtorInfo/ElabEnv.ctors/buildEnv/elabS/elabArms/matchDarms). Kernel untouched by construction (invariant #5) — namespacing is a RESOLUTION question, entirely pre-kernel; the elaboratedSurfa resolved ctor produces (ctorIntro/genCtorIntro,TypeCheck.lean:1870-1883) is byte-identical to today's for every UNAMBIGUOUS reference.
Context
ElabEnv.ctors : List (String × CtorInfo) (TypeCheck.lean:1712) is a single flat association
list keyed on the BARE constructor name, populated by buildEnv's .dataD arms
(TypeCheck.lean:2869-2892), both of which throw "duplicate constructor '{c}'"
(TypeCheck.lean:2878, 2889) the moment a SECOND data decl anywhere in the program declares a
ctor name already present — regardless of whether the two decls' bare uses ever actually collide
in the program body. This is genuinely global scope, confirmed live (see §Ground w0): a program
declaring both data List a = Nil | Cons(a, List a) and data IntList = Nil | Cons(Int, IntList)
back-to-back, with NO use of either type's ctors anywhere in the body, still fails to elaborate.
#105's first-slice lane hit this empirically trying to inject a free data List a = Nil | Cons(a, List a) into the prelude: it broke 4 pre-existing corpus #guards, all sharing the same
root cause — a corpus program declaring its own IntList/List (monomorphic) whose bare Nil/
Cons collide with the newly-omnipresent prelude List a's. Operator ruled option (a) — the ML/
Rust convention: a ctor belongs to its type; a bare name resolves when unambiguous. This ADR pins
the resolution rules, the qualified fallback, the migration story, and the diagnostics; #109-style
implementation is a follow-up.
Decision
1 — Resolution rules
A constructor's true key is (dataName, ctorName). env.ctors changes shape from
List (String × CtorInfo) to a structure that supports MULTIPLE CtorInfos under one bare name
— the minimal change is List (String × CtorInfo) UNCHANGED AS A REPRESENTATION (still a flat
assoc list of (name, info) pairs) but the DUPLICATE-KEY REFUSAL at registration
(TypeCheck.lean:2878/2889) is DELETED for the cross-type case; only a genuine same-type
duplicate (two ctors named Nil inside the SAME data decl's own cs list) still refuses — that
collision has no bare-name resolution story even post-#108 (which ctor would Nil even mean
within one type?), so it stays a decl-time error, unchanged. Every OTHER env.ctors.lookup
call site (elabS's .var/.app ctor-intro arms, matchD's arm-table lookups, elabArms's
payload-typing) changes from .lookup (first-match-wins on a flat list, silently picking
whichever data decl came first) to a NEW resolveCtor env x : Except String CtorInfo that:
- Filters
env.ctorsto every entry whose bare name equalsx— call thiscandidates. candidates = []→ not a ctor at all (today'snonecase — falls through to ordinary variable/application handling, UNCHANGED).candidates = [ci]→ resolves toci, UNCHANGED elaborated output (ctorIntro/genCtorIntroare pure functions of the ONE resolvedCtorInfo— nothing downstream of resolution needs to change).candidates.length > 1→ AMBIGUOUS,B012(§4).
The ambiguity set is every data decl's ctors currently reachable in env.ctors — which,
by construction, is exactly "every data decl this program's buildEnv walk has processed so
far" (ADR-0069's forward-reference-only decl order, TypeCheck.lean:2845-2846) UNION the
mention-filtered prelude (injectPrelude, ADR-0098) UNION any use-hoisted imported ctors
(ADR-0093 D2's "ctors travel with their type" — already unqualified in env.ctors by the time
buildEnv runs, since mergeModules resolves cross-FILE qualification before buildEnv ever
sees the merged decl list). This is a single, uniform set — v1 does NOT distinguish "prelude
ctor" from "user ctor" from "imported ctor" for ambiguity purposes; whichever combination of
data decls is in scope at the reference site is what counts. This is deliberately the SIMPLEST
rule available (flat set membership, no priority tiers) — §2 covers the one place a priority
tier is ALREADY established (shadowing) and why it does not need to interact with ambiguity.
Scrutinee-type-directed narrowing (fork (b) in #108's issue text) is NOT adopted as the primary
mechanism, but composes as free ADDITIONAL disambiguation where it already exists. matchD's
existing arm-table derivation (genBinderTable, TypeCheck.lean:2731-2733) already looks up the
FIRST arm's ctor name to seed ci0 before the ambiguity question is even asked — so resolveCtor
naturally runs INSIDE that existing type-directed context for match arms specifically (§1a). For
ctor INTRODUCTION (Cons(7, Nil) in expression position), there is no scrutinee to direct
resolution — check-mode ascription (: List Int) is a SEPARATE, ADR-0079-precedented mechanism
(annotation-driven generic introduction) that narrows WHICH monomorphization of an already-
resolved ctor to build, not WHICH data type owns the bare name; conflating the two would mean a
single resolution function doing double duty for unrelated questions. Recommendation: keep them
separate — resolveCtor answers "which type does this bare name belong to" using ONLY the
static ambiguity-set membership rule above; annotation-driven monomorphization (ADR-0079) runs
strictly AFTER, unchanged, once a single CtorInfo is in hand.
1a — Where resolution lives. The issue text asks "parser has no type info → elaborator" —
confirmed: pCtor/pCtors (Surface.lean:1929-1944) parse a ctor as a bare String with zero
semantic lookup (the parser doesn't even have env.ctors in scope structurally). Every site listed
above (elabS's .var/.app arms, matchD's two env.ctors.lookup c0 call sites plus the
per-arm-ordering env.ctors.find?, elabArms's payload-typing lookup) lives in elabS/
elabArms/buildEnv — all elaborator-side, confirming the issue's own framing. No parser change.
2 — Shadowing / priority
Bare-name AMBIGUITY and SHADOWING are different questions, already answered by different,
NON-INTERACTING mechanisms — this ADR does not need to invent a priority rule. Shadowing (which
of two SAME-spelled things is "the" one when one hides the other) already has an answer for the
type-NAME case: strPrelude/genericPrelude's "filtered out... when the user redeclares it"
(TypeCheck.lean:3786, declared.contains n) — verified live (§Ground w-shadow): a user's own
data Option a = MyNone | MySome(a) completely REPLACES the built-in Option (different ctor
names entirely; the built-in Option's None/Some are never even registered), so there is
NOTHING left to be ambiguous with — the built-in decl is filtered OUT of the decl list before
buildEnv ever runs. This is a same-TYPE-NAME shadow, orthogonal to #108's same-CTOR-NAME-
different-TYPE ambiguity — #105's actual break (IntList's Nil vs List a's Nil) is TWO
DISTINCT type names both surviving into scope, each contributing a Nil. Shadowing's filter
never fires here (the type names differ), so the question is purely "do two DIFFERENT survivors
share a ctor name" — exactly §1's ambiguity-set membership question, no priority tier needed.
A user's own data decl never automatically "wins" a ctor-name collision against a same-scope
prelude data — both are just members of the ambiguity set; a colliding bare use is ambiguous
regardless of which one is "the prelude's" vs "the user's". This is the CORRECT default (silently
picking the user's decl would be exactly the "guess, don't error" move Source.eval's discipline
forbids, ADR-0046) and it is what the migration story (§3) actually needs — a collision always
gets a LOUD, named, fixable error, never a silent semantic change depending on decl order.
Does the Prelude.bang "user wins" contract extend per-ctor? No — and this is not a
contradiction. ADR-0098 D4's "user wins" is mergeModules' EXISTING alias-shadowing for BARE
NAMES the user explicitly redeclares under the SAME spelling (let abs = … shadows the
auto-used prelude abs, one name, one binder, innermost wins — an ordinary lexical-scoping
fact, not a namespacing decision). A ctor-name COLLISION between two DIFFERENTLY-NAMED types
(IntList vs List) is not that shape at all — there is no "redeclaration" (the user did not
write a second data List a; a data IntList is a wholly different decl), so there is nothing
for D4's shadow rule to apply TO. The ctor-namespacing ambiguity is a NEW kind of collision D4
never covered, and correctly gets a NEW answer (ambiguity error, not silent precedence).
3 — Migration
The falsifier, run against the real binary (§Ground): does any TODAY-passing corpus program
rely on a cross-type bare-ctor PUN (the same bare name meaning DIFFERENT things in different
scopes of ONE program, exploiting the fact that today's flat table's first-match-wins semantics
picks a specific one)? Answer: no such program exists in the corpus — today's registration-
time duplicate constructor refusal (unconditional, decl-time, TypeCheck.lean:2878) makes this
STRUCTURALLY IMPOSSIBLE: two data decls sharing a ctor name in ONE program is ALREADY a hard
compile error today, with ZERO exceptions (confirmed: greping the corpus for any TWO co-present
data decls sharing a ctor name found none — IntList's Nil/Cons and List a's Nil/Cons,
the ONLY same-name pair in the whole 45k-line corpus, never appear in the SAME #guard's source
string; each List/List a test is its own standalone program). Consequence: #108 cannot
possibly break an EXISTING corpus program — every corpus program that compiles today has, by
construction, no cross-type ctor collision to become ambiguous. The only place #108 introduces new
possible failures is a genuinely NEW combination not previously expressible: a program that
declares/imports/uses TWO SEPARATE colliding types AT ONCE — which is exactly and ONLY what #105's
prelude injection does (adding a globally-present List a next to a program's own IntList/
List). The migration is therefore not "fix N broken corpus programs" (none break) but "the
4 #105-blocked entries become expressible, with the qualified fallback as their fix" — verified
by re-running the exact 3 corpus source-strings the #105 finding named (§Ground, "the exact break"
below): each fails IDENTICALLY today whether or not #108 has landed (both fail at the SAME
registration-time wall, confirmed live) and each is fixed the SAME mechanical way — replace the
colliding bare Nil/Cons in the AFFECTED program's own body with IntList_Nil/IntList_Cons
(or, if #105 additionally reorders so IntList's decl comes textually after List's, no source
change at all is needed for programs that already used a NON-colliding local type like L/
LNil/LCons, confirmed w3). No corpus-wide rewrite; the fix is strictly LOCAL to whichever
individual program's bare ctor becomes ambiguous once the prelude's List a is added to its
scope — and by ADR-0098's mention-filter, that scope-addition itself only happens for programs
that mention a List-family name in the first place (a program using neither Nil nor Cons
nor any other List name never sees the prelude List a merged in at all, so it can never become
ambiguous by mere prelude presence — verified structurally, injectPrelude's progUsesVar walk).
4 — Diagnostics
New code B012 (next free — the registry's highest is B011, DiagCodes.lean:142),
positioned BEFORE B006's broad "constructor '" catch-all (same specificity-ordering discipline
the registry already documents for B007/B011 vs B006, DiagCodes.lean:150-152 — an ambiguous-ctor
message necessarily also contains the substring "constructor '", so it must be listed earlier or
B006 silently swallows it). Anchor: ["ambiguous constructor"]. Message shape, matching the
existing s!"..." interpolation convention and the "error TEACHES the fix" discipline (ADR-0093
D2/D3's precedent):
error: ambiguous constructor 'Nil' — candidates: IntList (as 'IntList_Nil'), List (as 'List_Nil')
listing every candidate type name PAIRED with its qualified spelling (not just the type names) so
the message is directly actionable without a second lookup — the same "the error names the fix"
shape firstPrivateUse's private-decl diagnostic already established (ADR-0093 D3,
TypeCheck.lean:3263-3268, "names BOTH the module and the specific private name"). bang explain B012 teaching text: ambiguity is a scope fact (two co-present data types share a ctor name),
resolved by writing the type-qualified form Type_Ctor.
5 — The acceptance spike
Hand-simulated + machine-checked against the real binary (witnesses in docs/decisions/ witness-0099/, run via bang run on design-ctor-namespacing @ dd60564f, .lake/build/bin/ bang, lake build bang → Build completed successfully (1448 jobs)):
| witness | shape | TODAY's result (do-not-regress baseline) | POST-#108 target |
|---|---|---|---|
w0-baseline-duplicate-today.bang | data List a+data IntList co-present, ctors UNUSED | error: duplicate constructor 'Nil' | types + runs (0) — nothing is ambiguous, no bare use exists |
w1-coexist-qualified.bang | same co-presence, IntList's ctors used via qualified IntList_Nil/IntList_Cons | same duplicate constructor wall (registration never reaches use-time today) | types + runs, returns 7 — the #105/#108 acceptance case: prelude List + user IntList coexist, IntList matched correctly via its qualified ctors |
w2-ambiguous-bare-error.bang | same co-presence, BARE Nil referenced | same duplicate constructor wall | error[B012]: ambiguous constructor 'Nil' — candidates: IntList (as 'IntList_Nil'), List (as 'List_Nil') — loud, not silent |
w3-unambiguous-bare-still-works.bang | data L(LNil/LCons, no collision) + data IntList, BARE Nil/Cons used | passes today (7) — no collision exists | UNCHANGED (7) — the non-colliding common case is untouched |
w0–w3 machine-confirmed at the stated "TODAY" column (all four run identically against the
real binary this session — the first three at the identical duplicate constructor 'Nil'
registration wall, confirming they are ONE root cause, not three; w3 passes today, establishing
the non-regression floor). The "POST-#108 target" column is the falsifiable acceptance criterion
the implementation slice must satisfy — not yet built (this ADR is a design probe).
6 — Sequencing vs the derive handler (ADR-0097)
ADR-0097 §6 already analyzed this exact question and concluded the derive handler (issue #109) has
no ordering dependency on #108's implementation: a derive's emitted match arms name ONLY the
ctors of the ONE type being derived over, which is unambiguous by construction regardless of
whether OTHER co-present types share those ctor names. This ADR does not revise that
conclusion — §1's resolveCtor runs per-reference, and a derive-emitted reference is always
same-type-scoped, so it degrades gracefully whether #108 has landed or not, exactly as ADR-0097
predicted. Verified consistent: nothing in this ADR's resolution rule (§1) special-cases WHERE a
Surf node came from (hand-written vs derive-emitted) — resolution is purely a function of the
bare name + the ambiguity set, agnostic to provenance.
Rejected alternatives
- (b) Scrutinee-type-directed resolution ONLY, no type-namespacing — narrower than what #108
asks for (only disambiguates AT a
match, not at ctor INTRODUCTION —Cons(7, Nil)in expression position has no scrutinee to direct by), and doesn't dissolve the REGISTRATION-time wall at all (today's refusal fires before any use exists, §Context) — the operator's ruling already selected (a) over this; recorded here because §1 explicitly folds (b)'s mechanism in as a compositional aid for match arms specifically, not as the primary rule. - (c) Prelude ships under non-colliding names (
LNil/LCons) — permanently ugly (the operator's own framing in the issue), and doesn't fix the underlying disease for USER programs that hit the same collision with each other (two unrelated libraries both naming a variantSome/None-shaped type) — only patches the ONE instance #105 happened to surface. Rejected per the ruling; not reconsidered here. - (d) Wait for Prelude.bang's
use-selective import to make collisions user-managed —Prelude.bangLANDED (ADR-0098) auto-use, MENTION-FILTERED, which is exactly the mechanism that makes §3's migration story cheap (a program not mentioningList-family names never even merges the prelude'sList ain) — but auto-useis unconditional for MENTIONED names (no selective opt-out exists, and ADR-0098 explicitly declined to add one, "Revisit if" list) so a program that DOES mention a colliding name has no user-managed escape without #108's actual resolution rule. (d) is subsumed, not superseded — ADR-0098 already landed and its mention- filter is load-bearing for §3's "no corpus-wide cost" claim, but it alone does not dissolve ambiguity once a collision is actually mentioned. - A priority tier (user-declared ctors always beat prelude/imported ones on a bare collision) — rejected in §2: silently picking a winner by provenance is the exact "guess, don't error" move ADR-0046 forbids; it would also make a program's meaning depend on DECL ORDER or import order in a way the qualified fallback makes structurally unnecessary (the fix is always available, cheap, and named by the error itself).
Mod.Ctor(dot syntax) instead ofType_Ctor— rejected for the SAME reason ADR-0093 § "Mod_Type" rejected dot syntax for qualified types:.is the live.dotPerform/capability- call token (h.get,Foo.op), soList.Cons(...)would either need new grammar disambiguation or collide with the existing qualified-call parse;Type_Ctorneeds ZERO grammar change (ctor intro is bare application, ADR-0069 decision 4; a match-arm ctor name is already a bareString) and is the DIRECT extension of theMod_Typeconvention already documented indocs/reference/language.md— one qualification scheme for the whole language, not two.
Ground (witnesses run against the real binary, this session)
Built from a clean checkout (design-ctor-namespacing @ dd60564f, .lake/build reflink-seeded
from the main checkout per tools/seed-lake.sh's own logic — this is a standalone clone, not a
linked worktree, so the script's auto-detected main_root did not apply; the seed was done by
hand against the same source/target pair the script uses — lake build bang → Build completed successfully (1448 jobs), .lake/build/bin/bang).
| witness | result |
|---|---|
docs/decisions/witness-0099/w0-baseline-duplicate-today.bang | error: duplicate constructor 'Nil' — the registration-time wall, confirmed live |
docs/decisions/witness-0099/w1-coexist-qualified.bang | same wall (registration never reaches the qualified-use body) — confirms the wall is unconditional, not use-triggered |
docs/decisions/witness-0099/w2-ambiguous-bare-error.bang | same wall |
docs/decisions/witness-0099/w3-unambiguous-bare-still-works.bang | passes today, returns 7 — the non-colliding case, do-not-regress floor |
the exact IntList #guard source strings (listProg "let s = Cons(7, Nil) in match s { Nil -> 0, Cons(h, t) -> h }", TypeCheck.lean:4357) with data List a = Nil | Cons(a, List a) prepended | error: duplicate constructor 'Nil' — reproduces the #105 finding directly against the exact corpus source |
the listRec-shaped let rec sum source (TypeCheck.lean:4490) with the same prepend | error: duplicate type name 'List' — the ORTHOGONAL type-NAME collision (already solved today by strPrelude/genericPrelude's redeclare-filter mechanism, TypeCheck.lean:3786; NOT in #108's scope, confirmed by the Option-redeclare witness below) |
user data Option a = MyNone | MySome(a) (ctors renamed, type name reused) | bang run → 7, matched via MySome(x) -> x — confirms shadow-by-type-name-redeclaration already works, orthogonal to #108's ctor-collision-across-DIFFERENT-type-names problem |
data A = Foo(Int) + data B = Foo(Int) (no use, just co-presence) | error: duplicate constructor 'Foo' — a second, independently-confirmed instance of the registration-time wall, generalizing beyond the List/IntList pair |
ADR-0097 §6, Bang/Frontend/TypeCheck.lean:2854-2929 (buildEnv's ordering) | consulted, not re-verified this session (ADR-0097's own machine-checked finding, unrelated wall) |
Also consulted: Bang/Frontend/Surface.lean:1929-1963 (pCtor/pCtors/pDataParams — the
grammar, confirmed untouched by this design), Bang/Frontend/TypeCheck.lean:1645-1719 (CtorInfo/
ElabEnv field shapes), :2534-2843 (elabS/elabArms, every env.ctors.lookup/.find?/
.filter call site enumerated), :3013-3032 (strPrelude/genericPrelude, the type-name-
redeclare-filter precedent), :3045-3260 (qualifyName/qualifyDeclName/qualifyModule/
isPubName — the Mod_Type/ctorOwners cross-FILE qualification machinery this ADR's
Type_Ctor form is a same-scheme, different-axis extension of — module qualification disambiguates
by FILE, ADR-0099 disambiguates by TYPE, both mangle via name1_name2), docs/reference/ language.md:166-171 (the Mod_Type convention, the direct precedent), Bang/Frontend/ DiagCodes.lean (the B0xx registry + specificity-ordering discipline B012 follows).
Consequences
- #105's List-family prelude entries (
length/append/head/tail/take/drop/zip) can ship oncePrelude.bangdeclaresdata List a = Nil | Cons(a, List a)— this ADR's resolution rule is the ONLY blocker named in #108, and it is dissolved by construction for the common case (no collision) and by a named, local, mechanical fix for the colliding case. - A future user-authored library sharing a ctor name with another library (or the prelude) gets
the SAME treatment — ambiguity is a general property of the resolution rule, not a one-off patch
for
List/IntList. ElabEnv.ctors's REPRESENTATION does not need to change shape (stillList (String × CtorInfo)) — only the REFUSAL (deleted for cross-type dups) and every LOOKUP call site (replaced byresolveCtor's ambiguity-counting wrapper) change. This keeps the implementation slice small and localized toTypeCheck.lean, noSurface.lean/parser change, no kernel change.
Implementation slice map (sized, for the follow-up issue)
buildEnv's.dataDarms (TypeCheck.lean:2869-2892, both mono + generic): delete the cross-typeduplicate constructorthrow; KEEP the same-type-duplicate check (adatadecl's OWNcslist still can't repeat a ctor name — unrelated to #108, that collision has no resolution story). Small — twoifconditions narrowed from "any prior owner" to "an owner with the SAMEdataName".resolveCtor : ElabEnv → String → Except String CtorInfo(new function): the §1 filter + count logic. Small, self-contained, no existing function's signature changes.- Six call sites migrate from
env.ctors.lookup/.find?toresolveCtor:elabS's.vararm (:2537),.app (.var c) aarm (:2620),matchD's twoenv.ctors.lookup c0sites (:2727,:2740) plus the orderingenv.ctors.find?(:2762, this one is POST-resolution bookkeeping overdcs, a DIFFERENT list already filtered to onedataName— likely unchanged, needs a close read at implementation time, not a blind swap),elabArms's payload-typing lookup (:2809). Medium — six sites, each a small diff, but each needs its OWN#guard(positive: still resolves; negative:B012fires) per the "the corpus is the contract" discipline. Type_Ctorqualified-form PARSING: NONE needed —IntList_Nilparses today as an ordinary identifier (confirmed:pIdent/pCtorhave zero reserved-character handling for_,TypeCheck.lean:3043-3045's own comment onqualifyNamesays so explicitly for the module axis, and the same tokenizer fact holds for this axis). Zero parser-slice cost — the entire qualified form is FREE onceresolveCtor's bare-name lookup also checks for an EXACTdataName_ctorNamespelling as an equally-valid resolution path (aCtorInfo.dataName-prefixed literal match, not a NEW field —resolveCtortries the bare-name ambiguity-set filter first, then falls back to checking whether the reference IS already one candidate'sType_Ctorform).DiagCodes.leanregistry: addB012, ordered beforeB006(mirroring the existingB007/B011-before-B006precedent verbatim) + its#guard. Small, mechanical.- Regression
#guards: the four witnesses (§5) promoted fromdocs/decisions/witness-0099/intoTypeCheck.lean's corpus (or a newBang/Frontend/CtorNamespacingtest module, matching how other multi-witness features got their own test home) — expectations COMPUTED fromSource.eval, not guessed, per the corpus discipline. Small-medium (four cases, each already hand-verified against the real binary this session). Prelude.bang'sdata List adeclaration itself + the #105 List-family functions — OUT OF SCOPE for this slice (a separate #105 follow-up once #108 lands; this ADR only unblocks it).
No kernel-adjacent touch anywhere in this map — entirely Bang/Frontend/TypeCheck.lean +
Bang/Frontend/DiagCodes.lean, both fan-in-0 leaves.
Revisit if
- A future collision needs PRIORITY (not just qualification) — e.g. an ecosystem convention
emerges where "the innermost/most-local
datadecl should silently win" — this ADR's §2 explicitly rejects that as a default; revisit only with a concrete, named user need (none exists today, ADR-0098's own "no such need has surfaced" precedent for a similar ask).qualifyName/Type_Ctorbecomes onerous with theListfamily PLURALIZED further (deep type hierarchies with many small collisions) — ause-style selective ctor-import within ONE file (not just across module boundaries) could reduce qualification noise; no evidence this is needed yet (v1's collision census is exactly one pair,List/IntList). - #105's List-family prelude entries land and surface a SECOND real-world collision beyond
List/IntList(e.g. a user's ownResult-shaped type colliding with the preludeResult) — this ADR's rule already covers it identically (no type-name-specific logic anywhere in §1); no design revision expected, just confirms the rule generalizes.