diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 790920e1276..104bd3bfc5a 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -2682,10 +2682,10 @@ pub struct MultiEntryIndex { // front of the cross-process store (materialization-ladder tier ordering: // the share serves repeats, the store serves the process's FIRST touch of a // subject, and a store hit is INSTALLED here so every later demand takes the - // reference). Without the install-back, N same-subject resolves under - // GUNBC_RESOLVED_GRAPH_CACHE_DIR each decoded+retained an independent graph - // — the ~1 GiB/5s eval-phase runaway receipt (eager-ram-612, 2026-07-10). - // Store fills share — never replaces it. + // reference). Always populated on first assembly — not gated on + // GUNBC_RESOLVED_GRAPH_CACHE_DIR (the disk tier is opt-in separately). + // Without the install-back, N same-subject resolves each retained an independent + // graph — the reconcile_assembly per-entry rerun receipt (resolve-split #6535). resolved_graph_memo: RefCell< HashMap< String, @@ -2950,22 +2950,26 @@ fn resolve_entry_with_parse_cache( )?; resolve_stage_slot_add(|s| s.load += load_started.elapsed().as_nanos()); + let subject = subject_digest_for_closure(&sources); + // In-process share tier (resolved_graph_memo): always on — the ReferenceTier in + // front of the opt-in cross-process store. A subject this process has already + // assembled is served by reference, eliminating the per-entry reconcile assembly + // residue on re-resolve (Track A denomination receipt, resolve-split #6535). + if let Some((graph, si)) = index.resolved_graph_memo.borrow().get(&subject) { + return Ok((graph.clone(), si.clone())); + } + // Cross-process store tier: opt-in via GUNBC_RESOLVED_GRAPH_CACHE_DIR; installs into + // the share above on hit so later same-subject demands never re-decode. if let Some(cache_root) = resolved_graph_cache_root_from_env() { - let subject = subject_digest_for_closure(&sources); - // Share before store (ladder tier ordering): a subject this process has - // already decoded or built is served by reference — never re-decoded. - if let Some((graph, si)) = index.resolved_graph_memo.borrow().get(&subject) { - return Ok((graph.clone(), si.clone())); - } match cross_process_lookup(&cache_root, &subject) { CacheLookupResult::Hit(hit) => { eprintln!( "[resolved-graph-cache] decode subject={subject} (installed into process share)" ); - index - .resolved_graph_memo - .borrow_mut() - .insert(subject, (hit.graph.clone(), hit.source_indices.clone())); + index.resolved_graph_memo.borrow_mut().insert( + subject, + (hit.graph.clone(), hit.source_indices.clone()), + ); return Ok((hit.graph, hit.source_indices)); } CacheLookupResult::RejectedHit(_) | CacheLookupResult::Miss => {} @@ -3141,8 +3145,12 @@ fn resolve_entry_with_parse_cache( } resolve_stage_slot_add(|s| s.ownership += ownership_started.elapsed().as_nanos()); + // Install into the in-process share so same-subject re-resolves skip assembly. + index.resolved_graph_memo.borrow_mut().insert( + subject.clone(), + (typed.clone(), source_indices.clone()), + ); if let Some(cache_root) = resolved_graph_cache_root_from_env() { - let subject = subject_digest_for_closure(&sources); // A failed store write is a disclosed refusal, never a silent shrug — // the swallowed error hid that big closures never landed on disk (only // the prelude artifact ever existed), which mis-shaped a whole OOM @@ -3151,12 +3159,6 @@ fn resolve_entry_with_parse_cache( { eprintln!("[resolved-graph-cache] write refused subject={subject}: {e}"); } - // Build fills the share through the same seam as a store hit, so a - // same-subject re-resolve later in this process takes the reference. - index - .resolved_graph_memo - .borrow_mut() - .insert(subject, (typed.clone(), source_indices.clone())); } Ok((typed, source_indices)) @@ -3326,6 +3328,102 @@ mod module_schedule_batches_tests { } } +fn finish_resolved_graph_assembly( + modules: Rc>>, + diag_chunks: Vec>>>, + binding_fork_counts: (usize, usize), + source_indices: Rc>>, +) -> Result, String> { + let (same_tree_fork_count, cross_tree_fork_count) = binding_fork_counts; + let item_registry = modules.iter().fold(v1_rt::rc_empty_map(), |acc, typed| { + v1_rt::rc_map_merge(acc, typed.item_registry.clone()) + }); + let expanded_registry = + v1_compiler_infer::expand_transitive_services(modules.clone(), item_registry, 5); + let diagnostics: Rc>> = Rc::new({ + let mut acc = im_rc::Vector::new(); + for chunk in &diag_chunks { + acc.extend(chunk.iter().cloned()); + } + acc + }); + let total_fork_count = same_tree_fork_count + cross_tree_fork_count; + if total_fork_count > 0 && floor_verbose() { + eprintln!( + "[binding-fork-ledger] same_tree={same_tree_fork_count} cross_tree={cross_tree_fork_count} total={total_fork_count}" + ); + } + let modules = + v1_compiler_infer::rewire_type_env_parent_links(modules.clone(), source_indices.clone()); + let modules = v1_compiler_infer::rewire_type_env_import_str_binding_identity( + modules.clone(), + source_indices.clone(), + ); + let modules = + v1_compiler_infer::rewire_func_env_parent_links(modules.clone(), source_indices.clone()); + let has_v1_seed = v1_compiler_infer::corpus_has_v1_seed_source_indices(modules.clone()); + let emit_graph_info = v1_compiler_infer::build_emit_graph_info(modules.clone(), has_v1_seed); + Ok(Rc::new(ResolvedGraph { + modules, + item_registry: expanded_registry, + diagnostics, + emit_graph_info, + })) +} + +/// When every module in the closure is already in the typed cache, skip the +/// schedule-dispatch loop's per-module `collect_parent_envs` and +/// `build_variant_export_surface` work — both exist only to feed a cold +/// `typecheck_module` — and jump straight to closure assembly (expand, rewire, +/// emit). The assembled output matches the serial dispatch path because +/// variant surfaces are not consulted on a cache hit and parent-env diagnostics +/// are empty when every import parent is already in the store. +fn reconcile_all_cache_hits( + closure_modules: &[Rc], + closure_names: &[String], + source_indices: Rc>>, + typed_cache: &RefCell>>, + module_identity: &RefCell>, +) -> Result, String> { + let mut modules_vec = im_rc::Vector::new(); + let mut diag_chunks: Vec>>> = + Vec::with_capacity(closure_modules.len() * 2); + let mut same_tree_fork_count: usize = 0; + let mut cross_tree_fork_count: usize = 0; + let empty_parent_diags = Rc::new(im_rc::Vector::new()); + + for (resolved, mod_name) in closure_modules.iter().zip(closure_names.iter()) { + let decl_file = workspace_relative_repo_path(&resolved.module.span.file); + check_module_source_identity(module_identity, mod_name, &decl_file)?; + let tc_result = typed_cache + .borrow() + .get(mod_name) + .cloned() + .ok_or_else(|| { + format!( + "reconcile all-cache-hit path: module '{mod_name}' missing from typed store" + ) + })?; + modules_vec.push_back(tc_result.typed.clone()); + diag_chunks.push(empty_parent_diags.clone()); + diag_chunks.push(tc_result.diagnostics.clone()); + for fork in tc_result.binding_forks.iter() { + if fork.same_tree { + same_tree_fork_count += 1; + } else { + cross_tree_fork_count += 1; + } + } + } + + finish_resolved_graph_assembly( + Rc::new(modules_vec), + diag_chunks, + (same_tree_fork_count, cross_tree_fork_count), + source_indices, + ) +} + fn reconcile_with_typed_cache( graph: Rc, source_indices: Rc>>, @@ -3333,9 +3431,7 @@ fn reconcile_with_typed_cache( typed_cache: &RefCell>>, module_identity: &RefCell>, ) -> Result, String> { - let mut modules: Rc>> = Rc::new(im_rc::Vector::new()); let mut module_index: Rc>> = v1_rt::rc_empty_map(); - let mut item_registry: Rc>> = v1_rt::rc_empty_map(); let mut diag_chunks: Vec>>> = Vec::new(); let mut variant_surfaces: Rc>> = v1_rt::rc_empty_map(); @@ -3355,6 +3451,18 @@ fn reconcile_with_typed_cache( .iter() .map(|m| authored_name_at(source_indices.clone(), m.module.clone())) .collect(); + if closure_names + .iter() + .all(|name| typed_cache.borrow().contains_key(name)) + { + return reconcile_all_cache_hits( + &closure_modules, + &closure_names, + source_indices, + typed_cache, + module_identity, + ); + } let schedule = module_schedule_batches(&closure_modules, &closure_names); let mut dispatched: Vec< Option<( @@ -3366,13 +3474,6 @@ fn reconcile_with_typed_cache( for batch in &schedule { for &slot in batch { let resolved = closure_modules[slot].clone(); - let parent_envs_started = std::time::Instant::now(); - let parent_result = v1_compiler_infer::collect_parent_envs( - resolved.clone(), - module_index.clone(), - source_indices.clone(), - ); - resolve_stage_slot_add(|s| s.parent_envs += parent_envs_started.elapsed().as_nanos()); let mod_name = closure_names[slot].clone(); // Collision-honesty guard (union-resolve receipt §6.3): the typed cache is keyed by // authored name and shared across every co-resident entry, so a name that resolves @@ -3388,6 +3489,21 @@ fn reconcile_with_typed_cache( let decl_file = workspace_relative_repo_path(&resolved.module.span.file); check_module_source_identity(module_identity, &mod_name, &decl_file)?; let cached = typed_cache.borrow().get(&mod_name).cloned(); + let was_cache_hit = cached.is_some(); + let parent_diags = if was_cache_hit { + Rc::new(im_rc::Vector::new()) + } else { + let parent_envs_started = std::time::Instant::now(); + let parent_result = v1_compiler_infer::collect_parent_envs( + resolved.clone(), + module_index.clone(), + source_indices.clone(), + ); + resolve_stage_slot_add(|s| { + s.parent_envs += parent_envs_started.elapsed().as_nanos() + }); + parent_result.diagnostics.clone() + }; let tc_result = match cached { Some(hit) => hit, None => { @@ -3432,7 +3548,7 @@ fn reconcile_with_typed_cache( ), ); module_index = v1_rt::rc_map_insert(module_index, typed_path, typed.clone()); - dispatched[slot] = Some((parent_result.diagnostics.clone(), tc_result)); + dispatched[slot] = Some((parent_diags, tc_result)); } } @@ -3454,6 +3570,7 @@ fn reconcile_with_typed_cache( // concern; the graph handed to consumers — module list, registry merge order, and // diagnostic order — is assembled in the exact order the serial fold produced, so the // result is byte-identical regardless of how the schedule batched the closure. + let mut modules_vec = im_rc::Vector::new(); for (slot, entry) in dispatched.into_iter().enumerate() { let (parent_diags, tc_result) = entry.unwrap_or_else(|| { unreachable!( @@ -3462,15 +3579,10 @@ fn reconcile_with_typed_cache( closure_names[slot] ) }); - let typed = tc_result.typed.clone(); - modules = v1_rt::rc_list_push(modules, typed.clone()); - item_registry = v1_rt::rc_map_merge(item_registry, typed.item_registry.clone()); + modules_vec.push_back(tc_result.typed.clone()); diag_chunks.push(parent_diags); diag_chunks.push(tc_result.diagnostics.clone()); for fork in tc_result.binding_forks.iter() { - // `same_tree` is computed on the `.dag` side (04_env `source_tree_of`, the single - // classification authority) and carried on the conflict — no parallel Rust - // classifier to drift (§3). if fork.same_tree { same_tree_fork_count += 1; } else { @@ -3479,40 +3591,12 @@ fn reconcile_with_typed_cache( } } - let expanded_registry = - v1_compiler_infer::expand_transitive_services(modules.clone(), item_registry, 5); - let diagnostics: Rc>> = Rc::new({ - let mut acc = im_rc::Vector::new(); - for chunk in &diag_chunks { - acc.extend(chunk.iter().cloned()); - } - acc - }); - let total_fork_count = same_tree_fork_count + cross_tree_fork_count; - // Per-resolve §3 fork census — opt-in. The floor resolves once per entry, so an - // unconditional print streams one census line per entry (hundreds per run). The count is - // recomputed from source on demand; gate the narration behind the floor verbose flag. - if total_fork_count > 0 && floor_verbose() { - eprintln!( - "[binding-fork-ledger] same_tree={same_tree_fork_count} cross_tree={cross_tree_fork_count} total={total_fork_count}" - ); - } - let modules = - v1_compiler_infer::rewire_type_env_parent_links(modules.clone(), source_indices.clone()); - let modules = v1_compiler_infer::rewire_type_env_import_str_binding_identity( - modules.clone(), - source_indices.clone(), - ); - let modules = - v1_compiler_infer::rewire_func_env_parent_links(modules.clone(), source_indices.clone()); - let has_v1_seed = v1_compiler_infer::corpus_has_v1_seed_source_indices(modules.clone()); - let emit_graph_info = v1_compiler_infer::build_emit_graph_info(modules.clone(), has_v1_seed); - Ok(Rc::new(ResolvedGraph { - modules, - item_registry: expanded_registry, - diagnostics, - emit_graph_info, - })) + finish_resolved_graph_assembly( + Rc::new(modules_vec), + diag_chunks, + (same_tree_fork_count, cross_tree_fork_count), + source_indices, + ) } fn format_error_loc(file: &str, start: i64, si: &HashMap>) -> String {