Skip to content
Merged
6 changes: 3 additions & 3 deletions dag/gunbc/keyed_dependency_edge_read_seed_growth.dag
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import std.decl_ref { DeclarationRef, WholeDeclaration }
// any row it returns is the row the population read carries for that path, in the same order.
// Its first form grouped the whole population union by importer, which made the first keyed read
// in a process pay the whole-pool reference parse (the floor's reference_edges shared fill,
// 15.7s charged to one claim on gunbc#12523); the per-file form demands the pool name index only
// for an import-less importer, whose edges genuinely depend on other files' names.
// 15.7s charged to one claim on gunbc#12523); the per-file form demands the pool name index for
// every readable importer, import-bearing or not, since a reference edge depends on other files' names.
//
// A FOURTH ITEM, with_module_path_index, is the borrowing reader of the SAME MODULE_PATH_INDEX_CACHE
// build_module_path_index fills: the keyed reads above asked one entry of it per visited module and
Expand Down Expand Up @@ -54,5 +54,5 @@ data keyed_dependency_edge_read_seed_growth_justification: SeedGrowthJustificati
reason: "A keyed read of a host-produced edge census is a property of the host that holds it: the edges are produced by the seed's own source walk and parse, the population read that produces them is already native (dependency_resolution_facts, moved to the host because its interpreted union cost 104,943ms), and grouping them on the side of the seam where they still exist as host rows is the only place the grouping costs nothing. Running the grouping in the interpreter is precisely the corpus-wide fold being removed.\n\nTHE RETURN SHAPE IS THE POPULATION READ'S. It returns the same ModuleDependencyEdge rows the population read returns for that importer, including target_declared: false rows, so the demand walk applies the same edge law the adjacency fold did: an undeclared target contributes no path.\n\nA FILE PATH AS AN IMPORTER ROOT NOW REFUSES. Before this change a file passed to the population read's importer_roots reached cli_run anchor_source_root and aborted the process with a panic. Both arms now classify their directory roots with coproduct_reflection pool_root_defects first and refuse with InterpError PoolRootContributesNothing, naming the builtin and each offending root with its PoolRootDefect.",
owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId,
trigger: "Delete when a SELF-EMITTED reader reaches the per-file dependency edges of a module: that is, when the edge census is produced by an emitted v2 fold over the source roots rather than by the seed's host walk, so the self-emitted closure walk can ask one importer's edges of that fold directly. Sufficient means the emitted reader answers dependency_closure_live_excluding for the approval broker entry and one compiler-reaching entry with the same closures this host read produces; an emitted population read that the walk would still have to fold whole does not satisfy it.",
current_boundary: "dependency_resolution_facts_at reads one importer file and composes its two per-file halves: import_facts_for_file against the process-cached build_module_path_index, and reference_edges_for_file_on_demand, which decides an unreadable or import-bearing file from its own bytes and builds the process-cached pool name index (reference_pool_names) only for an import-less one. An importer outside every pool root, matched by an exclusion, or unreadable answers the empty list, as the population read carries no row for it. It adds no walk, no row, no field and no decision, and holds no state of its own: the two indexes it consults are the ones the population read already caches."
current_boundary: "dependency_resolution_facts_at reads one importer file and composes its two per-file halves: import_facts_for_file against the process-cached build_module_path_index, and reference_edges_for_file_on_demand, which decides an unreadable file from its own bytes and builds the process-cached pool name index (reference_pool_names) for every readable importer, import-bearing or not. An importer outside every pool root, matched by an exclusion, or unreadable answers the empty list, as the population read carries no row for it. It adds no walk, no row, no field and no decision, and holds no state of its own: the two indexes it consults are the ones the population read already caches."
}
8 changes: 8 additions & 0 deletions dag/gunbc/rust_emitted_edge.dag
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ fn rust_module_emit_filename(module_name: String) -> String {
// the same value.
data rust_runtime_prelude_module: String = "v1_rt"

// Files the emitter writes with no .dag module behind them, beside the runtime prelude:
// v1.compiler.emit_rust emit_dry_run_module writes dry_run.rs (the DryRunMode wrapper) whenever the
// closure declares services. The emitter still spells that path itself, so the two are not one
// value; the pairing is the workspace closure check, which refuses an emitted basename that is in
// neither the DAG nor this list, so an emitter that renames or adds such a file is a located
// refusal rather than a silent drift.
data rust_emit_only_basenames: List<String> = ["dry_run"]

fn rust_prelude_emitted_edges(module: String) -> List<EmittedEdge> {
if module == rust_runtime_prelude_module {
[
Expand Down
9 changes: 7 additions & 2 deletions dag/test/claim/module_graph_edge_source_witness_test.dag
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ data witness_subject_note: String = "THE EDGE SOURCE IS A UNION OF TWO PRODUCERS

WHY A FIXTURE TREE RATHER THAN HAND-BUILT LISTS. A unit test of union_import_resolution_fact_lists proves the fold and leaves the defect untouched, because the defect is in WHICH PRODUCERS the production function calls — the fold receives whatever it is handed. So this points the production function itself at a four-module fixture: consumer imports provider_imported and provider_shared explicitly, and reaches provider_referenced ONLY through a qualified container.member reference with no import. The union is then asserted at identity grain in both directions — every expected edge present, and no surplus target — so a missing arm reds on the reference-only edge and a doubled arm reds on the surplus.

WHY THERE IS NO SHARED-PROVIDER DEDUP CONTROL HERE, stated rather than quietly omitted: the two producers are DISJOINT BY FILE. reference_resolution_facts emits nothing for a file that still carries import lines, precisely so the reference producer cannot over-connect the un-stripped tree, so one path cannot appear in both producers and a same-path duplicate is unconstructible through this seam. Asserting a dedup that cannot arise would be a tautological control. What IS asserted instead is the union at identity grain in both directions - every producer row reaches the union, and the union carries no row neither producer produced - which is the property the dedup exists to preserve."
THE PRODUCERS OVERLAP, SO THE DEDUP HAS ITS OWN CONTROL. reference_resolution_facts answers import-bearing files too, so one path can appear in both producers and a dual-backed edge is constructible; witness_union_has_no_duplicate_target is the control that such an edge appears once. Beside it, the union is asserted at identity grain in both directions - every producer row reaches the union, and the union carries no row neither producer produced - which is the property the dedup exists to preserve."

data fixture_roots: List<String> = ["dag/test/fixture/module_graph_edge_source"]

Expand Down Expand Up @@ -89,9 +89,14 @@ test fn witness_every_producer_row_reaches_the_union() -> Bool {
fold(produced, init: true, f: fn(acc, t) { acc && contains_str(xs: edge_targets, want: t) })
}

// consumer_imported imports provider_imported AND reaches it by a qualified reference, so BOTH
// producers carry that edge: the overlap is constructed, not asserted in prose. Deleting the dedup
// makes the union carry it twice and this reds.
test fn witness_union_has_no_duplicate_target() -> Bool {
let edge_targets = targets_of(edges: fixture_edges())
count_of(xs: edge_targets, want: imported_provider) == 1
contains_str(xs: fact_targets(facts: fixture_import_facts()), want: imported_provider)
&& contains_str(xs: fact_targets(facts: fixture_reference_facts()), want: imported_provider)
&& count_of(xs: edge_targets, want: imported_provider) == 1
&& count_of(xs: edge_targets, want: referenced_provider) == 1
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import std.types { Int }
import test.fixture.module_graph_edge_source.provider_imported { edge_source_only_imported_value }

fn edge_source_imported_consumer_total() -> Int {
edge_source_only_imported_value()
edge_source_only_imported_value() + test.fixture.module_graph_edge_source.provider_imported.edge_source_only_imported_value()
}
143 changes: 135 additions & 8 deletions src/v1/stage0/src/cli_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6337,10 +6337,9 @@ impl ModuleGraphFactsLive {
/// reference edge (`selection_adjacency` minus `adjacency`) — i.e. the direct-import term
/// a stripped (no `import` line) module is otherwise missing from its typed-module content
/// key (DESIGN §3: consumes the same `selection_adjacency` authority affected-set selection
/// already reads; no second reference-edge producer). An import-bearing file's declared
/// imports are already covered by `resolved.resolved_imports`, so this returns empty for it
/// (`adjacency` and `selection_adjacency` agree on such a file — see
/// `reference_resolution_facts` pass 2).
/// already reads; no second reference-edge producer). For an import-bearing file it
/// returns the modules it reaches by qualified reference (bare names there are lexically bound) WITHOUT importing them;
/// its declared imports are already covered by `resolved.resolved_imports`.
/// Workspace-relative repo paths `importer_repo_path` depends on ONLY through a strict-tier
/// reference edge (`selection_adjacency` minus `adjacency`). The path-grain authority
/// `selection_adjacency` already carries; module names are derived only for diagnostics.
Expand Down Expand Up @@ -30145,8 +30144,8 @@ pub fn dependency_resolution_facts(
///
/// WHAT IT DOES NOT DEMAND is the corpus. The declared-module index is the process-cached
/// `build_module_path_index` the population read also consults; the reference half's pool name
/// index is built only when the importer carries no `import` line, which is the one case whose
/// edges depend on other files' names. An importer the population would not walk -- outside every
/// index is built for every readable importer, since a reference edge depends on other files'
/// names whether or not the importer carries `import` lines. An importer the population would not walk -- outside every
/// pool root, or matched by an exclusion -- answers the empty list, as the population carries no
/// row for it; so does an unreadable one, whose import half the population walk also skips.
pub fn dependency_resolution_facts_at(
Expand Down Expand Up @@ -30179,8 +30178,7 @@ pub fn dependency_resolution_facts_at(
entry_resolve::FileReferenceEdges::Edges(edges) => {
reference_edges_as_import_facts(&edges, /* strict */ true)
}
entry_resolve::FileReferenceEdges::ImportBearing
| entry_resolve::FileReferenceEdges::Unaccounted(_) => Vec::new(),
entry_resolve::FileReferenceEdges::Unaccounted(_) => Vec::new(),
};
union_dedup_import_facts_reference_first(reference_edges, import_edges)
}
Expand Down Expand Up @@ -37310,6 +37308,135 @@ mod output_policy_decode_tests {
}
}

#[cfg(test)]
mod import_bearing_reference_edges {
//! An `import` line does not own a module's edge set: the producer unions reference edges for
//! every importer. Supplied fixture (pool names handed in), no corpus resolve.

use super::*;

fn names() -> entry_resolve::ReferencePoolNames {
entry_resolve::ReferencePoolNames {
decl_index: Default::default(),
module_names: [
"v2.std.artifact",
"v2.std.refinement",
"v2.std.node",
"v2.std.layer",
]
.iter()
.map(|s| s.to_string())
.collect(),
}
}

const IMPORTING: &str =
"module v2.std.artifact\nimport v2.std.node\nfn g() -> Int { v2.std.refinement.k }\n";
const IMPORTLESS: &str = "module v2.std.artifact\nfn g() -> Int { v2.std.refinement.k }\n";

fn edge_targets(src: &str, imports_only_mutant: bool) -> Vec<String> {
if imports_only_mutant && !extract_import_paths(src).is_empty() {
return Vec::new();
}
let n = names();
let entry_resolve::FileReferenceEdges::Edges(edges) =
entry_resolve::reference_edges_for_file("v2/std/artifact.dag", Some(src), &n)
else {
panic!("a parsed file has edges");
};
let import_edges = entry_resolve::import_facts_for_file("v2/std/artifact.dag", src, |m| {
n.module_names.contains(m)
});
union_dedup_import_facts_reference_first(
entry_resolve::reference_edges_as_import_facts(&edges, true),
import_edges,
)
.into_iter()
.map(|f| f.import_module)
.collect()
}

/// RED: the qualified reference to a module the file does not import is an edge.
#[test]
fn an_import_bearing_file_carries_its_unimported_reference_edge() {
let t = edge_targets(IMPORTING, false);
assert!(t.contains(&"v2.std.refinement".to_string()), "{t:?}");
assert!(t.contains(&"v2.std.node".to_string()), "{t:?}");
}

/// The imports-only mutant is the pre-fix producer; the RED must reject it.
#[test]
fn the_imports_only_mutant_fails_the_red() {
let t = edge_targets(IMPORTING, true);
assert!(!t.contains(&"v2.std.refinement".to_string()));
}

/// Control: an import-less file's reference edges are the same under both producers.
#[test]
fn an_importless_file_is_unchanged() {
assert_eq!(
edge_targets(IMPORTLESS, false),
edge_targets(IMPORTLESS, true)
);
assert_eq!(
edge_targets(IMPORTLESS, false),
vec!["v2.std.refinement".to_string()]
);
}

fn homonym_names() -> entry_resolve::ReferencePoolNames {
let mut decl_index = HashMap::new();
decl_index.insert(
"Present".to_string(),
["std.optional", "test.fixture.planted"]
.iter()
.map(|s| s.to_string())
.collect(),
);
entry_resolve::ReferencePoolNames {
decl_index,
module_names: ["std.optional", "test.fixture.planted"]
.iter()
.map(|s| s.to_string())
.collect(),
}
}

fn homonym_edges(src: &str) -> Vec<String> {
let entry_resolve::FileReferenceEdges::Edges(edges) =
entry_resolve::reference_edges_for_file(
"test/claim/a.dag",
Some(src),
&homonym_names(),
)
else {
panic!("a parsed file has edges");
};
edges.into_iter().map(|e| e.target_module).collect()
}

/// RED (reader defect): `Present` is bound by the file's own `import std.optional { Present }`.
/// A fixture module declaring the same spelling and nearer in the containment tree must not
/// become a phantom dependency of the importer; in a file that imports, a bare name is
/// lexically bound, never resolved by proximity.
#[test]
fn a_bare_name_in_an_import_bearing_file_is_not_resolved_to_a_pool_homonym() {
let src =
"module test.claim.a\nimport std.optional { Present }\nfn g() -> Int { Present }\n";
let t = homonym_edges(src);
assert!(!t.contains(&"test.fixture.planted".to_string()), "{t:?}");
}

/// Control: the same bare name in an IMPORT-LESS file still resolves by proximity (unchanged),
/// and a qualified reference in an import-bearing file is still an edge (the other test).
#[test]
fn the_same_bare_name_without_the_import_still_resolves() {
let src = "module test.claim.a\nfn g() -> Int { Present }\n";
let t = homonym_edges(src);
assert!(t.contains(&"test.fixture.planted".to_string()), "{t:?}");
}
}

#[cfg(test)]
mod union_dedup_import_facts_law {
//! The dedup that carries the edge population's identity and order law, tested on synthetic
Expand Down
Loading