diff --git a/src/v3/compiler/src/bootstrap_generated.rs b/src/v3/compiler/src/bootstrap_generated.rs index b1d262c53b2..18f53ce4017 100644 --- a/src/v3/compiler/src/bootstrap_generated.rs +++ b/src/v3/compiler/src/bootstrap_generated.rs @@ -2490,5 +2490,6 @@ pub(crate) fn bootstrapped_fixture_dag() -> Dag { callable_strategy_variants: CallableStrategyVariants::default(), clusters: vec![], optional_match_disjs: HashMap::new(), + user_declaration_append_begin: 0, } } diff --git a/src/v3/compiler/src/bootstrap_generated_without_parse_surface.rs b/src/v3/compiler/src/bootstrap_generated_without_parse_surface.rs index d1048d1a666..ff7230b6c92 100644 --- a/src/v3/compiler/src/bootstrap_generated_without_parse_surface.rs +++ b/src/v3/compiler/src/bootstrap_generated_without_parse_surface.rs @@ -2407,5 +2407,6 @@ pub(crate) fn bootstrapped_fixture_without_parse_surface_dag() -> Dag { callable_strategy_variants: CallableStrategyVariants::default(), clusters: vec![], optional_match_disjs: HashMap::new(), + user_declaration_append_begin: 0, } } diff --git a/src/v3/compiler/src/bootstrap_std_generated.rs b/src/v3/compiler/src/bootstrap_std_generated.rs index 9e6c869ffa5..f470b27a09c 100644 --- a/src/v3/compiler/src/bootstrap_std_generated.rs +++ b/src/v3/compiler/src/bootstrap_std_generated.rs @@ -12413,5 +12413,6 @@ pub(crate) fn bootstrapped_std_fixture_dag() -> Dag { callable_strategy_variants: CallableStrategyVariants::default(), clusters: vec![], optional_match_disjs: HashMap::new(), + user_declaration_append_begin: 0, } } diff --git a/src/v3/compiler/src/dag.rs b/src/v3/compiler/src/dag.rs index bef81ed4788..02286b4f8d9 100644 --- a/src/v3/compiler/src/dag.rs +++ b/src/v3/compiler/src/dag.rs @@ -2358,6 +2358,10 @@ pub struct Dag { /// inference needs stable `Some` / `None` variant identities without /// promoting optionals into named top-level declarations. optional_match_disjs: HashMap, + /// First [`DeclarationId::raw`] value minted **after** the embedded bootstrap fixture + /// rows loaded at `Dag` construction (`lower` / `lower_into` append here; callers cannot + /// claim fixture provenance for these rows by stamping an authority-looking `span.file`). + user_declaration_append_begin: u32, } static BOOTSTRAPPED_DAG: LazyLock = LazyLock::new(|| { @@ -2366,6 +2370,7 @@ static BOOTSTRAPPED_DAG: LazyLock = LazyLock::new(|| { assert_bootstrap_fixture_paths_match_regen_keys(&dag); dag.populate_primitive_cache(); crate::int_literal_ranges::validate_rust_pilot_integer_primitives(&mut dag); + dag.user_declaration_append_begin = dag.declarations.len() as u32; dag }); @@ -2376,6 +2381,7 @@ static BOOTSTRAPPED_STD_FIXTURE_DAG: LazyLock = LazyLock::new(|| { let mut dag = bootstrap_std_generated::bootstrapped_std_fixture_dag(); dag.mark_bootstrap_secret_nominal_opacity(); dag.populate_primitive_cache(); + dag.user_declaration_append_begin = dag.declarations.len() as u32; dag }); @@ -2386,6 +2392,7 @@ static BOOTSTRAPPED_DAG_WITHOUT_PARSE_SURFACE_FIXTURE: LazyLock = LazyLock: assert_bootstrap_fixture_paths_match_regen_keys(&dag); dag.populate_primitive_cache(); crate::int_literal_ranges::validate_rust_pilot_integer_primitives(&mut dag); + dag.user_declaration_append_begin = dag.declarations.len() as u32; dag }); @@ -2417,6 +2424,7 @@ impl Dag { callable_strategy_variants: CallableStrategyVariants::default(), clusters: Vec::new(), optional_match_disjs: HashMap::new(), + user_declaration_append_begin: 0, } } @@ -2470,6 +2478,16 @@ impl Dag { self.primitives.string } + /// Raw [`DeclarationId`] boundary between embedded bootstrap fixture rows and any + /// declarations minted afterward (`lower_into`, tests, etc.). + /// + /// Lifetime extraction and similar consumers must **not** classify post-boundary rows as + /// “fixture authority” based on [`Declaration::span`] alone — callers can supply an + /// authority-*looking* `span.file` string without being part of the checked-in corpus. + pub fn declaration_append_boundary(&self) -> u32 { + self.user_declaration_append_begin + } + /// The std **scalar** `TypeShape` roots used to prune algebra / ring /// `TypeConnective::Arrow` from the first-class-`fn` *data* walk in /// `emit::rust_target` (`decl_includes_first_class_arrow_data` and related). diff --git a/src/v3/compiler/src/regen_bootstrap_emit.rs b/src/v3/compiler/src/regen_bootstrap_emit.rs index ec2454dfb30..794133657d3 100644 --- a/src/v3/compiler/src/regen_bootstrap_emit.rs +++ b/src/v3/compiler/src/regen_bootstrap_emit.rs @@ -96,6 +96,7 @@ fn emit_bootstrap_module(dag: &Dag, function_name: &str) -> String { &render_optional_match_disjs(dag), 2, ); + out.push_str(" user_declaration_append_begin: 0,\n"); out.push_str(" }\n"); out.push_str("}\n"); out diff --git a/src/v3/grounding_lifetime/src/extract.rs b/src/v3/grounding_lifetime/src/extract.rs index ea2c2cc15ef..ac23224e45f 100644 --- a/src/v3/grounding_lifetime/src/extract.rs +++ b/src/v3/grounding_lifetime/src/extract.rs @@ -5,9 +5,11 @@ //! so extraction returns an **empty** program (the structural fold is validated via //! [`LifetimeProgram`] fixtures). //! -//! Any **`data` or `fn` declaration** rooted in a **non-authority** source file (user -//! or test modules) is treated as load-bearing program surface we cannot project yet: -//! fail-closed per C-8 instead of returning `Ok(empty)` and dropping facts. +//! Any **`data` or `fn` declaration** minted **after** the embedded fixture snapshot +//! ([`Dag::declaration_append_boundary`]) is treated as load-bearing program surface we +//! cannot project yet — **even if** `span.file` looks like an authority path (callers +//! control the compile-time file string). Fail-closed per C-8 instead of returning +//! `Ok(empty)` and dropping facts. use v3_compiler::dag::{Dag, TypeConnective}; @@ -21,6 +23,9 @@ use std::collections::BTreeMap; /// Span roots that appear on [`Dag::new()`] bootstrap fixtures (regenerated snapshots). /// +/// Used only as **defense in depth** for rows with `DeclarationId::raw` **below** +/// [`Dag::declaration_append_boundary`]: those rows are expected to be corpus-only. +/// /// Keep aligned with `bootstrap_generated.rs` / `bootstrap_generated_without_parse_surface.rs` /// when new fixture corpora land; otherwise user/test modules may be misclassified. /// @@ -42,11 +47,9 @@ fn is_bootstrap_fixture_authority_source_file(file: &str) -> bool { ) } -fn first_non_authority_lifetime_surface_declaration(dag: &Dag) -> Option<(String, String)> { +fn first_unextracted_lifetime_surface_declaration(dag: &Dag) -> Option<(String, String)> { + let boundary = dag.declaration_append_boundary(); for decl in dag.declarations() { - if is_bootstrap_fixture_authority_source_file(&decl.span.file) { - continue; - } let surface = decl.value_body.is_some() || matches!(&decl.connective, TypeConnective::Arrow { .. }); if !surface { @@ -56,7 +59,14 @@ fn first_non_authority_lifetime_surface_declaration(dag: &Dag) -> Option<(String .name .clone() .unwrap_or_else(|| format!("declaration#{}", decl.id.raw())); - return Some((name, decl.span.file.clone())); + let post_fixture = decl.id.raw() >= boundary; + if post_fixture { + return Some((name, decl.span.file.clone())); + } + if !is_bootstrap_fixture_authority_source_file(&decl.span.file) { + // Fixture-id range but not stamped like the embedded corpus — do not trust. + return Some((name, decl.span.file.clone())); + } } None } @@ -65,7 +75,7 @@ fn first_non_authority_lifetime_surface_declaration(dag: &Dag) -> Option<(String /// /// Fail-closed on constructs the R2 analyzer does not model (once lowering surfaces them). pub fn extract_lifetime_program(dag: &Dag) -> Result { - if let Some((name, file)) = first_non_authority_lifetime_surface_declaration(dag) { + if let Some((name, file)) = first_unextracted_lifetime_surface_declaration(dag) { return Err(EmissionDiagnostic::LifetimeProgramExtractionPending { detail: format!("{name} ({file})"), }); diff --git a/src/v3/grounding_lifetime/src/lib.rs b/src/v3/grounding_lifetime/src/lib.rs index 3a3610bbd61..5c254c9945d 100644 --- a/src/v3/grounding_lifetime/src/lib.rs +++ b/src/v3/grounding_lifetime/src/lib.rs @@ -165,6 +165,21 @@ mod tests { } } + /// `span.file` is caller-controlled; authority-looking paths must not bypass C-8. + #[test] + fn extract_fail_closed_when_user_code_uses_authority_looking_span_path() { + let source = "module spoof\n\nfn f() -> Int = 0\n"; + let file = "dsl/std/user_spoof_under_extractor_test.dag"; + let dag = v3_compiler::compile_to_dag(source, file).expect("compile"); + let err = extract_lifetime_program(&dag).expect_err("must not Ok(empty) on spoof path"); + match err { + EmissionDiagnostic::LifetimeProgramExtractionPending { detail } => { + assert!(detail.contains('f'), "unexpected detail: {detail}"); + } + other => panic!("unexpected diagnostic: {other:?}"), + } + } + #[test] fn function_param_indeterminate_growability_fails_closed_even_when_borrowed() { let mut bindings = BTreeMap::new();