diff --git a/dag/gunbc/regen_round_cost.dag b/dag/gunbc/regen_round_cost.dag index af3d2b6d10d..2f57c54a3e2 100644 --- a/dag/gunbc/regen_round_cost.dag +++ b/dag/gunbc/regen_round_cost.dag @@ -4,6 +4,10 @@ import std.types { String, Int, List, Bool, NonEmptyStr } import std.nat { Nat } import std.measure { ClockBasis, CpuClock, WallClock, Nanosecond, nanosecond, nanosecond_count } import std.observation { TimedMeasurement, Measured, MeasuredValue, MeasuredUnavailable, observation_duration_of } +import gunbc.stage0_partition_rebuild_scope { + stage0_partition_rebuild_decision_line, + stage0_partition_rebuild_decision_today +} // ONE REGEN ROUND, PRICED BY PHASE, ON TWO CLOCKS -- the instrument behind // `claim_executor --regen-round-cost`. A round is the convergence recipe that @@ -109,6 +113,7 @@ type RegenRoundCostReceipt { rustfmt_spawns: Nat marks: List changed_paths: List + installed_mirrors: List } fn regen_round_phase_tag(phase: RegenRoundPhase) -> String { @@ -231,6 +236,24 @@ fn regen_round_total_line(receipt: RegenRoundCostReceipt) -> String { ) } +// WHAT THE REBUILD WOULD HAVE COST IF IT WERE CHANGE-DENOMINATED, printed beside what it did +// cost. `installed_mirrors` is the regen's own drift answer -- the basenames it just copied into +// the seed tree -- so the scope is derived from the round's real input, not from a fixture. The +// line reports which packages the changed mirrors own, the reverse package closure those force, +// and whether an executable can be assembled from them at all; today it reports +// `executable_assembly=unavailable`, which is why the phase above it is still a whole-crate +// rebuild. The empty `unlocatable` argument is a fact and not a convenience: every element of the +// drift answer is a basename the installer resolved to a file it copied, so at this boundary +// there is nothing the selection could fail to name. +fn regen_round_partition_rebuild_line(receipt: RegenRoundCostReceipt) -> String { + stage0_partition_rebuild_decision_line( + decision: stage0_partition_rebuild_decision_today( + changed_mirrors: receipt.installed_mirrors, + unlocatable: [] + ) + ) +} + fn regen_round_changed_paths_line(receipt: RegenRoundCostReceipt) -> String { let listed = fold(receipt.changed_paths, init: "", f: fn(acc, path) { if acc == "" { path } else { concat(concat(acc, ", "), path) } @@ -249,7 +272,10 @@ fn regen_round_cost_render(receipt: RegenRoundCostReceipt) -> String { }) concat( concat(concat(regen_round_header_line(receipt: receipt), "\n"), mark_lines), - concat(concat(regen_round_total_line(receipt: receipt), "\n"), concat(regen_round_changed_paths_line(receipt: receipt), "\n")) + concat( + concat(concat(regen_round_total_line(receipt: receipt), "\n"), concat(regen_round_changed_paths_line(receipt: receipt), "\n")), + concat(regen_round_partition_rebuild_line(receipt: receipt), "\n") + ) ) } diff --git a/dag/gunbc/stage0/stage0_partition_package_graph.dag b/dag/gunbc/stage0/stage0_partition_package_graph.dag new file mode 100644 index 00000000000..77b5571f0a0 --- /dev/null +++ b/dag/gunbc/stage0/stage0_partition_package_graph.dag @@ -0,0 +1,55 @@ +module gunbc.stage0_partition_package_graph + +import std.types { String, List, Bool } +import gunbc.stage0_crate_partition_generated { + GeneratedEmitCoreCrate, + GeneratedFoundationCrate, + GeneratedLayeredCoreCrate, + GeneratedPartitionCrateRow, + generated_partition_crate_rows +} + +// THE PACKAGE DEPENDENCY GRAPH OF THE STAGE0 PARTITION, in one place, as package NAMES. +// +// Two consumers ask the same question and must not answer it twice. v1.compiler.stage0_crates +// renders each partition crate's Cargo manifest from these edges, and +// gunbc.stage0_partition_rebuild_scope walks them in reverse to decide which packages one changed +// mirror forces cargo to recompile. If the renderer kept its own copy, the selector could compute +// a closure over edges cargo was never handed -- and the failure direction is the dangerous one: +// a missing edge NARROWS the closure, so a package that must recompile is skipped and the next +// pass runs stale code. One authority makes that state unwritable rather than checked. +// +// NAMES, NOT MANIFEST ROWS, because the two consumers need different things from an edge: the +// renderer needs a LocalPathDep carrying the sibling path it resolves from the row's crate_dir, +// and the selector needs only the identity of the package on the other end. Publishing the +// manifest row here would push the renderer's transport into the graph (DESIGN section 3: +// interface, realization and policy are three facts), and publishing the path would make the +// selector carry a directory it has no use for. +// +// THE KIND DECIDES THE EDGES, and each arm is the rule the partition already lives by: +// a foundation crate is the bottom of the layering and depends on no partition package; a +// layered core crate depends on exactly the packages it re-exports, which is what makes the +// `crate::` references in its included mirrors keep resolving; and the emit shell re-exports a +// SURFACE assembled from every module-bearing package, so it depends on all of them regardless +// of which modules it names. +fn stage0_partition_row_is_module_bearing_package(row: GeneratedPartitionCrateRow) -> Bool { + match row.kind { + GeneratedEmitCoreCrate => false + GeneratedFoundationCrate => true + GeneratedLayeredCoreCrate => true + } +} + +fn stage0_partition_module_bearing_package_names() -> List { + generated_partition_crate_rows + |> filter(row => stage0_partition_row_is_module_bearing_package(row: row)) + |> map(row => row.package_name) +} + +fn stage0_partition_package_dependency_names(row: GeneratedPartitionCrateRow) -> List { + match row.kind { + GeneratedFoundationCrate => [] + GeneratedLayeredCoreCrate => row.reexport_packages + GeneratedEmitCoreCrate => stage0_partition_module_bearing_package_names() + } +} diff --git a/dag/gunbc/stage0_partition_rebuild_scope.dag b/dag/gunbc/stage0_partition_rebuild_scope.dag new file mode 100644 index 00000000000..904b4efb7f4 --- /dev/null +++ b/dag/gunbc/stage0_partition_rebuild_scope.dag @@ -0,0 +1,394 @@ +module gunbc.stage0_partition_rebuild_scope + +import std.types { String, List, Bool } +import std.dissolution { DissolutionCondition, unbound_dissolution } +import gunbc.stage0_crate_partition_generated { + GeneratedPartitionCrateRow, + generated_partition_crate_rows +} +import gunbc.stage0_partition_package_graph { + stage0_partition_package_dependency_names, + stage0_partition_row_is_module_bearing_package +} + +// WHICH PACKAGES ONE CHANGED MIRROR MAKES cargo COMPILE, and the refusals that keep the answer +// from becoming "all of them". This is the rebuild half of the change-denominated regen round: +// gunbc.regen_affected_set bounds which MIRRORS an edit can change, and this module bounds which +// PACKAGES those changed mirrors force the next-pass compiler to rebuild. The two are different +// questions over different graphs -- one over .dag module references, one over cargo packages -- +// and the second is the larger of the two costs on every measured host +// (gunbc.regen_round_cost regen_round_cost_shape_note). +// +// THE CLAIM IS NOT "ONE MIRROR, ONE CRATE". A foundation edit legitimately rebuilds several +// downstream packages; the guarantee is that an UNRELATED package stays uncompiled, and that +// the executable the second generation runs is the one built from the installed mirrors. So the +// answer is the exact reverse package-dependency closure of the changed mirrors' owning +// packages, plus the executable/link package -- never a count, and never a superset. +// +// THE PACKAGE GRAPH IS DERIVED, NOT TRANSCRIBED. Every edge read here comes from +// gunbc.stage0_partition_package_graph stage0_partition_package_dependency_names, which is also +// what v1.compiler.stage0_crates renders each partition crate's Cargo manifest from. One graph, +// two consumers: the closure cannot disagree with what cargo is handed. A second hand-authored +// edge roster here would be a parallel authority for the same fact, and one that silently +// NARROWS -- a missing edge would drop a package that must recompile, which is a worse failure +// than widening because the next pass would then run stale code. +// +// REFUSE, NEVER WIDEN (DESIGN section 5, the absorbing fallback). Every arm below that cannot +// compute the precise package set REFUSES with the mirror or package it could not answer for. +// "Could not determine the owner" is not "build everything": collapsing them would destroy the +// only signal that the ownership roster has a hole, and the hole would then never rank for +// filling. WholeCompilerRebuildRequired is reserved for a POSITIVELY MODELED class -- an edit to +// an input every package's build is made of -- and is reached by naming that input, never by +// failing to name something else. + +// A changed mirror is a FILE basename as the regen compares it ("std_effects.rs"); a partition +// row owns MODULE basenames ("std_effects"). The join is the file suffix, applied in one +// direction only: a row's module name is extended to the file it owns, so no path string is ever +// taken apart and a mirror whose name is not `.rs` simply has no owner and refuses. +fn rebuild_scope_mirror_of_module(module_basename: String) -> String { + concat(module_basename, ".rs") +} + +// WHY "UNAVAILABLE" IS A DERIVED STATE AND NOT A REFUSAL. The scope question -- which packages +// would have to be compiled -- is answerable today and its answer is useful today. What is not +// available is the ACTUATION: there is no executable that could be assembled from those packages +// and run as the next generation. Collapsing the two would throw away a computed fact because a +// different fact is missing, and it would leave nothing for the projection into the round-cost +// receipt to report. So the scope is derived and the assembly carries its own typed absence, with +// the CAPABILITY that ends it named on the row rather than a date or an artifact. +type ExecutableAssemblyTrigger + = PartitionedClaimExecutorAssembly { capability: String } + +type PartitionExecutableAssembly + = PartitionAssembledExecutable { package_name: String, bin_name: String } + | ExecutableAssemblyUnavailable { trigger: ExecutableAssemblyTrigger, detail: String } + +type WholeCompilerRebuildCause + = PartitionGenerationAuthorityChanged { mirrors: List } + | SharedCompilerBuildInputChanged { mirrors: List } + +type RebuildScopeRefusalCause + = ChangedMirrorUnlocatable { mirrors: List, reason: String } + | MirrorHasNoOwningPackage { mirror: String } + | MirrorHasMultipleOwningPackages { mirror: String, packages: List } + | PackageDependencyClosureUnderivable { package_name: String, detail: String } + | ExecutableAssemblyNotCovered { executable_package: String, detail: String } + +type PartitionRebuildDecision + = PartitionRebuildScopeDerived { + changed_mirrors: List + owning_packages: List + package_closure: List + executable_assembly: PartitionExecutableAssembly + } + | WholeCompilerRebuildRequired { cause: WholeCompilerRebuildCause } + | RebuildScopeRefused { cause: RebuildScopeRefusalCause } + +// THE TWO POSITIVELY MODELED WHOLE-BUILD CLASSES, each named by the mirror whose bytes decide +// every package's build rather than any package's contents. +// +// (1) The partition generation authority. gunbc_stage0_crate_partition_generated.rs carries the +// ownership rows themselves and v1_compiler_stage0_crates.rs renders every manifest and lib +// from them, so a change to either can move which package any module compiles in. No reverse +// package closure can see that: the closure is computed FROM those rows. +// (2) The shared compiler build input. gunbc_stage0_crate_layout_generated.rs decides the +// monolith's declaration surface and v1_compiler_workspace_members.rs decides the workspace +// membership region, so a change to either re-plans the build for every package at once. +// +// A mirror that merely sits low in the graph -- std_types.rs, v1_rt.rs -- is deliberately NOT on +// these rosters: its blast radius IS its reverse package closure, which the partition arm +// computes exactly, and listing it here would be the absorbing fallback wearing a roster's name. +data rebuild_scope_partition_generation_mirrors: List = [ + "gunbc_stage0_crate_partition_generated.rs", + "v1_compiler_stage0_crates.rs" +] + +data rebuild_scope_shared_build_input_mirrors: List = [ + "gunbc_stage0_crate_layout_generated.rs", + "v1_compiler_workspace_members.rs" +] + +// THE EXECUTABLE THE SECOND GENERATION RUNS, stated as a typed state rather than assumed. +// +// TODAY THERE IS NONE, and saying so is the whole point of the row. claim_executor is a [[bin]] +// of the v1-compiler package, whose library declares the entire stage0 population -- including +// every module the partition crates also compile by path include. Compiling an owning partition +// crate and then running that binary would build one subset for validation and run a compiler +// built from a second declaration of the same mirrors, so it is not the fixed-point proof the +// round claims to perform and this module must not let a consumer mistake it for one. +fn stage0_next_pass_executable_assembly() -> PartitionExecutableAssembly { + ExecutableAssemblyUnavailable { + trigger: PartitionedClaimExecutorAssembly { + capability: "building and running the next-pass claim_executor from partition artifacts alone, with every module in its closure owned by exactly one admitted assembly surface" + }, + detail: "claim_executor is a bin of the monolithic v1-compiler package, whose library declares the partition-owned mirrors a second time; no executable can be assembled from the partition packages alone" + } +} + +// THE ONE PREDICATE A HOST MAY ACTUATE ON. A derived scope whose assembly is unavailable says +// exactly which packages would be compiled and exactly why they cannot become the next compiler, +// so a probe may build them and must terminate as packages-built-executable-unassembled: it may +// not feed the fixed-point receipt or the second pass. Making that a typed question the host asks +// -- rather than a sentence in a comment the host is trusted to have read -- is what keeps +// "we derived the scope" from being spent as "we rebuilt from it". +fn stage0_partition_rebuild_is_actuatable(decision: PartitionRebuildDecision) -> Bool { + match decision { + PartitionRebuildScopeDerived { changed_mirrors: _, owning_packages: _, package_closure: _, executable_assembly: assembly } => + match assembly { + PartitionAssembledExecutable { package_name: _, bin_name: _ } => true + ExecutableAssemblyUnavailable { trigger: _, detail: _ } => false + } + WholeCompilerRebuildRequired { cause: _ } => false + RebuildScopeRefused { cause: _ } => false + } +} + +fn rebuild_scope_list_has(items: List, item: String) -> Bool { + items |> any(x => x == item) +} + +fn rebuild_scope_push_new(items: List, item: String) -> List { + if rebuild_scope_list_has(items: items, item: item) { items } else { list_push(items, item) } +} + +fn rebuild_scope_intersect(left: List, right: List) -> List { + left |> filter(x => rebuild_scope_list_has(items: right, item: x)) +} + +// AN EDGE WHOSE OTHER END IS NOT A PACKAGE OF THIS PARTITION, or nothing. The graph names +// packages; a name that no row answers to is an edge the closure cannot follow, and following it +// silently would drop whatever sits beyond it. Taken over every row BEFORE any closure step so +// the refusal names the package whose edges could not be resolved, rather than whichever walk +// step happened to notice. +fn rebuild_scope_first_underivable(rows: List) -> List { + rows |> fold(init: [], f: fn(acc, row) { + stage0_partition_package_dependency_names(row: row) |> fold(init: acc, f: fn(inner, dep) { + if count(inner) > 0 { + inner + } else if rows |> any(candidate => candidate.package_name == dep) { + inner + } else { + list_push(inner, dep) + } + }) + }) +} + +fn rebuild_scope_row_deps_or_empty(row: GeneratedPartitionCrateRow) -> List { + stage0_partition_package_dependency_names(row: row) +} + +// ONE STEP of the reverse package walk: a package joins the set when any of its own path +// dependencies is already in it. Guarded by rebuild_scope_first_underivable, so an empty +// dependency list here means the package genuinely has no path dependencies. +fn rebuild_scope_reverse_step( + reached: List, + rows: List +) -> List { + rows |> fold(init: reached, f: fn(acc, row) { + let deps = rebuild_scope_row_deps_or_empty(row: row) + if count(rebuild_scope_intersect(left: deps, right: acc)) > 0 { + rebuild_scope_push_new(items: acc, item: row.package_name) + } else { + acc + } + }) +} + +// The fixpoint by construction: a package dependency path over n packages has at most n edges, +// so n steps reach it (DESIGN section 4 -- bounded and forward, no discovered termination). +fn rebuild_scope_reverse_closure( + seeds: List, + rows: List +) -> List { + rows |> fold(init: seeds, f: fn(acc, row) { rebuild_scope_reverse_step(reached: acc, rows: rows) }) +} + +fn rebuild_scope_owners_of_mirror( + mirror: String, + rows: List +) -> List { + rows + |> filter(row => stage0_partition_row_is_module_bearing_package(row: row)) + |> filter(row => row.modules |> any(m => rebuild_scope_mirror_of_module(module_basename: m) == mirror)) + |> map(row => row.package_name) +} + +// EXACTLY ONE OWNER, or a refusal naming which way the roster is wrong. The emit path's +// stage0_lookup_module_owner_package_name answers the same ownership question permissively -- +// it folds a multi-row match down to the first -- because at emit time a duplicate would show +// up as a duplicated `pub mod` and fail to compile. A rebuild SELECTOR has no such backstop: a +// duplicated owner would silently pick one package and leave the other stale, so this consumer +// needs the refusing form. See rebuild_scope_owner_resolution_dissolve_on. +type OwnerResolution + = OwnerResolved { packages: List } + | OwnerRefused { cause: RebuildScopeRefusalCause } + +fn rebuild_scope_resolve_owners( + changed_mirrors: List, + rows: List +) -> OwnerResolution { + changed_mirrors |> fold(init: OwnerResolved { packages: [] }, f: fn(acc, mirror) { + match acc { + OwnerRefused { cause: c } => OwnerRefused { cause: c } + OwnerResolved { packages: found } => { + let owners = rebuild_scope_owners_of_mirror(mirror: mirror, rows: rows) + if count(owners) == 0 { + OwnerRefused { cause: MirrorHasNoOwningPackage { mirror: mirror } } + } else if count(owners) > 1 { + OwnerRefused { cause: MirrorHasMultipleOwningPackages { mirror: mirror, packages: owners } } + } else { + OwnerResolved { + packages: owners |> fold(init: found, f: fn(inner, pkg) { + rebuild_scope_push_new(items: inner, item: pkg) + }) + } + } + } + } + }) +} + +fn rebuild_scope_changed_in_roster(changed_mirrors: List, roster: List) -> List { + changed_mirrors |> filter(m => rebuild_scope_list_has(items: roster, item: m)) +} + +// THE DECISION. `unlocatable` is the host's list of changed paths it could not name as a compared +// mirror; a non-empty list refuses before anything else is asked, exactly as +// gunbc.regen_affected_set EditedSetUnlocatable does, and for the same reason. +fn stage0_partition_rebuild_decision( + changed_mirrors: List, + unlocatable: List, + rows: List, + executable: PartitionExecutableAssembly +) -> PartitionRebuildDecision { + if count(unlocatable) > 0 { + RebuildScopeRefused { + cause: ChangedMirrorUnlocatable { + mirrors: unlocatable, + reason: "a changed path could not be named as a compared mirror; the rebuild scope cannot answer, and it does not widen to the whole compiler" + } + } + } else { + let generation = rebuild_scope_changed_in_roster(changed_mirrors: changed_mirrors, roster: rebuild_scope_partition_generation_mirrors) + let shared = rebuild_scope_changed_in_roster(changed_mirrors: changed_mirrors, roster: rebuild_scope_shared_build_input_mirrors) + if count(generation) > 0 { + WholeCompilerRebuildRequired { cause: PartitionGenerationAuthorityChanged { mirrors: generation } } + } else if count(shared) > 0 { + WholeCompilerRebuildRequired { cause: SharedCompilerBuildInputChanged { mirrors: shared } } + } else { + let underivable = rebuild_scope_first_underivable(rows: rows) + if count(underivable) > 0 { + RebuildScopeRefused { + cause: PackageDependencyClosureUnderivable { + package_name: underivable |> fold(init: "", f: fn(acc, p) { if acc == "" { p } else { acc } }), + detail: "a package dependency edge names a package no partition row answers to, so the reverse package closure is not derivable" + } + } + } else { + match rebuild_scope_resolve_owners(changed_mirrors: changed_mirrors, rows: rows) { + OwnerRefused { cause: c } => RebuildScopeRefused { cause: c } + OwnerResolved { packages: owners } => { + let closure = rebuild_scope_reverse_closure(seeds: owners, rows: rows) + match executable { + ExecutableAssemblyUnavailable { trigger: t, detail: d } => + PartitionRebuildScopeDerived { + changed_mirrors: changed_mirrors, + owning_packages: owners, + package_closure: closure, + executable_assembly: ExecutableAssemblyUnavailable { trigger: t, detail: d } + } + PartitionAssembledExecutable { package_name: p, bin_name: b } => + if !rebuild_scope_list_has(items: closure, item: p) { + RebuildScopeRefused { + cause: ExecutableAssemblyNotCovered { + executable_package: p, + detail: "the declared next-pass executable package is not reached from the changed mirrors' owning packages, so relinking it would not carry the changed bytes" + } + } + } else { + PartitionRebuildScopeDerived { + changed_mirrors: changed_mirrors, + owning_packages: owners, + package_closure: closure, + executable_assembly: PartitionAssembledExecutable { package_name: p, bin_name: b } + } + } + } + } + } + } + } + } +} + +fn stage0_partition_rebuild_decision_today( + changed_mirrors: List, + unlocatable: List +) -> PartitionRebuildDecision { + stage0_partition_rebuild_decision( + changed_mirrors: changed_mirrors, + unlocatable: unlocatable, + rows: generated_partition_crate_rows, + executable: stage0_next_pass_executable_assembly() + ) +} + +// THE PACKAGES NOT COMPILED. Reported beside the closure because the guarantee this module makes +// is an EXCLUSION -- an unrelated package stays uncompiled -- and an exclusion no consumer can +// read is not evidence of anything. +fn rebuild_scope_excluded_packages( + closure: List, + rows: List +) -> List { + rows + |> map(row => row.package_name) + |> filter(p => !rebuild_scope_list_has(items: closure, item: p)) +} + +fn rebuild_scope_join(items: List) -> String { + items |> fold(init: "", f: fn(acc, item) { + if acc == "" { item } else { concat(concat(acc, ", "), item) } + }) +} + +// THE PACKAGE IDENTITY ROSTER, NOT A COUNT. `rebuild_compiled_crates=3` is display; the roster +// below is the evidence, so every line names the packages it is talking about. +fn stage0_partition_rebuild_decision_line(decision: PartitionRebuildDecision) -> String { + match decision { + PartitionRebuildScopeDerived { changed_mirrors: m, owning_packages: o, package_closure: c, executable_assembly: assembly } => + concat( + concat(concat("partition-rebuild: PartitionRebuildScopeDerived changed_mirrors=[", rebuild_scope_join(items: m)), "]"), + concat( + concat(concat(" owning_packages=[", rebuild_scope_join(items: o)), "]"), + concat( + concat(concat(" package_closure=[", rebuild_scope_join(items: c)), "]"), + match assembly { + PartitionAssembledExecutable { package_name: p, bin_name: b } => + concat(concat(" executable_assembly=assembled package=", p), concat(" bin=", b)) + ExecutableAssemblyUnavailable { trigger: PartitionedClaimExecutorAssembly { capability: _ }, detail: _ } => + " executable_assembly=unavailable trigger=PartitionedClaimExecutorAssembly" + } + ) + ) + ) + WholeCompilerRebuildRequired { cause: PartitionGenerationAuthorityChanged { mirrors: ms } } => + concat("partition-rebuild: WholeCompilerRebuildRequired partition_generation_authority=[", concat(rebuild_scope_join(items: ms), "]")) + WholeCompilerRebuildRequired { cause: SharedCompilerBuildInputChanged { mirrors: ms } } => + concat("partition-rebuild: WholeCompilerRebuildRequired shared_build_input=[", concat(rebuild_scope_join(items: ms), "]")) + RebuildScopeRefused { cause: ChangedMirrorUnlocatable { mirrors: ms, reason: r } } => + concat(concat("partition-rebuild: RebuildScopeRefused ChangedMirrorUnlocatable mirrors=[", rebuild_scope_join(items: ms)), concat("] reason=", r)) + RebuildScopeRefused { cause: MirrorHasNoOwningPackage { mirror: m } } => + concat("partition-rebuild: RebuildScopeRefused MirrorHasNoOwningPackage mirror=", m) + RebuildScopeRefused { cause: MirrorHasMultipleOwningPackages { mirror: m, packages: ps } } => + concat(concat("partition-rebuild: RebuildScopeRefused MirrorHasMultipleOwningPackages mirror=", m), concat(" packages=[", concat(rebuild_scope_join(items: ps), "]"))) + RebuildScopeRefused { cause: PackageDependencyClosureUnderivable { package_name: p, detail: d } } => + concat(concat("partition-rebuild: RebuildScopeRefused PackageDependencyClosureUnderivable package=", p), concat(" detail=", d)) + RebuildScopeRefused { cause: ExecutableAssemblyNotCovered { executable_package: p, detail: d } } => + concat(concat("partition-rebuild: RebuildScopeRefused ExecutableAssemblyNotCovered executable_package=", p), concat(" detail=", d)) + } +} + +data rebuild_scope_owner_resolution_dissolve_on: DissolutionCondition = unbound_dissolution(description: "dissolve-on: rebuild_scope_resolve_owners — repoint v1.compiler.stage0_crates stage0_lookup_module_owner_package_name at this refusing resolver so one answer serves both consumers. Held back here because that lookup is on the partition EMIT path, where a duplicate owner is already caught by rustc as a duplicated pub mod, and moving it is a change to a load-bearing generated-artifact producer rather than to this selector.") + +data rebuild_scope_executable_assembly_dissolve_on: DissolutionCondition = unbound_dissolution(description: "dissolve-on: stage0_next_pass_executable_assembly — flips ExecutableAssemblyUnavailable to PartitionAssembledExecutable when the CAPABILITY named by its trigger exists: building and running the next-pass claim_executor from partition artifacts alone, with every module in its closure owned by exactly one admitted assembly surface and the monolithic v1-compiler library no longer declaring the partition-owned mirrors a second time. Until then stage0_partition_rebuild_is_actuatable is false for every live decision, and the authoritative rebuild_from_installed path stays the monolithic build.") diff --git a/dag/test/claim/self_host_regen_round_cost_witness_test.dag b/dag/test/claim/self_host_regen_round_cost_witness_test.dag index 2a173e9c3eb..9afd7fca9c5 100644 --- a/dag/test/claim/self_host_regen_round_cost_witness_test.dag +++ b/dag/test/claim/self_host_regen_round_cost_witness_test.dag @@ -48,7 +48,8 @@ fn a_receipt(marks: List) -> RegenRoundCostReceipt { rebuild_compiled_crates: 2, rustfmt_spawns: 7, marks: marks, - changed_paths: ["v1_rt.rs", "v1_compiler_runtime_rust.rs"] + changed_paths: ["v1_rt.rs", "v1_compiler_runtime_rust.rs"], + installed_mirrors: ["v1_rt.rs"] } } @@ -139,7 +140,10 @@ test fn a_receipt_renders_provenance_marks_totals_and_changed_paths() -> Bool { "regen-round-cost: producer=claim_executor --regen-round-cost host=srv1 tree=2a11b317d2caf3c37d1d38a4421e8e0c06188925 tree_dirty=false seed_build_compiled_crates=0 rebuild_compiled_crates=2 rustfmt_spawns=7\n", concat( "regen-round-cost: phase=seed_build wall_ms=1500 cpu_ms=9000\nregen-round-cost: phase=compile.emit wall_ms=300000 cpu_ms=299000\n", - "regen-round-cost: total wall_ms=301500 cpu_ms=308000\nregen-round-cost: changed_paths=2 [v1_rt.rs, v1_compiler_runtime_rust.rs]\n" + concat( + "regen-round-cost: total wall_ms=301500 cpu_ms=308000\nregen-round-cost: changed_paths=2 [v1_rt.rs, v1_compiler_runtime_rust.rs]\n", + "partition-rebuild: PartitionRebuildScopeDerived changed_mirrors=[v1_rt.rs] owning_packages=[v1-stage0-runtime] package_closure=[v1-stage0-runtime, v1-stage0-std-core, v1-stage0-std-surface, v1-stage0-extdeps-languages, v1-stage0-v1-artifact, v1-stage0-v1-infer, v1-stage0-emit-core] executable_assembly=unavailable trigger=PartitionedClaimExecutorAssembly\n" + ) ) ) } diff --git a/src/v1/stage0/src/emitted_population.rs b/src/v1/stage0/src/emitted_population.rs index 20bc0c7ef38..211f9f24700 100644 --- a/src/v1/stage0/src/emitted_population.rs +++ b/src/v1/stage0/src/emitted_population.rs @@ -36,6 +36,7 @@ // src/gunbc_stage0_crate_layout_generated.rs // src/gunbc_stage0_crate_partition_generated.rs // src/gunbc_stage0_emitted_population_manifest.rs +// src/gunbc_stage0_partition_package_graph.rs // src/lib.rs // src/main.rs // src/std_algebra.rs diff --git a/src/v1/stage0/src/gunbc_stage0_partition_package_graph.rs b/src/v1/stage0/src/gunbc_stage0_partition_package_graph.rs new file mode 100644 index 00000000000..a480cd01b9c --- /dev/null +++ b/src/v1/stage0/src/gunbc_stage0_partition_package_graph.rs @@ -0,0 +1,59 @@ +// Generated by v1 compiler -- do not edit. +// Source module: gunbc.stage0_partition_package_graph + +pub use crate::gunbc_stage0_crate_partition_generated::generated_partition_crate_rows; +use crate::gunbc_stage0_crate_partition_generated::GeneratedPartitionCrateKind::{ + GeneratedEmitCoreCrate, GeneratedFoundationCrate, GeneratedLayeredCoreCrate, +}; +pub use crate::gunbc_stage0_crate_partition_generated::{ + GeneratedPartitionCrateKind, GeneratedPartitionCrateRow, +}; +use crate::std_types::Bool::*; +pub use crate::std_types::{Bool, List}; +use crate::v1_rt; +use crate::v1_rt::{VecCompat, VecJoin}; +use crate::NonEmptyBTreeSet; +use crate::NonEmptyVec; +use im::{vector as vec, HashMap, OrdSet as BTreeSet, Vector as Vec}; +use std::rc::Rc; + +pub fn stage0_partition_row_is_module_bearing_package(row: Rc) -> bool { + match row.kind.clone() { + GeneratedPartitionCrateKind::GeneratedEmitCoreCrate => false, + GeneratedPartitionCrateKind::GeneratedFoundationCrate => true, + GeneratedPartitionCrateKind::GeneratedLayeredCoreCrate => true, + } +} + +pub fn stage0_partition_module_bearing_package_names() -> Rc> { + Rc::new({ + let mut __result = Vec::new(); + for row in Rc::new({ + let mut __result = Vec::new(); + for row in generated_partition_crate_rows().iter().cloned() { + if stage0_partition_row_is_module_bearing_package(row.clone()) { + __result.push(row); + } + } + __result + }) + .iter() + .cloned() + { + __result.push(row.package_name.clone()); + } + __result + }) +} + +pub fn stage0_partition_package_dependency_names( + row: Rc, +) -> Rc> { + match row.kind.clone() { + GeneratedPartitionCrateKind::GeneratedFoundationCrate => Rc::new(vec![]), + GeneratedPartitionCrateKind::GeneratedLayeredCoreCrate => row.reexport_packages.clone(), + GeneratedPartitionCrateKind::GeneratedEmitCoreCrate => { + stage0_partition_module_bearing_package_names() + } + } +} diff --git a/src/v1/stage0/src/lib.rs b/src/v1/stage0/src/lib.rs index d5e60864f72..004f4d43607 100644 --- a/src/v1/stage0/src/lib.rs +++ b/src/v1/stage0/src/lib.rs @@ -50,6 +50,7 @@ pub mod gunbc_rust_source_type_bindings; pub mod gunbc_stage0_crate_layout_generated; pub mod gunbc_stage0_crate_partition_generated; pub mod gunbc_stage0_emitted_population_manifest; +pub mod gunbc_stage0_partition_package_graph; pub mod std_algebra; pub mod std_checked_arithmetic; pub mod std_coercion; diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index 1f022fdad55..69432e320d3 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -2902,6 +2902,7 @@ fn render_round_cost_receipt( rustfmt_spawns: u64, marks: &[v1_rt::TraceLedgerRow], changed_paths: &[String], + installed_mirrors: &[String], ) -> Result { use crate::v1_interpreter::{self, str_value, ExecutionMode, Value}; let entry = round_cost_entry(source_roots)?; @@ -2971,6 +2972,10 @@ fn render_round_cost_receipt( }) .collect(); let path_values: Vec = changed_paths.iter().map(str_value).collect(); + // The regen's own drift answer, at the grain it installed: `gunbc.stage0_partition_rebuild_scope` + // reads it to derive which packages a change-denominated rebuild would compile. Passed as data, + // never re-derived here — the host owns no part of that decision. + let installed_values: Vec = installed_mirrors.iter().map(str_value).collect(); let receipt = Value::Record { type_name: ctx.sym("RegenRoundCostReceipt"), fields: Rc::new(vec![ @@ -2992,6 +2997,10 @@ fn render_round_cost_receipt( ctx.sym("changed_paths"), Value::List(Rc::new(path_values.into())), ), + ( + ctx.sym("installed_mirrors"), + Value::List(Rc::new(installed_values.into())), + ), ]), }; let args = vec![(Some("receipt".to_string()), receipt)]; @@ -3077,7 +3086,12 @@ pub fn run_regen_round_cost( let mut rebuild_compiled_crates = 0; let mut changed_paths: Vec = Vec::new(); + // The drift answer is kept past the install because it is the rebuild scope's input: the + // receipt reports which packages a change-denominated rebuild of THESE mirrors would compile, + // beside what the whole-crate rebuild above actually cost. + let mut installed_mirrors: Vec = Vec::new(); if let Some(drifted) = drifted { + installed_mirrors = drifted.clone(); v1_rt::trace_mark("round.install.begin".to_string()); install_candidate_paths(&candidate_src, &stage0_src, &drifted)?; v1_rt::trace_mark("round.install.done".to_string()); @@ -3116,6 +3130,7 @@ pub fn run_regen_round_cost( rustfmt_spawn_count() - rustfmt_spawns_before, &marks, &changed_paths, + &installed_mirrors, )?; let receipt_path = workspace.join(REGEN_ROUND_COST_RECEIPT_REL); if let Some(parent) = receipt_path.parent() { @@ -3168,6 +3183,7 @@ mod regen_round_cost_tests { 7, &marks, &["v1_rt.rs".to_string()], + &["v1_rt.rs".to_string()], ) .expect("the model renders a host-built receipt"); assert_eq!( @@ -3178,7 +3194,13 @@ mod regen_round_cost_tests { regen-round-cost: phase=seed_build wall_ms=1500 cpu_ms=9000\n\ regen-round-cost: phase=compile.emit wall_ms=300000 cpu_ms=na\n\ regen-round-cost: total wall_ms=301500 cpu_ms=na\n\ - regen-round-cost: changed_paths=1 [v1_rt.rs]\n" + regen-round-cost: changed_paths=1 [v1_rt.rs]\n\ + partition-rebuild: PartitionRebuildScopeDerived changed_mirrors=[v1_rt.rs] \ + owning_packages=[v1-stage0-runtime] \ + package_closure=[v1-stage0-runtime, v1-stage0-std-core, v1-stage0-std-surface, \ + v1-stage0-extdeps-languages, v1-stage0-v1-artifact, v1-stage0-v1-infer, \ + v1-stage0-emit-core] \ + executable_assembly=unavailable trigger=PartitionedClaimExecutorAssembly\n" ); } diff --git a/src/v1/stage0/src/v1_compiler_stage0_crates.rs b/src/v1/stage0/src/v1_compiler_stage0_crates.rs index 3efbd60390b..1168bcaa916 100644 --- a/src/v1/stage0/src/v1_compiler_stage0_crates.rs +++ b/src/v1/stage0/src/v1_compiler_stage0_crates.rs @@ -24,6 +24,9 @@ use crate::gunbc_stage0_crate_partition_generated::GeneratedPartitionCrateKind:: pub use crate::gunbc_stage0_crate_partition_generated::{ GeneratedPartitionCrateKind, GeneratedPartitionCrateRow, }; +pub use crate::gunbc_stage0_partition_package_graph::{ + stage0_partition_package_dependency_names, stage0_partition_row_is_module_bearing_package, +}; pub use crate::std_dissolution::unbound_dissolution; pub use crate::std_dissolution::DissolutionCondition; use crate::std_dissolution::DissolutionCondition::*; @@ -130,14 +133,6 @@ pub fn stage0_crate_dir_to_sibling_dep_path(crate_dir: String) -> String { v1_rt::replace(crate_dir.clone(), "src/v1/".to_string(), "../".to_string()) } -pub fn stage0_partition_row_is_module_bearing(row: Rc) -> bool { - match row.kind.clone() { - GeneratedPartitionCrateKind::GeneratedEmitCoreCrate => false, - GeneratedPartitionCrateKind::GeneratedFoundationCrate => true, - GeneratedPartitionCrateKind::GeneratedLayeredCoreCrate => true, - } -} - #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(tag = "_variant")] pub enum Stage0ModuleOwnerRefusalCause { @@ -283,18 +278,7 @@ pub fn stage0_lookup_module_owner_package_name( let matches = Rc::new({ let mut __result = Vec::new(); for row in generated_partition_crate_rows().iter().cloned() { - if (stage0_partition_row_is_module_bearing(row.clone()) && { - let mut __found = false; - for m in row.modules.clone().iter().cloned() { - if (m.clone() == module_basename.clone()) { - __found = true; - break; - } - } - __found - }) { - __result.push(row); - } + if (crate::gunbc_stage0_partition_package_graph::stage0_partition_row_is_module_bearing_package(row.clone()) && { let mut __found = false; for m in row.modules.clone().iter().cloned() { if (m.clone() == module_basename.clone()) { __found = true; break; } } __found }) { __result.push(row); } } __result }); @@ -463,32 +447,6 @@ pub fn stage0_foundation_runtime_dependencies() -> Rc>> ]) } -pub fn stage0_emit_shell_registry_dependencies() -> Rc>> { - Rc::new(vec![ - Rc::new(CargoDependency { - name: "stacker".to_string(), - source: Rc::new(CargoDepSource::RegistryDep { - version: "0.1".to_string(), - features: Rc::new(vec![]), - }), - }), - Rc::new(CargoDependency { - name: "im".to_string(), - source: Rc::new(CargoDepSource::RegistryDep { - version: "15.1".to_string(), - features: Rc::new(vec!["serde".to_string()]), - }), - }), - Rc::new(CargoDependency { - name: "serde".to_string(), - source: Rc::new(CargoDepSource::RegistryDep { - version: "1".to_string(), - features: Rc::new(vec!["derive".to_string(), "rc".to_string()]), - }), - }), - ]) -} - pub fn stage0_reexport_path_dependencies_outcome( reexport_packages: Rc>, ) -> Rc { @@ -534,32 +492,6 @@ pub fn stage0_reexport_path_dependencies_outcome( ) } -pub fn stage0_emit_shell_path_dependencies() -> Rc>> { - Rc::new({ - let mut __result = Vec::new(); - for row in Rc::new({ - let mut __result = Vec::new(); - for row in generated_partition_crate_rows().iter().cloned() { - if stage0_partition_row_is_module_bearing(row.clone()) { - __result.push(row); - } - } - __result - }) - .iter() - .cloned() - { - __result.push(Rc::new(CargoDependency { - name: row.package_name.clone(), - source: Rc::new(CargoDepSource::LocalPathDep { - path: stage0_crate_dir_to_sibling_dep_path(row.crate_dir.clone()), - }), - })); - } - __result - }) -} - pub fn stage0_partition_row_kind(row: Rc) -> Stage0CrateKind { match row.kind.clone() { GeneratedPartitionCrateKind::GeneratedFoundationCrate => Stage0CrateKind::FoundationCrate, @@ -579,42 +511,24 @@ pub fn stage0_partition_row_header_doc(row: Rc) -> S pub fn stage0_partition_row_dependencies_outcome( row: Rc, ) -> Rc { - match row.kind.clone() { - GeneratedPartitionCrateKind::GeneratedFoundationCrate => { - Rc::new(Stage0PartitionRowDepsOutcome::Stage0PartitionRowDepsOk { - deps: stage0_foundation_runtime_dependencies(), - }) - } - GeneratedPartitionCrateKind::GeneratedLayeredCoreCrate => { - match (*stage0_reexport_path_dependencies_outcome(row.reexport_packages.clone())) - .clone() - { - Stage0ReexportPathDepsOutcome::Stage0ReexportPathDepsOk { - deps: reexport_deps, - .. - } => Rc::new(Stage0PartitionRowDepsOutcome::Stage0PartitionRowDepsOk { - deps: v1_rt::concat( - stage0_foundation_runtime_dependencies(), - reexport_deps.clone(), - ), - }), - Stage0ReexportPathDepsOutcome::Stage0ReexportPathDepsRefused { - cause: cause, - .. - } => Rc::new( - Stage0PartitionRowDepsOutcome::Stage0PartitionRowDepsRefused { - cause: cause.clone(), - }, - ), - } - } - GeneratedPartitionCrateKind::GeneratedEmitCoreCrate => { - Rc::new(Stage0PartitionRowDepsOutcome::Stage0PartitionRowDepsOk { - deps: v1_rt::concat( - stage0_foundation_runtime_dependencies(), - stage0_emit_shell_path_dependencies(), - ), - }) + match (*stage0_reexport_path_dependencies_outcome( + crate::gunbc_stage0_partition_package_graph::stage0_partition_package_dependency_names( + row.clone(), + ), + )) + .clone() + { + Stage0ReexportPathDepsOutcome::Stage0ReexportPathDepsOk { + deps: path_deps, .. + } => Rc::new(Stage0PartitionRowDepsOutcome::Stage0PartitionRowDepsOk { + deps: v1_rt::concat(stage0_foundation_runtime_dependencies(), path_deps.clone()), + }), + Stage0ReexportPathDepsOutcome::Stage0ReexportPathDepsRefused { cause: cause, .. } => { + Rc::new( + Stage0PartitionRowDepsOutcome::Stage0PartitionRowDepsRefused { + cause: cause.clone(), + }, + ) } } } diff --git a/src/v1/stage0_crates.dag b/src/v1/stage0_crates.dag index 6df1dad9c18..d5f7b0ca2dd 100644 --- a/src/v1/stage0_crates.dag +++ b/src/v1/stage0_crates.dag @@ -6,6 +6,10 @@ import v1.std.core { TextFile } import v1.compiler.emit_rust { emit_non_empty_wrappers, emit_cargo_dep } import extdeps.cargo { CargoDependency, CargoFeature, RegistryDep, LocalPathDep } import extdeps.cargo_version { render_cargo_package_header_prefix } +import gunbc.stage0_partition_package_graph { + stage0_partition_package_dependency_names, + stage0_partition_row_is_module_bearing_package +} import gunbc.stage0_crate_partition_generated { GeneratedEmitCoreCrate, GeneratedFoundationCrate, @@ -114,14 +118,6 @@ fn stage0_crate_dir_to_sibling_dep_path(crate_dir: String) -> String { replace(crate_dir, "src/v1/", "../") } -fn stage0_partition_row_is_module_bearing(row: GeneratedPartitionCrateRow) -> Bool { - match row.kind { - GeneratedEmitCoreCrate => false - GeneratedFoundationCrate => true - GeneratedLayeredCoreCrate => true - } -} - type Stage0ModuleOwnerRefusalCause = Stage0ModuleOwnerMissing { module_basename: String } @@ -171,7 +167,7 @@ type Stage0CrateBoundaryEmitOutcome fn stage0_lookup_module_owner_package_name(module_basename: String) -> Stage0ModuleOwnerLookup { let matches = generated_partition_crate_rows |> filter(row => - stage0_partition_row_is_module_bearing(row: row) + stage0_partition_row_is_module_bearing_package(row: row) && (row.modules |> any(m => m == module_basename)) ) let package_name = matches @@ -246,14 +242,6 @@ fn stage0_foundation_runtime_dependencies() -> List { ] } -fn stage0_emit_shell_registry_dependencies() -> List { - [ - CargoDependency { name: "stacker", source: RegistryDep { version: "0.1", features: [] } }, - CargoDependency { name: "im", source: RegistryDep { version: "15.1", features: ["serde"] } }, - CargoDependency { name: "serde", source: RegistryDep { version: "1", features: ["derive", "rc"] } } - ] -} - fn stage0_reexport_path_dependencies_outcome( reexport_packages: List ) -> Stage0ReexportPathDepsOutcome { @@ -282,17 +270,6 @@ fn stage0_reexport_path_dependencies_outcome( }) } -fn stage0_emit_shell_path_dependencies() -> List { - generated_partition_crate_rows - |> filter(row => stage0_partition_row_is_module_bearing(row: row)) - |> map(row => - CargoDependency { - name: row.package_name, - source: LocalPathDep { path: stage0_crate_dir_to_sibling_dep_path(crate_dir: row.crate_dir) } - } - ) -} - fn stage0_partition_row_kind(row: GeneratedPartitionCrateRow) -> Stage0CrateKind { match row.kind { GeneratedFoundationCrate => FoundationCrate @@ -309,26 +286,24 @@ fn stage0_partition_row_header_doc(row: GeneratedPartitionCrateRow) -> String { } } +// EVERY PARTITION CRATE'S MANIFEST IS THE SAME TWO PARTS: the registry dependencies every stage0 +// crate carries, then one LocalPathDep per package edge. Which packages those are is not decided +// here -- gunbc.stage0_partition_package_graph owns the edges, and this function is the transport +// that turns each package name into the sibling path cargo reads. The three kinds used to fork +// here with the emit shell's rule written out a second time; they no longer differ at all, which +// is what having one graph means. fn stage0_partition_row_dependencies_outcome( row: GeneratedPartitionCrateRow ) -> Stage0PartitionRowDepsOutcome { - match row.kind { - GeneratedFoundationCrate => - Stage0PartitionRowDepsOk { deps: stage0_foundation_runtime_dependencies() } - GeneratedLayeredCoreCrate => - match stage0_reexport_path_dependencies_outcome(reexport_packages: row.reexport_packages) { - Stage0ReexportPathDepsOk { deps: reexport_deps } => - Stage0PartitionRowDepsOk { - deps: stage0_foundation_runtime_dependencies() |> concat(reexport_deps) - } - Stage0ReexportPathDepsRefused { cause: cause } => - Stage0PartitionRowDepsRefused { cause: cause } - } - GeneratedEmitCoreCrate => + match stage0_reexport_path_dependencies_outcome( + reexport_packages: stage0_partition_package_dependency_names(row: row) + ) { + Stage0ReexportPathDepsOk { deps: path_deps } => Stage0PartitionRowDepsOk { - deps: stage0_foundation_runtime_dependencies() - |> concat(stage0_emit_shell_path_dependencies()) + deps: stage0_foundation_runtime_dependencies() |> concat(path_deps) } + Stage0ReexportPathDepsRefused { cause: cause } => + Stage0PartitionRowDepsRefused { cause: cause } } } diff --git a/src/v2/test/claim/stage0_partition_rebuild_scope_witness_test.dag b/src/v2/test/claim/stage0_partition_rebuild_scope_witness_test.dag new file mode 100644 index 00000000000..9c4e582a117 --- /dev/null +++ b/src/v2/test/claim/stage0_partition_rebuild_scope_witness_test.dag @@ -0,0 +1,279 @@ +module v2.test.claim.stage0_partition_rebuild_scope_witness + +import std.types { Bool, List, String } +import gunbc.stage0_crate_partition_generated { + GeneratedFoundationCrate, + GeneratedPartitionCrateRow, + generated_partition_crate_rows +} +import gunbc.stage0_partition_rebuild_scope { + ChangedMirrorUnlocatable, + ExecutableAssemblyNotCovered, + MirrorHasMultipleOwningPackages, + MirrorHasNoOwningPackage, + ExecutableAssemblyUnavailable, + PackageDependencyClosureUnderivable, + PartitionAssembledExecutable, + PartitionGenerationAuthorityChanged, + PartitionRebuildDecision, + PartitionRebuildScopeDerived, + RebuildScopeRefused, + SharedCompilerBuildInputChanged, + WholeCompilerRebuildRequired, + rebuild_scope_excluded_packages, + stage0_partition_rebuild_is_actuatable, + stage0_partition_rebuild_decision, + stage0_partition_rebuild_decision_today, + stage0_next_pass_executable_assembly +} + +// THE DISCRIMINATING REDS THIS FILE HOLDS OPEN, each one a state the selector must not enter. +// +// 1. A leaf edit that compiles a package it has no path to. witness_leaf_edit_excludes_unrelated +// names v1-stage0-runtime and v1-stage0-std-core as UNCOMPILED for a std-surface edit; a +// selector that widened on any deficit would compile them and this goes red. This is the +// control the whole module exists for -- the guarantee is the exclusion, not the closure. +// 2. A closure hardcoded to the owner alone. witness_foundation_edit_widens_to_real_closure +// edits the foundation and requires MORE than one package, so "one mirror, one crate" fails. +// 3. Silence on an input every package is built from. Two rosters, two arms. +// 4/5. Ownership that cannot answer. A mirror with no owner and a mirror with two owners must +// refuse with DIFFERENT causes -- collapsing them would hide which way the roster is wrong. +// 6. A derived scope spent as a rebuild. The live decision derives real packages while its +// executable assembly is unavailable, so the pair witness_today_derives_scope_with_unavailable +// _assembly / witness_today_scope_is_not_actuatable holds those two facts apart, against a +// positive actuatability control so the predicate cannot be a constant false. +// +// The fixture executable below is a PARTITION-ASSEMBLED one because the positive arm has no live +// inhabitant yet (see rebuild_scope_executable_assembly_dissolve_on). It is a fixture handed to +// the selector, not a claim that the tree builds it -- and the selector is the subject, so a +// fixture is the right and only place its accepting arm can be exercised today. +fn witness_fixture_executable() -> PartitionAssembledExecutable { + PartitionAssembledExecutable { + package_name: "v1-stage0-emit-core", + bin_name: "claim_executor" + } +} + +fn witness_decide(changed: List) -> PartitionRebuildDecision { + stage0_partition_rebuild_decision( + changed_mirrors: changed, + unlocatable: [], + rows: generated_partition_crate_rows, + executable: witness_fixture_executable() + ) +} + +fn witness_closure_of(changed: List) -> List { + match witness_decide(changed: changed) { + PartitionRebuildScopeDerived { changed_mirrors: _, owning_packages: _, package_closure: c, executable_assembly: _ } => c + WholeCompilerRebuildRequired { cause: _ } => [] + RebuildScopeRefused { cause: _ } => [] + } +} + +fn witness_has(items: List, item: String) -> Bool { + items |> any(x => x == item) +} + +// CONTROL 1 -- leaf-partition edit: the owner, its real downstream closure, and the executable +// relink; two named unrelated packages stay uncompiled. +test fn witness_leaf_edit_compiles_owner_and_downstream_holds() -> Bool { + match witness_decide(changed: ["std_effects.rs"]) { + PartitionRebuildScopeDerived { changed_mirrors: _, owning_packages: o, package_closure: c, executable_assembly: assembly } => + (count(o) == 1) + && witness_has(items: o, item: "v1-stage0-std-surface") + && witness_has(items: c, item: "v1-stage0-std-surface") + && witness_has(items: c, item: "v1-stage0-extdeps-languages") + && witness_has(items: c, item: "v1-stage0-v1-artifact") + && witness_has(items: c, item: "v1-stage0-v1-infer") + && witness_has(items: c, item: "v1-stage0-emit-core") + && (match assembly { + PartitionAssembledExecutable { package_name: p, bin_name: _ } => p == "v1-stage0-emit-core" + ExecutableAssemblyUnavailable { trigger: _, detail: _ } => false + }) + WholeCompilerRebuildRequired { cause: _ } => false + RebuildScopeRefused { cause: _ } => false + } +} + +test fn witness_leaf_edit_excludes_unrelated_packages_holds() -> Bool { + let closure = witness_closure_of(changed: ["std_effects.rs"]) + let excluded = rebuild_scope_excluded_packages(closure: closure, rows: generated_partition_crate_rows) + (count(closure) > 0) + && witness_has(items: excluded, item: "v1-stage0-runtime") + && witness_has(items: excluded, item: "v1-stage0-std-core") + && !witness_has(items: closure, item: "v1-stage0-runtime") + && !witness_has(items: closure, item: "v1-stage0-std-core") +} + +// CONTROL 2 -- foundation edit: the derived closure is wider than the owner, so the selector is +// not hardcoded to one package. +test fn witness_foundation_edit_widens_to_derived_closure_holds() -> Bool { + let closure = witness_closure_of(changed: ["v1_rt.rs"]) + let excluded = rebuild_scope_excluded_packages(closure: closure, rows: generated_partition_crate_rows) + (count(closure) > 1) + && witness_has(items: closure, item: "v1-stage0-runtime") + && witness_has(items: closure, item: "v1-stage0-std-core") + && witness_has(items: closure, item: "v1-stage0-emit-core") + && (count(excluded) == 0) +} + +// The two arms of control 2 are only meaningful against each other: the leaf closure must be a +// strict subset of the foundation one, or "wider" is a word about nothing. +test fn witness_leaf_closure_is_narrower_than_foundation_holds() -> Bool { + count(witness_closure_of(changed: ["std_effects.rs"])) < count(witness_closure_of(changed: ["v1_rt.rs"])) +} + +// CONTROL 3 -- whole-build inputs, one arm per positively modeled class. +test fn witness_partition_generation_edit_requires_whole_rebuild_holds() -> Bool { + match witness_decide(changed: ["gunbc_stage0_crate_partition_generated.rs"]) { + WholeCompilerRebuildRequired { cause: PartitionGenerationAuthorityChanged { mirrors: ms } } => count(ms) == 1 + WholeCompilerRebuildRequired { cause: SharedCompilerBuildInputChanged { mirrors: _ } } => false + PartitionRebuildScopeDerived { changed_mirrors: _, owning_packages: _, package_closure: _, executable_assembly: _ } => false + RebuildScopeRefused { cause: _ } => false + } +} + +test fn witness_shared_build_input_edit_requires_whole_rebuild_holds() -> Bool { + match witness_decide(changed: ["v1_compiler_workspace_members.rs"]) { + WholeCompilerRebuildRequired { cause: SharedCompilerBuildInputChanged { mirrors: ms } } => count(ms) == 1 + WholeCompilerRebuildRequired { cause: PartitionGenerationAuthorityChanged { mirrors: _ } } => false + PartitionRebuildScopeDerived { changed_mirrors: _, owning_packages: _, package_closure: _, executable_assembly: _ } => false + RebuildScopeRefused { cause: _ } => false + } +} + +// CONTROL 4 -- a mirror the partition does not own refuses BEFORE any build, naming the mirror. +// cli_run.rs is a real hand-authored host module in the compared population and is owned by no +// partition row, so this is the live shape of the coverage hole, not an invented name. +test fn witness_unowned_mirror_refuses_holds() -> Bool { + match witness_decide(changed: ["cli_run.rs"]) { + RebuildScopeRefused { cause: MirrorHasNoOwningPackage { mirror: m } } => m == "cli_run.rs" + RebuildScopeRefused { cause: MirrorHasMultipleOwningPackages { mirror: _, packages: _ } } => false + RebuildScopeRefused { cause: ChangedMirrorUnlocatable { mirrors: _, reason: _ } } => false + RebuildScopeRefused { cause: PackageDependencyClosureUnderivable { package_name: _, detail: _ } } => false + RebuildScopeRefused { cause: ExecutableAssemblyNotCovered { executable_package: _, detail: _ } } => false + WholeCompilerRebuildRequired { cause: _ } => false + PartitionRebuildScopeDerived { changed_mirrors: _, owning_packages: _, package_closure: _, executable_assembly: _ } => false + } +} + +// CONTROL 5 -- two owners for one mirror refuses with its own cause. Fixture rows, because the +// live roster must never contain this state; a foundation kind is used so both rows' manifests +// resolve without a crate-dir lookup and the refusal under test is the ownership one. +fn witness_duplicate_owner_rows() -> List { + [ + GeneratedPartitionCrateRow { + package_name: "witness-pkg-left", + crate_dir: "src/v1/witness_left", + kind: GeneratedFoundationCrate, + modules: ["witness_dup_module"], + reexport_packages: [], + carries_non_empty_wrappers: false + }, + GeneratedPartitionCrateRow { + package_name: "witness-pkg-right", + crate_dir: "src/v1/witness_right", + kind: GeneratedFoundationCrate, + modules: ["witness_dup_module"], + reexport_packages: [], + carries_non_empty_wrappers: false + } + ] +} + +test fn witness_duplicate_owner_refuses_holds() -> Bool { + match stage0_partition_rebuild_decision( + changed_mirrors: ["witness_dup_module.rs"], + unlocatable: [], + rows: witness_duplicate_owner_rows(), + executable: witness_fixture_executable() + ) { + RebuildScopeRefused { cause: MirrorHasMultipleOwningPackages { mirror: m, packages: ps } } => + (m == "witness_dup_module.rs") && (count(ps) == 2) + RebuildScopeRefused { cause: MirrorHasNoOwningPackage { mirror: _ } } => false + RebuildScopeRefused { cause: ChangedMirrorUnlocatable { mirrors: _, reason: _ } } => false + RebuildScopeRefused { cause: PackageDependencyClosureUnderivable { package_name: _, detail: _ } } => false + RebuildScopeRefused { cause: ExecutableAssemblyNotCovered { executable_package: _, detail: _ } } => false + WholeCompilerRebuildRequired { cause: _ } => false + PartitionRebuildScopeDerived { changed_mirrors: _, owning_packages: _, package_closure: _, executable_assembly: _ } => false + } +} + +// An unreadable changed path refuses, and refuses as ITS OWN cause: "the selection could not +// answer" and "the whole compiler must rebuild" are different states. +test fn witness_unlocatable_changed_path_refuses_holds() -> Bool { + match stage0_partition_rebuild_decision( + changed_mirrors: [], + unlocatable: ["src/v1/stage0/src/../unreadable"], + rows: generated_partition_crate_rows, + executable: witness_fixture_executable() + ) { + RebuildScopeRefused { cause: ChangedMirrorUnlocatable { mirrors: ms, reason: _ } } => count(ms) == 1 + RebuildScopeRefused { cause: MirrorHasNoOwningPackage { mirror: _ } } => false + RebuildScopeRefused { cause: MirrorHasMultipleOwningPackages { mirror: _, packages: _ } } => false + RebuildScopeRefused { cause: PackageDependencyClosureUnderivable { package_name: _, detail: _ } } => false + RebuildScopeRefused { cause: ExecutableAssemblyNotCovered { executable_package: _, detail: _ } } => false + WholeCompilerRebuildRequired { cause: _ } => false + PartitionRebuildScopeDerived { changed_mirrors: _, owning_packages: _, package_closure: _, executable_assembly: _ } => false + } +} + +// An executable package the changed bytes do not reach cannot be relinked into a next pass that +// carries them, so it refuses rather than linking a stale binary. +test fn witness_unreached_executable_package_refuses_holds() -> Bool { + match stage0_partition_rebuild_decision( + changed_mirrors: ["std_effects.rs"], + unlocatable: [], + rows: generated_partition_crate_rows, + executable: PartitionAssembledExecutable { package_name: "v1-stage0-runtime", bin_name: "claim_executor" } + ) { + RebuildScopeRefused { cause: ExecutableAssemblyNotCovered { executable_package: p, detail: _ } } => p == "v1-stage0-runtime" + RebuildScopeRefused { cause: MirrorHasNoOwningPackage { mirror: _ } } => false + RebuildScopeRefused { cause: MirrorHasMultipleOwningPackages { mirror: _, packages: _ } } => false + RebuildScopeRefused { cause: ChangedMirrorUnlocatable { mirrors: _, reason: _ } } => false + RebuildScopeRefused { cause: PackageDependencyClosureUnderivable { package_name: _, detail: _ } } => false + WholeCompilerRebuildRequired { cause: _ } => false + PartitionRebuildScopeDerived { changed_mirrors: _, owning_packages: _, package_closure: _, executable_assembly: _ } => false + } +} + +// CONTROL 6 -- the rung-honesty pair, and the reason it is a PAIR. Today's live decision DERIVES +// the scope (the packages are a real answer, and the round-cost projection reports them) while +// carrying ExecutableAssemblyUnavailable, so one arm alone cannot separate "we derived a scope" +// from "we rebuilt from it". The first test fixes the derived shape; the second fixes that the +// same decision is NOT actuatable. Both go red the moment this module claims an assembly the tree +// does not build -- which is exactly the inflation the acceptance bar forbids -- and both are +// edited, not deleted, when the executable cut lands: the shape survives, the expected arm flips. +test fn witness_today_derives_scope_with_unavailable_assembly_holds() -> Bool { + match stage0_partition_rebuild_decision_today(changed_mirrors: ["std_effects.rs"], unlocatable: []) { + PartitionRebuildScopeDerived { changed_mirrors: _, owning_packages: o, package_closure: c, executable_assembly: assembly } => + witness_has(items: o, item: "v1-stage0-std-surface") + && (count(c) > 1) + && (match assembly { + ExecutableAssemblyUnavailable { trigger: PartitionedClaimExecutorAssembly { capability: _ }, detail: _ } => true + PartitionAssembledExecutable { package_name: _, bin_name: _ } => false + }) + WholeCompilerRebuildRequired { cause: _ } => false + RebuildScopeRefused { cause: _ } => false + } +} + +test fn witness_today_scope_is_not_actuatable_holds() -> Bool { + !stage0_partition_rebuild_is_actuatable( + decision: stage0_partition_rebuild_decision_today(changed_mirrors: ["std_effects.rs"], unlocatable: []) + ) +} + +// The positive control for actuatability: without it, the predicate above could be a constant +// false and nothing would notice. +test fn witness_assembled_executable_scope_is_actuatable_holds() -> Bool { + stage0_partition_rebuild_is_actuatable(decision: witness_decide(changed: ["std_effects.rs"])) +} + +test fn witness_next_pass_executable_assembly_is_unavailable_holds() -> Bool { + match stage0_next_pass_executable_assembly() { + ExecutableAssemblyUnavailable { trigger: PartitionedClaimExecutorAssembly { capability: _ }, detail: _ } => true + PartitionAssembledExecutable { package_name: _, bin_name: _ } => false + } +}