diff --git a/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag b/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag index cd740274d85..99013da74fb 100644 --- a/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag +++ b/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag @@ -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 @@ -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." } diff --git a/dag/gunbc/rust_emitted_edge.dag b/dag/gunbc/rust_emitted_edge.dag index 69d413308de..7e6cafeaabc 100644 --- a/dag/gunbc/rust_emitted_edge.dag +++ b/dag/gunbc/rust_emitted_edge.dag @@ -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 = ["dry_run"] + fn rust_prelude_emitted_edges(module: String) -> List { if module == rust_runtime_prelude_module { [ diff --git a/dag/test/claim/module_graph_edge_source_witness_test.dag b/dag/test/claim/module_graph_edge_source_witness_test.dag index 92cf9bb171b..17f17003a3f 100644 --- a/dag/test/claim/module_graph_edge_source_witness_test.dag +++ b/dag/test/claim/module_graph_edge_source_witness_test.dag @@ -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 = ["dag/test/fixture/module_graph_edge_source"] @@ -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 } diff --git a/dag/test/fixture/module_graph_edge_source/consumer_imported.dag b/dag/test/fixture/module_graph_edge_source/consumer_imported.dag index 71d17ba1c40..f30558bc599 100644 --- a/dag/test/fixture/module_graph_edge_source/consumer_imported.dag +++ b/dag/test/fixture/module_graph_edge_source/consumer_imported.dag @@ -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() } diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 728d0d0d83a..a9bab411344 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -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. @@ -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( @@ -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) } @@ -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 { + 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 { + 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 diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 9aa4c5988d2..035271bbb88 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -521,8 +521,10 @@ pub(crate) fn build_module_graph_facts_live_uncached( // modules), while for SELECTION it is precisely the thing that destroys the answer. The loader // (`extend_with_bare_reference_closure`) is deliberately left alone. // - // Import-bearing files emit no reference edges at all (see `reference_resolution_facts` pass 2), - // so on an un-stripped file the union is a no-op and the graph is byte-identical to before. + // (SUPERSEDED: this measurement predates the all-importer producer. Import-bearing files now + // also emit strict-tier reference edges for modules they reach without importing, so the union + // is no longer a no-op on an un-stripped file. The selection-tier effect of that widening is + // measured by the floor, not asserted here.) // FIVE ROWS, EACH NET OF THE OTHERS. The import-edge facts are the module path index's first // demander, and that index is the first demander of every pool file's lexing, newline index // and heads parse (`pool_acquire`, which records those three as `pool_source_tokenize`, @@ -3155,9 +3157,6 @@ impl ReferenceSelectionTier { /// One file's answer from the reference-edge producer. pub(crate) enum FileReferenceEdges { - /// The file carries `import` lines: its edges are owned EXACTLY by `import_resolution_facts`, - /// and emitting reference edges for it would only over-connect. - ImportBearing, /// The file's reference edges, every tier (`reference_edges_as_import_facts` filters). Edges(Vec), /// An import-less file the producer could not answer for, with the located cause. @@ -3338,9 +3337,10 @@ pub(crate) fn reference_edges_for_file( } /// The same per-file answer with the pool name index DEMANDED rather than supplied. An unreadable -/// or import-bearing file is decided from its own bytes, so a caller asking about one such file -/// never builds the whole-pool heads index; only an import-less file, whose references must be -/// resolved against pool names, forces it. +/// file is decided from its own bytes and never builds the whole-pool heads index. EVERY readable +/// file, import-bearing or not, answers its reference edges: an `import` line declares some of a +/// module's dependencies, not all of them (`v2.std.artifact` imports three modules and reaches +/// `v2.std.refinement` only by qualified reference), so imports never own a file's edge set. pub(crate) fn reference_edges_for_file_on_demand< R: std::ops::Deref, >( @@ -3351,9 +3351,6 @@ pub(crate) fn reference_edges_for_file_on_demand< let Some(content) = content else { return FileReferenceEdges::Unaccounted("unreadable"); }; - if !extract_import_paths(content).is_empty() { - return FileReferenceEdges::ImportBearing; - } let names = names(); let names: &ReferencePoolNames = &names; let Some(self_module) = extract_module_path(content) else { @@ -3363,7 +3360,12 @@ pub(crate) fn reference_edges_for_file_on_demand< Ok(refs) => refs, Err(cause) => return FileReferenceEdges::Unaccounted(cause), }; - let ParsedFileReferences { bare, chains, .. } = refs; + let ParsedFileReferences { + bare, + chains, + imports, + .. + } = refs; // Resolve to per-file (target_module → strongest confidence). let mut file_edges: std::collections::BTreeMap = std::collections::BTreeMap::new(); @@ -3387,6 +3389,15 @@ pub(crate) fn reference_edges_for_file_on_demand< if super::is_substrate_vocabulary(name) { continue; } + // LEXICAL BINDING, NOT PROXIMITY, IN A FILE THAT IMPORTS: a bare name there is a local + // declaration or a name the file imports (the import edge already carries that), so + // guessing a pool declarer for it is a heuristic the closed substrate never needs. It + // manufactured a phantom edge from `std.optional { Present }` importers to a fixture that + // merely declares the same spelling. The proximity tier below stays for import-less files, + // where it already applied. + if !imports.is_empty() { + continue; + } if let Some(mods) = names.decl_index.get(name) { // Same-module declaration wins by lexical scope (namespace-only): a bare name the // referencing file itself declares resolves LOCALLY — no cross-module edge. This @@ -3490,7 +3501,6 @@ pub fn reference_resolution_facts( } let content = std::fs::read_to_string(&file).ok(); match reference_edges_for_file(&rel, content.as_deref(), &names) { - FileReferenceEdges::ImportBearing => {} FileReferenceEdges::Edges(file_edges) => edges.extend(file_edges), FileReferenceEdges::Unaccounted(cause) => { unaccounted.push(ReferenceAccountingRefusal { diff --git a/src/v1/stage0/src/gunbc_rust_emitted_edge.rs b/src/v1/stage0/src/gunbc_rust_emitted_edge.rs index fcc446709ba..aea43fe5e24 100644 --- a/src/v1/stage0/src/gunbc_rust_emitted_edge.rs +++ b/src/v1/stage0/src/gunbc_rust_emitted_edge.rs @@ -73,6 +73,15 @@ pub fn rust_runtime_prelude_module() -> String { CACHED.with(|c: &String| c.clone()) } +pub fn rust_emit_only_basenames() -> Rc> { + thread_local! { + static CACHED: Rc> = { + Rc::new(vec!["dry_run".to_string()]) + }; + } + CACHED.with(|c: &Rc>| c.clone()) +} + pub fn rust_prelude_emitted_edges(module: String) -> Rc>> { if (module.clone() == rust_runtime_prelude_module()) { Rc::new(vec![Rc::new(EmittedEdge { diff --git a/src/v2/lens/manufactured_dependency_census.dag b/src/v2/lens/manufactured_dependency_census.dag index 2f33272b25d..050063031e3 100644 --- a/src/v2/lens/manufactured_dependency_census.dag +++ b/src/v2/lens/manufactured_dependency_census.dag @@ -28,18 +28,17 @@ import v2.lens.module_graph { // forbids one and is right to; this is a pure reader over the two that module already owns, in the // idiom DESIGN section 6 gives a lens. // -// WHAT THE COUNT IS AND IS NOT, because the honest bound matters more than the number. The two -// producers agree on an import-BEARING file (`ModuleGraphFactsLive::reference_only_direct_import_paths` -// returns empty for one), so this census sees the manufactured edges of import-LESS modules and -// NOT the bare cross-module references of modules that carry an import line. Those exist -- -// `src/v2/lens/grounding.dag` has three import lines and calls functions it never imports, each one -// named by that file's own entry-scoped compile refusal -- and they are invisible here BY -// CONSTRUCTION, not by measuring zero. So this figure is a LOWER BOUND on -// the population that binds without an authored dependency, and quoting it as the population is the -// denominator error. The upper half needs a per-REFERENCE census on the value channel, which does -// not exist: the mask check that emits `UnlistedImportUse` lives in the TYPE resolver -// (`v1.compiler.infer_resolve` `resolve_node`, keyed on `type_name`), so a bare cross-module CALL -// produces neither a refusal nor an advisory and no census can currently see it. +// WHAT THE COUNT IS AND IS NOT, because the honest bound matters more than the number. What it +// counts depends on whether a module imports. An import-LESS module's manufactured edges are every +// reference the reader resolves, qualified or bare. An import-BEARING module's are its qualified +// references only: a bare name there is lexically bound (a local, or an import), so the reader +// resolves no pool declarer for it, and an unimported bare cross-module call in such a module is +// not an edge here. The count is therefore a LOWER BOUND on the population that binds without an +// authored dependency, and quoting it as the population is the denominator error. The upper half +// needs a per-REFERENCE census on the value channel, which does not exist: the mask check that +// emits `UnlistedImportUse` lives in the TYPE resolver (`v1.compiler.infer_resolve` `resolve_node`, +// keyed on `type_name`), so a bare cross-module CALL the reader does not collect produces neither a +// refusal nor an advisory. // // SUBJECT: `gunbc.recurring_failure_mode` // `binding_chosen_by_pool_membership_rather_than_by_the_declared_rule`, whose next-rung trigger diff --git a/src/v2/test/claim/emitted_crate_workspace_witness_test.dag b/src/v2/test/claim/emitted_crate_workspace_witness_test.dag index 99dd93ec995..65e52531931 100644 --- a/src/v2/test/claim/emitted_crate_workspace_witness_test.dag +++ b/src/v2/test/claim/emitted_crate_workspace_witness_test.dag @@ -117,6 +117,38 @@ test fn a_dag_module_the_emission_did_not_produce_refuses_by_name() -> Bool { } } +// dry_run is a declared emit-only basename: emitted with no DAG module, it is admitted beside v1_rt. +test fn a_declared_emit_only_basename_is_not_outside_the_dag() -> Bool { + match emitted_workspace_plan_over( + module_dag: chain_dag(), + emitted_basenames: ["fx_a", "fx_b", "fx_c", "v1_rt", "dry_run"], + edge_froms: chain_edge_froms(), + edge_tos: chain_edge_tos(), + edge_provenances: chain_edge_provenances(), + target_crate_count: 3 + ) { + EmittedWorkspacePlanned { plan: plan } => + plan.rows |> any(r => r.package_name == emitted_workspace_runtime_package && r.modules |> any(m => m == "dry_run")) + EmittedWorkspaceRefused { cause: _ } => false + } +} + +// A closure the emitter wrote no dry_run for (no services) plans a runtime crate that names none. +test fn the_runtime_crate_names_dry_run_only_when_the_emission_wrote_it() -> Bool { + match emitted_workspace_plan_over( + module_dag: chain_dag(), + emitted_basenames: chain_emitted_basenames(), + edge_froms: chain_edge_froms(), + edge_tos: chain_edge_tos(), + edge_provenances: chain_edge_provenances(), + target_crate_count: 3 + ) { + EmittedWorkspacePlanned { plan: plan } => + plan.rows |> any(r => r.package_name == emitted_workspace_runtime_package && !(r.modules |> any(m => m == "dry_run"))) + EmittedWorkspaceRefused { cause: _ } => false + } +} + test fn an_emitted_module_outside_the_dag_refuses_by_name() -> Bool { match emitted_workspace_plan_over( module_dag: chain_dag(), diff --git a/src/v2/workflow/emitted_crate_workspace.dag b/src/v2/workflow/emitted_crate_workspace.dag index f090819e8a1..171f33e1c45 100644 --- a/src/v2/workflow/emitted_crate_workspace.dag +++ b/src/v2/workflow/emitted_crate_workspace.dag @@ -20,6 +20,7 @@ import gunbc.rust_emitted_edge { ReexportFacade, SemanticSourceReference, rust_module_emit_filename, + rust_emit_only_basenames, rust_runtime_prelude_module } import gunbc.stage0_crate_partition_generated { @@ -216,6 +217,7 @@ fn emitted_workspace_basename_dag(module_dag: ModuleDag, module_by_basename: Map match emitted_workspace_lookup(m: emitted_set, key: rust_module_emit_filename(module_name: m as String)) { Present { value: _ } => false Absent => true }) let outside = emitted_basenames |> filter(b => b != rust_runtime_prelude_module + && !(rust_emit_only_basenames |> any(e => e == b)) && match emitted_workspace_lookup(m: module_by_basename, key: b) { Present { value: _ } => false Absent => true }) if ((not_emitted |> count) + (outside |> count)) > 0 { BasenameDagRefused { @@ -284,12 +286,12 @@ fn emitted_workspace_generated_row(row: Stage0PartitionCrateRow, reexports: List } } -fn emitted_workspace_runtime_row() -> GeneratedPartitionCrateRow { +fn emitted_workspace_runtime_row(emitted_basenames: List) -> GeneratedPartitionCrateRow { GeneratedPartitionCrateRow { package_name: emitted_workspace_runtime_package, crate_dir: emitted_workspace_crate_dir(package: emitted_workspace_runtime_package), kind: GeneratedFoundationCrate, - modules: [rust_runtime_prelude_module], + modules: concat([rust_runtime_prelude_module], rust_emit_only_basenames |> filter(e => emitted_basenames |> any(b => b == e))), reexport_packages: [], carries_non_empty_wrappers: true } @@ -299,8 +301,8 @@ type DerivedRowsBuild = DerivedRowsOk { rows: List } | DerivedRowsRefused { cause: EmittedWorkspaceRefusal } -fn emitted_workspace_derived_rows(unit_rows: List, unit_edges: List) -> DerivedRowsBuild { - unit_rows |> fold(init: DerivedRowsOk { rows: [emitted_workspace_runtime_row()] }, f: fn(acc, row) { +fn emitted_workspace_derived_rows(unit_rows: List, unit_edges: List, emitted_basenames: List) -> DerivedRowsBuild { + unit_rows |> fold(init: DerivedRowsOk { rows: [emitted_workspace_runtime_row(emitted_basenames: emitted_basenames)] }, f: fn(acc, row) { match acc { DerivedRowsRefused { cause: c } => DerivedRowsRefused { cause: c } DerivedRowsOk { rows: rows } => @@ -422,7 +424,7 @@ fn emitted_workspace_plan_over( ResolvePartitionOk { assignment: assignment, provenance: _ } => { let unit_ids = list_unique_sorted_ids(items: assignment |> map_values) let unit_edges = assignment_unit_dep_edges(module_dag: closure_dag, assignment: assignment) - match emitted_workspace_derived_rows(unit_rows: emitted_workspace_unit_rows(modules: closure_dag.modules, assignment: assignment, unit_ids: unit_ids), unit_edges: unit_edges) { + match emitted_workspace_derived_rows(unit_rows: emitted_workspace_unit_rows(modules: closure_dag.modules, assignment: assignment, unit_ids: unit_ids), unit_edges: unit_edges, emitted_basenames: emitted_basenames) { DerivedRowsRefused { cause: c } => EmittedWorkspaceRefused { cause: c } DerivedRowsOk { rows: rows } => match emitted_workspace_red(edges: edges, assignment: assignment, unit_edges: unit_edges) {