diff --git a/dag/extdeps/tools/hostname.dag b/dag/extdeps/tools/hostname.dag index fef724f9fed..946a55eaadc 100644 --- a/dag/extdeps/tools/hostname.dag +++ b/dag/extdeps/tools/hostname.dag @@ -1,6 +1,6 @@ module extdeps.tools.hostname -import std.types { String, List, Bool, Int, NonEmptyStr } +import std.types { String, List, Bool, Int, NonEmptyStr, Unit } import std.algebra { Empty } import v2.std.algebra { fold_list, list_append, list_snoc_item } import v2.std.collection { Map, empty_map, map_insert } diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index ca5798e2afb..7cc16179eb9 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -31081,6 +31081,22 @@ struct ExprVarClassification<'a> { /// reconciliation identity is over the same population as the tally; walking per-module with /// per-module counters against a shared tally would compare two different denominators. module: String, + /// THE VALUE-POSITION SUBSET OF THIS MODULE'S FREE REFERENCES, accumulated for the module + /// currently being walked and drained by the caller at its end. + /// + /// `bare` — the collector's other output — is deliberately a UNION over several question + /// kinds, because its consumer asks only "which modules must this scope reach": a type + /// annotation's `String`, a record literal's type name, a variant pattern's constructor and + /// an expression's callee all widen the closure equally, and over-approximating there is + /// harmless. + /// + /// It is NOT harmless as a refusal's population. A wall keyed on the union would refuse a + /// TYPE whose spelling two modules share, and would refuse it at a site where the evaluator's + /// shared value slot is never consulted at all. So the value positions — a free `ExprVar` and + /// a call's callee, the two forms the interpreter resolves through that slot — are collected + /// separately here rather than filtered back out of the union afterwards, which could only + /// ever be an approximation of the walk that already knows the answer. + value_refs: std::collections::BTreeSet, } impl ExprVarClassification<'_> { @@ -31304,12 +31320,27 @@ fn collect_node_refs_inner( .map(|(_, class)| *class); if classify.classify(&node.name, bound_as, chain_receiver) { bare.insert(node.name.clone()); + // A free `ExprVar` IS a value-position read: nothing binds it here, so + // the interpreter resolves it through the file's declarations, then the + // author's imports, then the shared slot. + classify.value_refs.insert(node.name.clone()); } } } ExprData::ExprCall { .. } => { if !node.name.is_empty() { bare.insert(node.name.clone()); + // The callee of a call is resolved through the same tiers as a free variable + // — `lookup_fn_from` — so it is a value-position read too, UNLESS an + // enclosing binder holds the spelling. `fn f(observe: fn(..) -> ..) { match + // observe(x) { .. } }` calls its own parameter, and `let observe = + // handler.observe` calls its own local; neither reaches the shared slot, and + // both were reported as ambiguous reads until the binder stack was consulted + // here as it already is for `ExprVar` one arm above. + let bound_as = bound.iter().rev().find(|(n, _)| n == &node.name); + if bound_as.is_none() { + classify.value_refs.insert(node.name.clone()); + } } } ExprData::ExprRecordLit { .. } => { @@ -39708,6 +39739,83 @@ impl ScopeBuildSplit { } } +/// ONE AMBIGUOUS BARE NAME AND THE DECLARATIONS THAT CLAIM IT, carried rather than counted. +/// +/// The count alone sizes the population; it cannot name a single site, so it cannot be acted on. +/// Layer 2 of this class is a REFUSAL landed together with the rename or qualification of every +/// site it would refuse, and that set is exactly this list — the count answers "how many", the +/// list answers "which", and only the second is a census a deletion can be planned from. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AmbiguousBareName { + pub name: String, + /// Every module OUTSIDE the authored region that declares this bare name, with the kind it + /// declares it as, sorted and deduped. The kind is carried because the two failure shapes + /// are not equally dangerous: a `data`/`fn` homonym crosses the evaluator's kind dispatch, + /// while `fn`/`fn` across two modules merely picks the wrong body. + pub claimants: Vec<(String, &'static str)>, +} + +impl AmbiguousBareName { + /// The distinct kinds claiming this name, sorted and joined with `+` — the grouping key the + /// floor's kind histogram folds over, derived from the claimants rather than authored beside + /// them. + pub fn kind_signature(&self) -> String { + let mut kinds: Vec<&'static str> = self.claimants.iter().map(|(_, k)| *k).collect(); + kinds.sort_unstable(); + kinds.dedup(); + kinds.join("+") + } +} + +/// The stable spelling of an item kind for the ambiguity census. `Debug` would do as bytes and +/// would silently re-spell every census line if a variant were renamed, so the projection is +/// written once here. +pub fn item_kind_census_label(kind: &crate::v1_compiler_infer_items::ItemKind) -> &'static str { + use crate::v1_compiler_infer_items::ItemKind; + match kind { + ItemKind::FnItem => "fn", + ItemKind::FuncItem => "func", + ItemKind::TypeItem => "type", + ItemKind::DataItem => "data", + ItemKind::ServiceItem => "service", + ItemKind::OtherItem => "other", + } +} + +/// ONE BARE-NAME REFERENCE SITE THAT RESOLVES THROUGH THE AMBIGUOUS SHARED SLOT. +/// +/// The declaration census ([`AmbiguousBareName`]) says which names two transitively-reached +/// modules both spell; it does NOT say that anything reads one. A name nothing references bare +/// harms nothing, and the corpus deliberately carries whole families of per-module convention +/// rows that every extdeps module declares — so a refusal keyed on the DECLARATION population +/// would refuse a convention rather than a defect. +/// +/// The refusal's real population is the READ. This row is one of them: a module in the scope +/// that references `name` in value position where `lookup_fn_from`'s own first two tiers — the +/// site file's module's own declaration, then the module that file explicitly imported the name +/// FROM — both decline, so the reference falls through to the shared slot and the slot holds one +/// of several declarations that nothing the author wrote ranks. +/// +/// COUNTED STATICALLY, over the scope's whole closure rather than over the lookups a fold +/// happens to EXECUTE. An execution-keyed census omits a reference on a path no witness runs, +/// which is precisely the site that would be refused later — or, if the wall were placed at +/// runtime lookup, never refused at all. +/// +/// THE GRAIN IS (name, referring module), NOT the occurrence. `refs_by_module` publishes a +/// module's free references as a deduped set, so three references to one name from one module +/// are one row here. That is the grain a rename is written at — a module either resolves the +/// name through the shared slot or it does not, and fixing it fixes every occurrence in it — but +/// it is not an occurrence count and must not be reported as one. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AmbiguousBareRead { + pub name: String, + /// The module whose body carries the reference(s) — where a qualification would be written, + /// or whose declaring side has to move. + pub referring_module: String, + /// The out-of-region declarations the shared slot is choosing between for this read. + pub claimants: Vec<(String, &'static str)>, +} + pub struct PreparedClaimScope { /// THE IMMUTABLE INTERPRETER INDEXES FOR THIS SCOPE, built ONCE here rather than once per /// claim. `InterpContext::with_runtime_options` walks every module and every item to build @@ -39746,7 +39854,16 @@ pub struct PreparedClaimScope { /// genuinely undecided residue: a bare reference to a name the referring module neither /// declares nor imports, and a name reached through a wildcard import, claimed by two or more /// modules it reached. - pub ambiguous_bare_names: usize, + pub ambiguous_bare_names: Vec, + /// THE READS, which is the population a refusal is affordable against — see + /// [`AmbiguousBareRead`]. A subset of `ambiguous_bare_names` by name, and the only one of + /// the two whose members are defects rather than declarations. + pub ambiguous_bare_reads: Vec, + /// THE POSITIVE HALF: a reference to an otherwise-ambiguous name that does NOT fall through, + /// with the module that answered it. A pair leaving `ambiguous_bare_reads` proves only that + /// the list moved; this says what the reference resolves TO, which is the whole content of a + /// qualification. `(name, referring module, resolved module)`. + pub qualified_bare_reads: Vec<(String, String, String)>, /// WHERE THE ~120ms OF ONE SCOPE CONSTRUCTION ACTUALLY GOES, split three ways at the /// grain the terminal correction has to choose between. The floor already reports what a /// scope COSTS in resident bytes (`[floor-scope-cost]`) and how many it built, and neither @@ -39810,7 +39927,14 @@ pub struct ReferenceClosureIndex { pub module_count: usize, pub decl_index: HashMap>, pub module_names: std::collections::HashSet, + /// EVERY name a module reaches, in any position — the closure question. Over-approximate by + /// design: see `ExprVarClassification::value_refs` for why that is right here and wrong as a + /// refusal's population. pub refs_by_module: HashMap>, + /// The VALUE-POSITION subset — free variables and call targets, the two forms the + /// interpreter resolves through the shared slot. This is the population the bare-name + /// ambiguity census and its wall are keyed on. + pub value_refs_by_module: HashMap>, } /// THE PREPARED SUBJECTS ONE FLOOR PROCESS BUILDS, BY DESIGN: the policy module's own closure @@ -40023,6 +40147,8 @@ fn reference_closure_index( let mut decl_index: HashMap> = HashMap::new(); let mut module_names: std::collections::HashSet = std::collections::HashSet::new(); let mut refs_by_module: HashMap> = HashMap::new(); + let mut value_refs_by_module: HashMap> = + HashMap::new(); for m in prepared.graph.modules.iter() { let name = m.func_env.name.clone(); module_names.insert(name.clone()); @@ -40037,6 +40163,7 @@ fn reference_closure_index( tally: &mut class_tally, unclassified: &mut unclassified, module: String::new(), + value_refs: std::collections::BTreeSet::new(), occurrences: 0, free_reference_edges: 0, bound_occurrences_suppressed: 0, @@ -40064,6 +40191,7 @@ fn reference_closure_index( for chain in chains { flat.insert(format!("\u{1f}{}", chain.join("."))); } + value_refs_by_module.insert(name.clone(), std::mem::take(&mut classify.value_refs)); refs_by_module.insert(name, flat); } // EXACT RECONCILIATION IS ASSERTED BEFORE THE INDEX IS PUBLISHED, not reported after it is @@ -40133,6 +40261,7 @@ fn reference_closure_index( decl_index, module_names, refs_by_module, + value_refs_by_module, }); eprintln!( "[floor-phase] phase=reference-closure-index state=completed wall_ms={} modules={} names={} subject={}", @@ -40360,8 +40489,8 @@ fn claim_scope_for_with_memos( // Which module won each bare name, and whether it won inside the authored region. A later // module claiming a name already won OUTSIDE that region is the ambiguous case: two // transitively-reached declarations spell the same and nothing the author wrote ranks them. - let mut winner_of: HashMap = HashMap::new(); - let mut ambiguous: HashSet = HashSet::new(); + let mut winner_of: HashMap = HashMap::new(); + let mut ambiguous: BTreeMap> = BTreeMap::new(); { let module_by_name: HashMap<&str, &Rc> = modules .iter() @@ -40380,21 +40509,26 @@ fn claim_scope_for_with_memos( // the registry underneath it stops being ambiguous. for (_identity, info) in module.item_registry.iter() { let name = &info.name; + let kind = item_kind_census_label(&info.kind); match winner_of.get(name) { None => { item_registry.insert(name.clone(), info.clone()); - winner_of.insert(name.clone(), (module_name.clone(), authored)); + winner_of.insert(name.clone(), (module_name.clone(), authored, kind)); } // Already claimed by this same module — one module's own registry, not a // collision between two. - Some((winner, _)) if winner == module_name => {} + Some((winner, _, _)) if winner == module_name => {} // Already won inside the authored region: the author's imports rank it and // precedence has settled it. Ordinary shadowing, not ambiguity. - Some((_, true)) => {} + Some((_, true, _)) => {} // Won outside it, and now claimed again from outside it. Nothing the author - // wrote decides between these two spellings. - Some((_, false)) => { - ambiguous.insert(name.clone()); + // wrote decides between these two spellings. BOTH sides are recorded, not + // just the loser: a census that named only the newcomer could not say what + // it collided with, and the rename that dissolves the site needs both. + Some((held_module, false, held_kind)) => { + let claimants = ambiguous.entry(name.clone()).or_default(); + claimants.insert((held_module.clone(), *held_kind)); + claimants.insert((module_name.clone(), kind)); } } } @@ -40424,11 +40558,71 @@ fn claim_scope_for_with_memos( fragments, ); let indexes_nanos = indexes_started.elapsed().as_nanos(); + // WHICH OF THOSE AMBIGUOUS NAMES IS ACTUALLY READ THROUGH THE SHARED SLOT, decided + // statically over every value-position reference in the scope rather than over the lookups + // any fold executes. Computed HERE, after the indexes exist, because the question "does this + // reference fall through to the shared slot" is `lookup_fn_from`'s and is answered on + // `lookup_fn_from`'s own index — see `PreparedScopeIndexes::falls_through_to_shared_slot`. + // An earlier revision asked it of `func_env.parents` instead and under-reported silently: + // that carrier is the flattened transitive closure, so a module merely REACHED by the site + // suppressed a row the interpreter would still resolve through the slot. + // + // The reference sites come from `reference_closure_index`, which already classified every + // `ExprVar` occurrence into bindings and free references; its VALUE-POSITION projection is + // the one consulted, since `refs_by_module` unions type annotations, record-literal type + // names and variant constructors into one set and a wall keyed on that union would refuse a + // TYPE collision at a site where the shared value slot is never consulted. + let mut ambiguous_reads: Vec = Vec::new(); + let mut qualified_reads: Vec<(String, String, String)> = Vec::new(); + if !ambiguous.is_empty() { + for module in scoped_graph.modules.iter() { + let referring = module.func_env.name.as_str(); + let Some(refs) = ref_index.value_refs_by_module.get(referring) else { + continue; + }; + let site_file = module.module.span.file.as_str(); + for name in refs.iter() { + let Some(claimants) = ambiguous.get(name) else { + continue; + }; + if !indexes.falls_through_to_shared_slot(site_file, name) { + // Not a defect — and worth publishing anyway, because "this site no longer + // appears in the ambiguous list" and "this site now reads the declaration its + // author named" are different claims and only the second is the fix. + if let Some(resolved) = indexes.site_resolved_module(site_file, name) { + qualified_reads.push((name.clone(), referring.to_string(), resolved)); + } + continue; + } + ambiguous_reads.push(AmbiguousBareRead { + name: name.clone(), + referring_module: referring.to_string(), + claimants: claimants.iter().cloned().collect(), + }); + } + } + // A function of the scope's data, not of the module vector's order. + ambiguous_reads.sort_by(|a, b| { + a.name + .cmp(&b.name) + .then_with(|| a.referring_module.cmp(&b.referring_module)) + }); + qualified_reads.sort(); + qualified_reads.dedup(); + } Ok(PreparedClaimScope { indexes, module_count, scope_identity, - ambiguous_bare_names: ambiguous.len(), + ambiguous_bare_reads: ambiguous_reads, + qualified_bare_reads: qualified_reads, + ambiguous_bare_names: ambiguous + .into_iter() + .map(|(name, claimants)| AmbiguousBareName { + name, + claimants: claimants.into_iter().collect(), + }) + .collect(), build_split: ScopeBuildSplit { order_nanos, registry_nanos, @@ -42927,6 +43121,7 @@ mod reference_collector_binder_fixtures { tally: &mut tally, unclassified: &mut unclassified, module: "fixture".to_string(), + value_refs: std::collections::BTreeSet::new(), occurrences: 0, free_reference_edges: 0, bound_occurrences_suppressed: 0, diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 91c0358772b..2c2a461d180 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -2610,6 +2610,7 @@ pub fn reference_resolution_facts( tally: &mut scratch_tally, unclassified: &mut scratch_unclassified, module: self_module.clone(), + value_refs: std::collections::BTreeSet::new(), occurrences: 0, free_reference_edges: 0, bound_occurrences_suppressed: 0, diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index 86031c094d3..8cb4ea06a52 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -6113,6 +6113,22 @@ pub fn run_required_floor( let mut scope_build_split = crate::cli_run::ScopeBuildSplit::default(); let mut ambiguous_total: usize = 0; let mut ambiguous_max: usize = 0; + // THE DISTINCT POPULATION, which the summed total cannot express: `names_total` adds one + // scope's count to the next, so a name claimed in 300 scopes is counted 300 times. The set a + // rename campaign has to dissolve is this one — name to the claimants that spell it, unioned + // across every scope that reached them — plus how many scopes each name is ambiguous in, + // which is what ranks the offenders. + let mut ambiguous_claimants: BTreeMap> = + BTreeMap::new(); + let mut ambiguous_scope_count: BTreeMap = BTreeMap::new(); + // AND THE READS: (name, referring module) sites, unioned across scopes. This is the + // population a refusal is affordable against — a declared name nothing reads bare is a + // convention, and the corpus carries whole families of those. + let mut ambiguous_read_sites: BTreeMap<(String, String), BTreeSet<(String, &'static str)>> = + BTreeMap::new(); + // The positive half: (name, referring module) -> the module that answered it, for names that + // ARE ambiguous in the shared slot but are not read through it at this site. + let mut qualified_read_sites: BTreeMap<(String, String), BTreeSet> = BTreeMap::new(); let mut final_symbol_retention = None; for (index, claim) in claims.iter().enumerate() { if index % 1000 == 0 { @@ -6146,10 +6162,29 @@ pub fn run_required_floor( scope_build_split.accumulate(&built.build_split); scope_module_total += built.indexes.modules.len(); scope_module_max = scope_module_max.max(built.indexes.modules.len()); - if built.ambiguous_bare_names > 0 { + if !built.ambiguous_bare_names.is_empty() { scopes_with_ambiguity += 1; - ambiguous_total += built.ambiguous_bare_names; - ambiguous_max = ambiguous_max.max(built.ambiguous_bare_names); + ambiguous_total += built.ambiguous_bare_names.len(); + ambiguous_max = ambiguous_max.max(built.ambiguous_bare_names.len()); + for (name, referring, resolved) in built.qualified_bare_reads.iter() { + qualified_read_sites + .entry((name.clone(), referring.clone())) + .or_default() + .insert(resolved.clone()); + } + for row in built.ambiguous_bare_reads.iter() { + ambiguous_read_sites + .entry((row.name.clone(), row.referring_module.clone())) + .or_default() + .extend(row.claimants.iter().cloned()); + } + for row in built.ambiguous_bare_names.iter() { + *ambiguous_scope_count.entry(row.name.clone()).or_insert(0) += 1; + ambiguous_claimants + .entry(row.name.clone()) + .or_default() + .extend(row.claimants.iter().cloned()); + } } current_scope = Some((claim.module_path.clone(), built)); } @@ -6955,9 +6990,127 @@ pub fn run_required_floor( // reference closure never donates a colliding name and the flat registry is adequate in // practice; anything else sizes the terminal per-module-environment correction. eprintln!( - "[floor-bare-name-ambiguity] scopes_affected={} of {} names_total={} worst_scope={}", - scopes_with_ambiguity, scope_constructions, ambiguous_total, ambiguous_max + "[floor-bare-name-ambiguity] scopes_affected={} of {} names_total={} worst_scope={} \ + names_distinct={}", + scopes_with_ambiguity, + scope_constructions, + ambiguous_total, + ambiguous_max, + ambiguous_claimants.len() ); + let ambiguous_claimants_len = ambiguous_claimants.len(); + // AND THE NAMES THEMSELVES. `names_total` sizes the population and names nothing in it, so + // it can size a campaign and cannot be the campaign's input. Layer 2 of this class is a + // refusal landed together with the rename or qualification of every site it would refuse; + // these lines ARE that site list, one per distinct name, carrying every claimant and the + // kinds they claim it as. Not truncated to a top-N: a census whose tail is elided is an + // allow-list with extra steps, and the refusal this feeds admits none. + { + let mut kind_histogram: BTreeMap = BTreeMap::new(); + // The union is folded back into the SAME carrier the scope produced, so the census line + // and the per-scope rows answer about one type and the kind signature is read off it + // rather than recomputed here — the alternative was a second copy of that fold living in + // the reporter. + let mut ranked: Vec = ambiguous_claimants + .into_iter() + .map(|(name, claimants)| crate::cli_run::AmbiguousBareName { + name, + claimants: claimants.into_iter().collect(), + }) + .collect(); + // Scope count descending, then the name, so the ordering is a function of the census and + // not of a map's iteration. + ranked.sort_by(|a, b| { + let a_scopes = ambiguous_scope_count.get(&a.name).copied().unwrap_or(0); + let b_scopes = ambiguous_scope_count.get(&b.name).copied().unwrap_or(0); + b_scopes.cmp(&a_scopes).then_with(|| a.name.cmp(&b.name)) + }); + for row in ranked.iter() { + let name = &row.name; + let claimants = &row.claimants; + let signature = row.kind_signature(); + *kind_histogram.entry(signature.clone()).or_insert(0) += 1; + let sites = claimants + .iter() + .map(|(module, kind)| format!("{module}:{kind}")) + .collect::>() + .join(","); + eprintln!( + "[floor-bare-name-ambiguity-name] name={name} scopes={} kinds={signature} \ + claimants={sites}", + ambiguous_scope_count.get(name).copied().unwrap_or(0) + ); + } + let mix = kind_histogram + .iter() + .map(|(signature, count)| format!("{signature}={count}")) + .collect::>() + .join(" "); + // The kind mix, folded from the same rows rather than tallied beside them. A signature + // mixing `data` with `fn` is the dangerous shape — it crosses the evaluator's kind + // dispatch — and one that is `fn+fn` is the common shape that merely picks a body. + eprintln!("[floor-bare-name-ambiguity-kinds] {mix}"); + } + // THE READS, WHICH ARE THE DEFECTS. Everything above is a DECLARATION census: it says which + // names two transitively-reached modules both spell. A name nothing references bare is + // harmless, and the corpus deliberately carries per-module convention rows that hundreds of + // modules each declare — so the population a refusal has to dissolve is not that one, it is + // the set of reference SITES that actually fall through to the ambiguous shared slot. + // Counted statically over each scope's whole closure, never over the lookups this fold + // happened to execute: a reference on a path no witness runs is exactly the site a later + // refusal would surprise. The grain is (name, referring module), which is the grain a fix is + // written at — `refs_by_module` publishes a module's free references deduped, so this is a + // count of referencing modules per name and NOT a count of occurrences. + { + let mut read_names: BTreeMap = BTreeMap::new(); + for ((name, referring_module), claimants) in ambiguous_read_sites.iter() { + *read_names.entry(name.clone()).or_insert(0) += 1; + let sites = claimants + .iter() + .map(|(module, kind)| format!("{module}:{kind}")) + .collect::>() + .join(","); + eprintln!( + "[floor-bare-name-ambiguity-read] name={name} referring_module={referring_module} \ + claimants={sites}" + ); + } + eprintln!( + "[floor-bare-name-ambiguity-reads] read_name_module_pairs={} \ + read_names_distinct={} declared_names_distinct={}", + ambiguous_read_sites.len(), + read_names.len(), + ambiguous_claimants_len + ); + // THE ADJACENT CLASS THIS CENSUS DELIBERATELY DOES NOT COVER, named so that a zero here + // can never be read as "no ambiguity in the corpus". A value-position read is what the + // evaluator resolves through the shared slot; a TYPE whose spelling two modules share is + // a real ambiguity in a different channel, and the corpus carries a large deliberate + // instance of it -- String, Int, List, Bool, WireContract each declared by both `std.*` + // and its `v2.std.*` self-host copy. That double is owned by the v2 self-host + // replacement migration, which ends it by ending the double, and is not a rename this + // wall could ask for. + // WHAT THE QUALIFIED SITES RESOLVE TO. A site that has left the ambiguous list above + // has either been qualified or has stopped being read; these lines say which, and name + // the declaration the reference now reaches. Without them the only evidence a + // qualification worked is a row disappearing from a census this same process produces. + for ((name, referring_module), resolved) in qualified_read_sites.iter() { + eprintln!( + "[floor-bare-name-ambiguity-bound] name={name} referring_module={referring_module} \ + resolves_to={}", + resolved.iter().cloned().collect::>().join(",") + ); + } + eprintln!( + "[floor-bare-name-ambiguity-reads] qualified_name_module_pairs={}", + qualified_read_sites.len() + ); + eprintln!( + "[floor-bare-name-ambiguity-reads] channel=value_position_only \ + not_covered=type_position_name_collisions \ + owner=v2_self_host_replacement_migration" + ); + } // WHAT ONE SCOPE COSTS. `mean` divides only by constructions that measured a rise, so it is // the mean cost of a scope that cost anything; a scope whose modules were all resident from // the previous one reads as free and would otherwise drag the mean toward zero. This is the @@ -8118,6 +8271,10 @@ mod scope_fragment_memo_equivalence { memoized.ambiguous_bare_names, control.ambiguous_bare_names, "{entry}: ambiguity population diverged" ); + assert_eq!( + memoized.ambiguous_bare_reads, control.ambiguous_bare_reads, + "{entry}: ambiguous READ sites diverged" + ); assert_eq!( memoized.resolution_fingerprint(), control.resolution_fingerprint(), diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index e1d17559015..a96ffee98bd 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -4189,6 +4189,65 @@ pub struct PreparedScopeIndexes { } impl PreparedScopeIndexes { + /// THE SITE-RELATIVE TIERS, AND THE ONLY IMPLEMENTATION OF THEM. + /// + /// A bare reference resolves first through the site file's OWN module's qualified + /// declaration, then through the module that file explicitly imported the name FROM — and + /// both only for a name the shared slot cannot represent unambiguously, which is the + /// condition that makes the tiers worth consulting at all. `None` means no tier answered and + /// the reference reaches the shared bare slot below. + /// + /// `InterpContext::lookup_fn_from` IS this method plus that slot, and the bare-name + /// ambiguity census asks this same method whether a reference falls through to it. They + /// cannot drift, because there is nothing to drift from: an earlier revision stated "one + /// rule, one home" while leaving the interpreter its own copy, and the two copies had + /// already disagreed on this gate. + /// + /// `ResolvedFuncEnv.parents` is exactly wrong for the second tier and was tried — this + /// carrier's own `file_import_bindings` note says why: it is the FLATTENED TRANSITIVE + /// closure, which "does not separate a direct import from a transitively reachable module". + /// A wildcard import binds no names and contributes nothing here, which is the residue the + /// census exists to find. + fn site_resolved_fn(&self, site_file: &str, name: &str) -> Option<&Rc> { + if name.contains('.') + || site_file.is_empty() + || !self.ambiguous_bare_function_names.contains(name) + { + return None; + } + if let Some(module_path) = self.file_module_paths.get(site_file) { + if let Some(node) = self.fn_nodes.get(&format!("{module_path}.{name}")) { + return Some(node); + } + } + if let Some(source_module) = self + .file_import_bindings + .get(&(site_file.to_string(), name.to_string())) + { + if let Some(node) = self.fn_nodes.get(&format!("{source_module}.{name}")) { + return Some(node); + } + } + None + } + + /// DOES A BARE VALUE REFERENCE AT THIS SITE FALL THROUGH TO THE SHARED SLOT? The census's + /// question and the wall's, answered by the resolution the interpreter performs rather than + /// by a restatement of it. + pub fn falls_through_to_shared_slot(&self, site_file: &str, name: &str) -> bool { + self.site_resolved_fn(site_file, name).is_none() + } + + /// AND WHICH MODULE ANSWERED, when one did. The census's negative — a pair that has left the + /// ambiguous-read list — proves only that the list moved; this says what the reference now + /// resolves TO, which is what a qualification is actually for. Derived from the resolved + /// declaration's own source file through the same `file_module_paths` the first tier reads, + /// so it names the module that answered rather than the module the census hoped would. + pub fn site_resolved_module(&self, site_file: &str, name: &str) -> Option { + let node = self.site_resolved_fn(site_file, name)?; + self.file_module_paths.get(node.span.file.as_str()).cloned() + } + /// EVERY RESOLUTION THIS INDEX SET CAN ANSWER, rendered at identity grain and sorted, so /// two index sets can be compared for equality of ANSWERS rather than of construction path. /// Items are identified by `Rc` address: the same declaration node, not merely an equal @@ -4979,33 +5038,20 @@ impl InterpContext { /// subject and retires with namespace-only resolution, where a reference has exactly one /// declarer by construction. fn lookup_fn_from(&self, name: &str, site_file: &str) -> Option<&Rc> { - if !name.contains('.') - && !site_file.is_empty() - && self.indexes.ambiguous_bare_function_names.contains(name) - { - if let Some(module_path) = self.indexes.file_module_paths.get(site_file) { - let qualified = format!("{}.{}", module_path, name); - if let Some(node) = self.indexes.fn_nodes.get(&qualified) { - return Some(node); - } - } - // THEN WHERE THE AUTHOR SAID IT COMES FROM: an explicitly imported name resolves to - // the module it was imported FROM, a fact the shared slot discards. Without this tier - // `import a.b.c { anchor }` is inert whenever another module in scope declares - // `anchor` -- the reference lands on the precedence winner and the import line reads - // as though it decided something. - if let Some(source_module) = self - .indexes - .file_import_bindings - .get(&(site_file.to_string(), name.to_string())) - { - let qualified = format!("{}.{}", source_module, name); - if let Some(node) = self.indexes.fn_nodes.get(&qualified) { - return Some(node); - } - } - } - self.indexes.fn_nodes.get(name) + // WHERE THE REFERENCE WAS WRITTEN ANSWERS FIRST: the site file's own module's + // declaration, then where the author said the name comes from -- an explicitly imported + // name resolves to the module it was imported FROM, a fact the shared slot discards. + // Without that tier `import a.b.c { anchor }` is inert whenever another module in scope + // declares `anchor`: the reference lands on the precedence winner and the import line + // reads as though it decided something. + // + // Both tiers live in `PreparedScopeIndexes::site_resolved_fn`, which is also what the + // bare-name ambiguity census asks whether a reference falls through to the slot below. + // One implementation, so the population the census names is the population execution + // resolves. + self.indexes + .site_resolved_fn(site_file, name) + .or_else(|| self.indexes.fn_nodes.get(name)) } pub fn lookup_fn_node(&self, qualified_name: &str) -> Option> {