Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 155 additions & 71 deletions src/v1/stage0/src/cli_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 => {}
Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -3326,16 +3328,110 @@ mod module_schedule_batches_tests {
}
}

fn finish_resolved_graph_assembly(
modules: Rc<im_rc::Vector<Rc<TypedModule>>>,
diag_chunks: Vec<Rc<im_rc::Vector<Rc<ErrorNode>>>>,
binding_fork_counts: (usize, usize),
source_indices: Rc<HashMap<String, Rc<NewlineIndex>>>,
) -> Result<Rc<ResolvedGraph>, 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<im_rc::Vector<Rc<ErrorNode>>> = 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<v1_compiler_resolve::ResolvedModule>],
closure_names: &[String],
source_indices: Rc<HashMap<String, Rc<NewlineIndex>>>,
typed_cache: &RefCell<HashMap<String, Rc<v1_compiler_infer::TypecheckModuleResult>>>,
module_identity: &RefCell<HashMap<String, String>>,
) -> Result<Rc<ResolvedGraph>, String> {
let mut modules_vec = im_rc::Vector::new();
let mut diag_chunks: Vec<Rc<im_rc::Vector<Rc<ErrorNode>>>> =
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<v1_compiler_resolve::ModuleGraph>,
source_indices: Rc<HashMap<String, Rc<NewlineIndex>>>,
intern_table: Rc<InternTable>,
typed_cache: &RefCell<HashMap<String, Rc<v1_compiler_infer::TypecheckModuleResult>>>,
module_identity: &RefCell<HashMap<String, String>>,
) -> Result<Rc<ResolvedGraph>, String> {
let mut modules: Rc<im_rc::Vector<Rc<TypedModule>>> = Rc::new(im_rc::Vector::new());
let mut module_index: Rc<HashMap<String, Rc<TypedModule>>> = v1_rt::rc_empty_map();
let mut item_registry: Rc<HashMap<String, Rc<ItemInfo>>> = v1_rt::rc_empty_map();
let mut diag_chunks: Vec<Rc<im_rc::Vector<Rc<ErrorNode>>>> = Vec::new();
let mut variant_surfaces: Rc<HashMap<String, Rc<v1_compiler_infer::VariantExportSurface>>> =
v1_rt::rc_empty_map();
Expand All @@ -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<(
Expand All @@ -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
Expand All @@ -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 => {
Expand Down Expand Up @@ -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));
}
}

Expand All @@ -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!(
Expand All @@ -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 {
Expand All @@ -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<im_rc::Vector<Rc<ErrorNode>>> = 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, Rc<NewlineIndex>>) -> String {
Expand Down
Loading