diff --git a/dag/gunbc/rung_drop/shared_index_residency_asserted_after_the_run.dag b/dag/gunbc/rung_drop/shared_index_residency_asserted_after_the_run.dag index 875e40c9fdb..c53649e1971 100644 --- a/dag/gunbc/rung_drop/shared_index_residency_asserted_after_the_run.dag +++ b/dag/gunbc/rung_drop/shared_index_residency_asserted_after_the_run.dag @@ -1,7 +1,7 @@ module gunbc.rung_drop.shared_index_residency_asserted_after_the_run import std.types { NonEmptyStr } -import gunbc.rung_drop { RungDrop, Standing, TypedDeclaration, ReplacementStaged } +import gunbc.rung_drop { RungDrop, Retired, TypedDeclaration, ReplacementStaged } import gunbc.guarantee_rung { MechanicallyPreventable, Mitigatable } // THE DECLARED RUNG DROP for where one-pool-per-slot is enforced in the thread's shared index memo @@ -31,7 +31,7 @@ data shared_index_residency_asserted_after_the_run: RungDrop = RungDrop { declared: "2026-09-30", - standing: Standing, + standing: Retired { trigger_fired: "2026-09-30 -- every population caller's root set was audited (the table on the retiring PR): each passes the run's own roots except three one-shot readers of a fixture pool (the regen round's dag-artifact identity compile, the XL-1 primary-root tap, the required-lane resolution census builtin), which now own their index; try_process_shared_index_for_pool refuses a second resident pool in a slot at the build site (SharedIndexSecondResidentPool, naming both root sets and the demanding site), and shared_index_residency_control stays as its positive control" as NonEmptyStr }, declaration: TypedDeclaration { previous: MechanicallyPreventable, diff --git a/docs/design-rung-drops.md b/docs/design-rung-drops.md index ec2f6ec7147..81207dd9106 100644 --- a/docs/design-rung-drops.md +++ b/docs/design-rung-drops.md @@ -446,6 +446,8 @@ the python->typescript compile inhabitance claim (the real parse, bridge and com an edited witness file classified BinWitnessWet in gunbc.ci_layer_roots witness_exclusion_frontier: the required floor's changed-witness sublane declines it as DeclinedNoCiWetLane instead of blocking, and no CI lane executes it: RUNG DROP, mechanically preventable -> mitigatable (lost as a passenger of the per-PR bin-witness wet batch, deleted with the floor cut of 2026-08-15, and .github/workflows/falsifier.yml, deleted at 611fd02770 (gunbc#8283)). Population: self_host_artifact_materialization_real_execution_witness_test.dag, stage0_regen_convergence_real_execution_witness_test.dag, typed_witness_invocation_test.dag, namespace_structural_root_exposure_generated_witness_test.dag, emit_host_typed_smoke_test.dag, build_artifact_corruption_probe_witness_test.dag, dag_collect_fingerprint_witness_test.dag, dag_compile_clean_perturb_receipts_test.dag, test/claim/diagnostics_test.dag, effects_rest_transport_parse_witness_test.dag, test/claim/parse_test.dag, v1_dag_parse_witness_test.dag, auth_declared_but_unwired_witness_test.dag, test/claim/bootstrap_test.dag, infer_semantics_witness_test.dag, dag_compile_clean_shard_a_witness_test.dag, dag_compile_clean_shard_totality_witness_test.dag, dag_compile_clean_seam_witness_test.dag, run_verdict_exit_status_witness_test.dag, http_client_get_real_execution_witness_test.dag, roadmap_belt_actuate_witness_test.dag, host_build_cache_provision_real_execution_witness_test.dag, materialized_ssh_key_file_real_execution_witness_test.dag, proc_self_cgroup_real_execution_witness_test.dag, repo_local_git_config_real_execution_witness_test.dag, transport_script_stdin_byte_fidelity_witness_test.dag, push_event_witness_wet_test.dag. Restored when: a REQUIRED lane executes changed BinWitnessWet rows for the same pull_request or merge_group -- builds their witness binaries and runs the edited entries to a terminal verdict on a host that admits their effects -- so the changed-witness sublane can plan them instead of declining; sufficient for deleting DeclinedNoCiWetLane and this row together. -### one resident pool per precedence slot in the thread's shared index memo (entry_resolve::PROCESS_RESOLVE_INDEX) — declared 2026-09-30 +### one resident pool per precedence slot in the thread's shared index memo (entry_resolve::PROCESS_RESOLVE_INDEX) — declared 2026-09-30 · RETIRED + +**RETIRED — TRIGGER FIRED.** 2026-09-30 -- every population caller's root set was audited (the table on the retiring PR): each passes the run's own roots except three one-shot readers of a fixture pool (the regen round's dag-artifact identity compile, the XL-1 primary-root tap, the required-lane resolution census builtin), which now own their index; try_process_shared_index_for_pool refuses a second resident pool in a slot at the build site (SharedIndexSecondResidentPool, naming both root sets and the demanding site), and shared_index_residency_control stays as its positive control one resident pool per precedence slot in the thread's shared index memo (entry_resolve::PROCESS_RESOLVE_INDEX): RUNG DROP, mechanically preventable -> mitigatable (replacement staged: the caller audit plus a build-site refusal in try_process_shared_index_for_pool (the follow-up to gunbc#12821)). Population: v1_compiler::bin::claim_batch::run, v1_compiler::cli_run::call_compile_clean_bool_list_fn, v1_compiler::cli_run::ci_floor_commit_witness_claim_pairs, v1_compiler::cli_run::compile_clean::compile_clean_scope_plan_from_touched_paths, v1_compiler::cli_run::compile_emission, v1_compiler::cli_run::discover_owned_data_decls, v1_compiler::cli_run::emit_host::compile_dag_candidate_resolved_call_edges_uncached, v1_compiler::cli_run::emit_host::compile_xl1_primary_root_tap, v1_compiler::cli_run::emitted_crate_workspace_host::evaluate_plan, v1_compiler::cli_run::entry_resolve::load_sources_for_entry, v1_compiler::cli_run::entry_resolve::load_sources_for_entry_with_pool_index, v1_compiler::cli_run::entry_resolve::resolve_entry_graph, v1_compiler::cli_run::handle_serve, v1_compiler::cli_run::install_group_syntax, v1_compiler::cli_run::install_output_policy, v1_compiler::cli_run::materialization_provider_consumer::build_materialization_provider_ctx_cold, v1_compiler::cli_run::measure_selected_entry_closure_overlap, v1_compiler::cli_run::prime_witness_execution_legs_from_authority, v1_compiler::cli_run::project_witness_cost_receipt, v1_compiler::cli_run::render_batch_summary_line, v1_compiler::cli_run::required_floor_runner::cost_debt_changed_witness_ceiling, v1_compiler::cli_run::required_floor_runner::floor_base_test_decl_census, v1_compiler::cli_run::required_floor_runner::floor_compile_clean_emit_ok_via_index, v1_compiler::cli_run::required_floor_runner::floor_diff_baseline_readout, v1_compiler::cli_run::required_floor_runner::floor_diff_comparison_readout, v1_compiler::cli_run::required_floor_runner::floor_git_diff_name_status_range, v1_compiler::cli_run::required_floor_runner::floor_git_diff_range, v1_compiler::cli_run::required_floor_runner::head, v1_compiler::cli_run::required_floor_runner::run_discovery_corpus_with_options_inner, v1_compiler::cli_run::required_floor_runner::run_required_floor, v1_compiler::cli_run::required_floor_runner::unimported_bare_provider_roster_source_at_base, v1_compiler::cli_run::required_lane_resolution_census::entry_closure_module_identities, v1_compiler::cli_run::required_lane_resolution_census::required_floor_nominal_subject_module_identities, v1_compiler::cli_run::resolve, v1_compiler::cli_run::resolve_workspace_entry, v1_compiler::cli_run::run_required_bare_reference_admission, v1_compiler::cli_run::terminal_ledger_publish::build_ledger_wire_ctx, v1_compiler::cli_run::test_module_hygiene_bridge::resolve_hygiene_ctx, v1_compiler::cli_run::witness_gates::commit_witness_claim_pair_resolvable, v1_compiler::cli_run::witness_gates::commit_witness_claim_roster_defects, v1_compiler::generated_artifact_boundary_host::generated_artifact_ctx, v1_compiler::generated_artifact_boundary_host::run_docs_projection_agreement, v1_compiler::pre_push::load_plan_ctx, v1_compiler::required_regen_host::load, v1_compiler::required_regen_host::partition_rebuild_actuation, v1_compiler::required_regen_host::regen_generation_role_population, v1_compiler::required_regen_host::render_affected_set_bound, v1_compiler::required_regen_host::render_round_cost_receipt, v1_compiler::required_regen_host::render_scope_selection. Restored when: every population caller's root set is audited (it passes the run's own roots, or owns an index for a pool no other caller reads) and try_process_shared_index_for_pool refuses a second resident pool in a slot at the build site, before it is built; shared_index_residency_control then becomes the positive control of that refusal. Observed at retirement: the build-site refusal fires on a two-pool fixture and the floor and a regen round pass with it; this row retires in the same change. diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index fc1c7290eb7..f7a020f51f1 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -8558,6 +8558,20 @@ pub fn compile_entry_emission( /// the returned files, renders diagnostics and picks an exit code -- those are boundary /// concerns, not a second pipeline. pub fn compile_emission(request: &CompileRequest) -> CompileRun { + compile_emission_over(request, IndexResidency::SharedProcessPool) +} + +/// WHO HOLDS THE INDEX A COMPILE RESOLVES OVER. The run's own pool lives in the thread's shared +/// memo, one per precedence slot (`try_process_shared_index_for_pool`, which refuses a second). +/// A compile over a different pool whose index nothing after it reads -- a fixture root compiled +/// once by a control -- owns its index for the length of the compile instead. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IndexResidency { + SharedProcessPool, + OwnedByThisCompile, +} + +pub fn compile_emission_over(request: &CompileRequest, residency: IndexResidency) -> CompileRun { let source_roots: &[String] = &request.source_roots; let primary_precedence = request.primary_precedence; let subject_label = request.subject.label(); @@ -8746,7 +8760,23 @@ pub fn compile_emission(request: &CompileRequest) -> CompileRun { // compiling N entries reconciled the shared prefix N times over closures that overlap almost // entirely. See `try_process_shared_index_for_pool` for why that is a §2 cost-shape defect // rather than a budget fact, and for what is NOT changed by the routing. - let index: Rc = if primary_precedence { + let index: Rc = if residency == IndexResidency::OwnedByThisCompile { + let built = if primary_precedence { + try_build_module_index_primary_precedence(source_roots) + } else { + try_build_module_index(source_roots) + }; + match built { + Ok(module_index) => Rc::new(new_multi_entry_index_shell( + module_index, + source_roots, + None, + )), + Err(cause) => { + return compile_not_executed(&request.subject, started, "source-discovery", cause) + } + } + } else if primary_precedence { match try_process_shared_index_for_pool(source_roots, true) { Ok(idx) => idx, Err(cause) => { @@ -10473,41 +10503,34 @@ mod closure_edge_demand_tests { .unwrap(); } - /// The residency bound refuses a second resident pool per slot and admits one. + /// A SECOND POOL REFUSES AT BUILD, naming both root sets and the demanding site, and the + /// resident pool survives it: the residency control still reads one pool and the first + /// roots are still served from the memo. Red before the build-site refusal (the second + /// demand built and returned a second index); green with it. #[test] - fn a_second_resident_pool_in_one_slot_refuses() { + fn a_second_pool_in_one_slot_refuses_at_build() { let a = Fixture::new(&[("m.dag", "module res_a\nfn f() -> Int { 1 }\n")]); let b = Fixture::new(&[("m.dag", "module res_b\nfn f() -> Int { 1 }\n")]); - try_process_shared_index(&[a.0.to_string_lossy().into_owned()]).unwrap(); - assert_eq!(entry_resolve::shared_index_residency_control(), Ok(1)); - try_process_shared_index(&[b.0.to_string_lossy().into_owned()]).unwrap(); - let err = entry_resolve::shared_index_residency_control().unwrap_err(); - assert!(err.contains("SharedIndexMoreThanOneResidentPool"), "{err}"); - } - - /// DISTINCT ROOTS ARE KEPT SIDE BY SIDE, NOT EVICTED. Roots A, then roots B, then A again on - /// one thread (the regen round's shape): the third demand returns the FIRST index -- the same - /// `Rc`, its parse and typed caches intact -- rather than rebuilding A (a one-entry slot) or - /// refusing it (the #12765 refusal that broke the regen round). - #[test] - fn alternating_roots_on_one_thread_reuse_each_index() { - let a = Fixture::new(&[("m.dag", "module alt_a\nfn f() -> Int { 1 }\n")]); - let b = Fixture::new(&[("m.dag", "module alt_b\nfn f() -> Int { 1 }\n")]); let roots_a = vec![a.0.to_string_lossy().into_owned()]; let roots_b = vec![b.0.to_string_lossy().into_owned()]; let first_a = try_process_shared_index(&roots_a).unwrap(); - let first_b = try_process_shared_index(&roots_b).unwrap(); + assert_eq!(entry_resolve::shared_index_residency_control(), Ok(1)); + let err = match try_process_shared_index(&roots_b) { + Ok(_) => panic!("a second pool in slot 0 was built"), + Err(err) => err, + }; + assert!(err.contains("SharedIndexSecondResidentPool"), "{err}"); + for roots in [&roots_a, &roots_b] { + let canonical = entry_resolve::canonical_shared_index_roots(roots); + assert!( + err.contains(&format!("{canonical:?}")), + "names {canonical:?}: {err}" + ); + } + assert!(err.contains(file!()), "names the demanding site: {err}"); + assert_eq!(entry_resolve::shared_index_residency_control(), Ok(1)); let again_a = try_process_shared_index(&roots_a).unwrap(); - let again_b = try_process_shared_index(&roots_b).unwrap(); - assert!( - Rc::ptr_eq(&first_a, &again_a), - "A is reused after B, not rebuilt" - ); - assert!(Rc::ptr_eq(&first_b, &again_b), "B is reused after A"); - assert!( - !Rc::ptr_eq(&first_a, &first_b), - "distinct roots are distinct indexes" - ); + assert!(Rc::ptr_eq(&first_a, &again_a), "the resident pool is kept"); } /// THE VALID TWIN: the same shape with the homonym removed is admitted by the entry and by diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index dd12f97bdde..0cc57b39c3c 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -2448,7 +2448,7 @@ pub(crate) fn compile_xl1_primary_root_tap( ), }; } - let index = match try_process_shared_index(source_roots) { + let index = match super::entry_resolve::try_index_for_run_or_owned_pool(source_roots) { Ok(idx) => idx, Err(cause) => { return Xl1PrimaryRootTap::Refused { diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index aac339559f3..92747024401 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -671,8 +671,8 @@ thread_local! { // policy + group syntax, plus the floor runner) resolves against this single // MultiEntryIndex, so its parse/typed caches share the union of all those closures: // the shared std/spec prefix typechecks ONCE, not once per prelude entry. Keyed by - // source_roots — a run's roots are fixed, so this is a get-or-build, rebuilt only on - // the rare roots change. Thread-local by the same Rc-not-Send reason as the store: + // source_roots — a run's roots are fixed, so this is a get-or-build over exactly one pool + // per slot (below). Thread-local by the same Rc-not-Send reason as the store: // each shard keeps its own index rather than smuggling Rc across threads. // // TWO SLOTS, ONE PER POOL SEMANTICS, and the pair is what makes the memo safe rather than @@ -681,15 +681,16 @@ thread_local! { // for is a silently divergent resolution, which is the §5 fail-open this cache would // otherwise introduce. So precedence is part of the identity of the slot, not a build flag // applied to a shared one -- a roots-keyed single slot cannot express the distinction and - // would answer whichever mode ran first. Each slot keeps the original single-entry, - // rebuild-on-roots-change shape, so an index is never held for a pool nobody is asking about. + // would answer whichever mode ran first. #[allow(clippy::type_complexity)] // - // ONE INDEX PER ROOTS KEY WITHIN A SLOT, never one index per slot. A process may legitimately - // demand several pools on one thread (the regen round indexes [src/v1, dag] and [dag, src/v2]); - // a single entry per slot made each such demand evict the other, and the next demand for the - // evicted roots rebuilt it from scratch -- every file re-parsed, typed caches lost, the evicted - // index often still held. Distinct roots are distinct demands and are kept side by side. + // ONE RESIDENT POOL PER SLOT, REFUSED AT BUILD. The map is keyed by canonical roots so a + // second demand for the resident roots is a hit, never an eviction and rebuild; a demand for + // DIFFERENT roots is refused by `try_process_shared_index_for_pool` + // (`SharedIndexSecondResidentPool`) before anything is walked, so a slot never holds more + // than one entry. A one-shot reader of another pool (a fixture, the regen round's emitted + // tree) owns its index instead. `shared_index_residency_control` reads the bound back at + // the end of the floor and of a regen round as the positive control. pub(crate) static PROCESS_RESOLVE_INDEX: RefCell<[BTreeMap>; 2]> = const { RefCell::new([BTreeMap::new(), BTreeMap::new()]) }; @@ -897,10 +898,10 @@ fn release_per_entry_graphs_on_live_pool_thread() { }); } -/// AT MOST ONE RESIDENT POOL PER SLOT, ASSERTED. The memo keeps every pool a thread demands, so a -/// run's retention is the number of distinct root sets that entered it; the floor and the regen -/// round each demand one per precedence, and a one-shot reader owns its own index instead. A second -/// resident pool is a demand that should have been released, so it refuses rather than printing. +/// AT MOST ONE RESIDENT POOL PER SLOT, the positive control of the build-site refusal in +/// `try_process_shared_index_for_pool`. That refusal makes a second pool unbuildable through the +/// memo; this reads the memo back at the end of the floor and of a regen round, so a route that +/// ever installed one another way would still refuse rather than print. pub(crate) fn shared_index_residency_control() -> Result { PROCESS_RESOLVE_INDEX.with(|s| { let slots = s.borrow(); @@ -930,6 +931,32 @@ pub(crate) fn memoized_process_shared_index( PROCESS_RESOLVE_INDEX.with(|s| s.borrow()[0].get(&roots_key).cloned()) } +/// THE STRICT INDEX FOR A READER WHOSE ROOTS ARE A BUILTIN'S ARGUMENT. Over the layer roots +/// (`witness_layer_roots`) it is the run's pool and is read from the shared memo; over any other +/// roots -- a fixture a witness names -- it is a one-shot read of a pool nothing else reads, so +/// the reader owns the index and drops it with the read. The same split +/// `emit_host::resolve_roots_for_call_edge_pool` makes; it keeps a fixture out of the shared +/// memo, where `try_process_shared_index_for_pool` refuses a second pool. +#[track_caller] +pub(crate) fn try_index_for_run_or_owned_pool( + source_roots: &[String], +) -> Result, String> { + let layers = super::witness_layer_roots(); + if !source_roots.is_empty() + && source_roots + .iter() + .all(|root| layers.iter().any(|layer| layer == root)) + { + return try_process_shared_index(source_roots); + } + let roots = canonical_shared_index_roots(source_roots); + Ok(Rc::new(new_multi_entry_index_shell( + try_build_module_index(&roots)?, + &roots, + None, + ))) +} + /// Fallible twin of `process_shared_index`. The MEMO IS ONLY WRITTEN ON SUCCESS -- a failed /// discovery must not install a partial index that every later caller in the process would /// then read as complete. @@ -974,6 +1001,24 @@ pub fn try_process_shared_index_for_pool( if let Some(idx) = existing { return Ok(idx); } + // A SECOND POOL IS REFUSED WHERE IT WOULD BE BUILT, before it is walked, parsed and held + // beside the first. Every production caller passes the run's own roots or owns a private + // index for a pool nothing else reads (the audit that retired + // `gunbc.rung_drop.shared_index_residency_asserted_after_the_run`), so a different key here + // is a new demand for a second resident pool: carry it to its own index + // (`build_multi_entry_index`), never into the shared memo. + let resident = PROCESS_RESOLVE_INDEX.with(|s| s.borrow()[slot].keys().next().cloned()); + if let Some(resident) = resident { + let site = std::panic::Location::caller(); + return Err(format!( + "SharedIndexSecondResidentPool: slot {slot} already holds roots {:?}; roots {roots:?} \ + demanded at {}:{} would be a second resident pool on this thread -- a reader of \ + another pool owns its index (build_multi_entry_index), not a slot in the shared memo", + resident.split('\u{1f}').collect::>(), + site.file(), + site.line() + )); + } #[cfg(test)] yield_live_pool_before_building_another(); let build_started = std::time::Instant::now(); diff --git a/src/v1/stage0/src/cli_run/required_lane_resolution_census.rs b/src/v1/stage0/src/cli_run/required_lane_resolution_census.rs index 5a49f2f8f59..0dedbd60c6f 100644 --- a/src/v1/stage0/src/cli_run/required_lane_resolution_census.rs +++ b/src/v1/stage0/src/cli_run/required_lane_resolution_census.rs @@ -99,7 +99,14 @@ pub fn entry_closure_module_identities( source_roots: &[String], entry_path: &str, ) -> ModuleIdentityPopulation { - let index = process_shared_index(source_roots); + let index = match entry_resolve::try_index_for_run_or_owned_pool(source_roots) { + Ok(index) => index, + Err(cause) => { + return ModuleIdentityPopulation::Refused { + cause: format!("entry closure of {entry_path}: source-discovery: {cause}"), + } + } + }; let sources = match load_sources_for_entry_with_pool(&index, entry_path) { Ok(sources) => sources, Err(cause) => { diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index 95da64764a5..e92be94c367 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -9052,13 +9052,18 @@ fn emit_dag_artifact_text(root_rel: &str) -> Result { // repository, so it is addressed as one. let root = workspace_root().join(root_rel); let entry = root.join(DAG_ARTIFACT_IDENTITY_SPECIMEN_BASENAME); - let run = super::compile_emission(&super::CompileRequest { - subject: super::CompileSubject::Entry(entry.to_string_lossy().to_string()), - root_demand: super::RootDemandDeclaration::default(), - source_roots: vec![root.to_string_lossy().to_string()], - primary_precedence: false, - render_targets: vec![RenderTarget::Dag], - }); + // A FIXTURE POOL, COMPILED ONCE AND READ BY NOTHING AFTER: it owns its index, so the + // subject and the perturbed root never sit beside the run's pool in the shared memo. + let run = super::compile_emission_over( + &super::CompileRequest { + subject: super::CompileSubject::Entry(entry.to_string_lossy().to_string()), + root_demand: super::RootDemandDeclaration::default(), + source_roots: vec![root.to_string_lossy().to_string()], + primary_precedence: false, + render_targets: vec![RenderTarget::Dag], + }, + super::IndexResidency::OwnedByThisCompile, + ); match &run.disposition { super::CompileDisposition::Completed { .. } => {} super::CompileDisposition::Refused { phase, cause } => {