From bef3da3eb81ebe6a6ae59d4f961c7ea9e3a5f760 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Sun, 2 Aug 2026 13:43:11 +0900 Subject: [PATCH] feat: validate declaration headers before MIR lowering --- HANDOFF.md | 64 +- bench/library_boundary/README.md | 3 + bench/library_boundary/src/main.rs | 33 + crates/align_codegen_llvm/src/lib.rs | 2 +- crates/align_interface/src/codec.rs | 17 + crates/align_mir/src/lib.rs | 39 +- crates/align_mir/src/validate_hir.rs | 409 ++++++++++ crates/align_mir/src/validate_hir_tests.rs | 712 +++++++++++++++++- crates/align_sema/src/hir.rs | 31 +- crates/align_sema/src/hir_depth.rs | 15 +- crates/align_sema/src/lib.rs | 220 +++++- docs/impl/07-roadmap.md | 2 +- .../impl/17-library-boundary-prerequisites.md | 47 +- 13 files changed, 1497 insertions(+), 97 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 01d2f795..16eb26bb 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -5,9 +5,10 @@ about the present state, the next decision, and operational facts. The former per-PR journal is preserved in [`docs/archive/HANDOFF-2026-07-25.md`](docs/archive/HANDOFF-2026-07-25.md). -_Last updated: 2026-08-02. `main` includes the shipped wave through #688 and the merged am-p -placement-validation PR #690 (`39f9c7d`). The current resumable branch is -`feat/am-n-nominal-link`, implementing the next am-n nominal/link slice. +_Last updated: 2026-08-02. `main` includes the shipped wave through #688 plus the merged am-p +placement-validation PR #690 (`39f9c7d`) and am-n nominal/link PR #691 (`755cb9c`). The current +resumable branch is `feat/am-h-declarations-headers`, implementing the next am-h +declarations/headers slice. #667 adds the canonical recursive Drop plan and sound `Option` fields; #668 admits one direct recursively Move payload per tagged arm; #669 admits multiple Move payloads; #670 completes nested tagged payload representation and the exact pkg.db L1b acceptance shape. @@ -409,9 +410,10 @@ contract fixes, graph-valid negative fixtures, four lowering-entrypoint parity, ledger are one atomic boundary; an intermediate split would either publish an unvalidated entrypoint or leave the reviewed producer/validator matrix without its required owner evidence. -### am-n implementation checkpoint (2026-08-02) +### am-n shipped checkpoint (2026-08-02) -The am-n implementation is active on `feat/am-n-nominal-link`. MIR now validates struct/enum +The am-n implementation shipped in PR #691 (`755cb9c`), after am-p shipped in PR #690 +(`39f9c7d`). MIR now validates struct/enum identity text, combined internal-name collisions, ASCII member names, repeated source-name structural equality, tuple interning uniqueness, alignment, enum field bases, and link-library names before all four HIR-to-MIR entrypoints copy HIR. Source-shape comparison uses an explicit @@ -437,8 +439,54 @@ required benchmark row is implemented, but this host cannot run the release harn `llvm-config-22` is absent. The current diff is about 940 changed lines: the validator, four-entrypoint owner matrix, shared-graph regression, and benchmark are one atomic am-n boundary, so splitting it would leave an activated identity gate without its complete owner -evidence. The author self-review is complete; remaining work is the final pre-PR stamp, then one -bounded post-open review cycle for the am-n SHA. +evidence. The author self-review, pre-PR stamp, CI, post-open host/independent review records, +and final attestation all bind to the merged am-n SHA. The branch is now reset to the next am-h +slice. + +### am-h implementation checkpoint (2026-08-02) + +The next slice is am-h declarations and headers. It activates body-independent validation for +externs, imported functions, stored functions, `main`, locals, parameter modes, return-borrow and +return-region summaries, and structural drop-set records. Imported interface facts gain a +normalized `FnEffect`: a present compatibility-map entry is copied as-is, while an absent entry +becomes `Impure`. HIR retains that fact for validation and MIR strips it after validation so the +existing six-field imported declaration and its structural Debug bytes, interface bytes, +`impl_hash`, and cache identity remain unchanged. `FnOrigin` replaces the overloaded +`lifted_capture_count`/`exportable` pair and is derived before per-unit export decisions. + +The public contract and owner evidence are recorded in the am-h ledger and implementation +closure matrix in `docs/impl/17-library-boundary-prerequisites.md`; the universal malformed-record +rules are in `docs/impl/19-hir-validation-ledger.md`. A fresh independent adversarial review of +the completed matrix returned `CLEAN`; its record is +`.git/align-am-h-matrix-review-2026-08-02.log`. The intended owner tests are +`malformed_hir_declaration_header_metadata_fails_closed`, +`main_header_abi_matrix_is_exhaustive`, +`valid_hir_declaration_header_preflight_is_mir_identity`, imported-effect normalization twins, +deep signature/summary twins, and the `mir-header-validation` benchmark row. The first fresh +independent am-h preflight found one valid P2 in the parameter-id mutation fixture; the baseline +now contains a valid non-parameter local so that mutation reaches the header validator. A second +fresh review found two P2s: capture-count rollback now truncates nested lifted functions, and the +matrix now covers duplicate declaration names, FnTy mode/summary fields, root order/range/ +borrowability, drop-set ranges, and the full main/Error ABI matrix. The final fresh review is +required on this revised tree. A follow-up Galileo review was run against the revised worktree +but returned no verdict after repeated bounded waits and no findings were reported; it was stopped +as an incomplete review, not treated as CLEAN. The final review must start from the committed SHA +and record its last completed scope if its bound is reached. +The current am-h worktree delta is 1,445 changed hand-written lines under `crates/`, above the +750–950 estimate and the roughly 1,000-line throughput target. The count includes the common +validator, its all-entrypoint mutation matrix, the expanded exact header/ABI matrix, the +producer/effect migration and every consumer, the distinct MIR imported record, and the +codec/producer owners. It cannot be split safely: a producer-only slice would need a temporary +old/new HIR representation, a validator-only slice would leave the overloaded origin pair, and a +MIR-only slice would publish an unvalidated effect; the reviewed am-h matrix therefore keeps this +as one atomic vertical boundary. + +The standalone benchmark now checks and runs with the repository LLVM 22 path. Its release +`provenance` run reported `mir-header-validation 0.730 ms/valid+malformed-lower` for 257 +functions, alongside the unchanged global-type, nominal/link, continuation, and interface rows. +The revised header owner test compiled but its macOS test binary again paused in dyld startup +(CPU 0, no output after sampling) and was stopped; this host limitation is recorded with the +successful compile and benchmark evidence rather than treated as a test pass. #660's final verification records 48/48 `align_driver` `par_map` tests, including 65,537-element worker-range tests for both materializing chunks and @@ -803,7 +851,7 @@ return-provenance slices, L2c cleanup-ABI record plus dynamic Move-return bit, L then L2e mutable borrow/out and all-peer exclusivity, for twenty-three L2b and twenty-seven L2 implementation PRs. The counts are fixed by -#678; #690 now lands am-p, and am-n is the next implementation slice. The required milestone order +#678; #690 lands am-p, #691 lands am-n, and am-h is the next implementation slice. The required milestone order is L2, L3 package-defined/dependent resources, L4 named region capability, L5 deterministic static inputs/Query/command artifacts, and diff --git a/bench/library_boundary/README.md b/bench/library_boundary/README.md index 0e211583..89acda0f 100644 --- a/bench/library_boundary/README.md +++ b/bench/library_boundary/README.md @@ -29,6 +29,9 @@ a 256-function, high-CFG fixture with three expression-valued branches per funct - `mir-nominal-link-validation`: whole-program MIR-lowering milliseconds per iteration for nominal structs/enums, repeated id-free source-shape twins, and one linked library, isolating the nominal/source-identity, enum-base, alignment, and link-name preflight. +- `mir-header-validation`: paired valid and malformed whole-program MIR-lowering milliseconds per + iteration for a large function-header/signature fixture, isolating declaration/header validation + and its canonical-empty failure path. - `mir-continuation-lowering`: L2b-a2-ac whole-program MIR-lowering milliseconds per iteration and the fixture's total basic-block count, tracking the O(1) required-child continuation protocol. diff --git a/bench/library_boundary/src/main.rs b/bench/library_boundary/src/main.rs index ff4db3a9..59adab74 100644 --- a/bench/library_boundary/src/main.rs +++ b/bench/library_boundary/src/main.rs @@ -328,6 +328,39 @@ fn run_provenance() { "mir-nominal-link-validation\t{milliseconds:.3}\tms/lower\t{nominal_definitions}\tdefinitions" ); + let header_source = provenance_fixture(); + let mut source_map = align_span::SourceMap::new(); + let checked = align_driver::check( + &mut source_map, + "mir-header.align", + &header_source, + ); + assert!( + !checked.diags.has_errors(), + "MIR header fixture must check" + ); + let valid = align_driver::lower_to_mir(&checked.hir); + assert!(!valid.fns.is_empty(), "header fixture must lower"); + let mut malformed = checked.hir.clone(); + malformed.fns[0].name.push('\0'); + let invalid = align_driver::lower_to_mir(&malformed); + assert!(invalid.fns.is_empty(), "malformed header must fail closed"); + + let mut iterations = 0_u64; + let start = Instant::now(); + while start.elapsed() < minimum { + let valid = align_driver::lower_to_mir(black_box(&checked.hir)); + let invalid = align_driver::lower_to_mir(black_box(&malformed)); + black_box((valid, invalid)); + iterations += 1; + } + let elapsed = start.elapsed(); + let milliseconds = elapsed.as_secs_f64() * 1_000.0 / iterations as f64; + println!( + "mir-header-validation\t{milliseconds:.3}\tms/valid+malformed-lower\t{}\tfunctions", + checked.hir.fns.len() + ); + let continuation_source = mir_continuation_fixture(); let mut source_map = align_span::SourceMap::new(); let checked = align_driver::check( diff --git a/crates/align_codegen_llvm/src/lib.rs b/crates/align_codegen_llvm/src/lib.rs index cd8ddb8e..1f5bdd8d 100644 --- a/crates/align_codegen_llvm/src/lib.rs +++ b/crates/align_codegen_llvm/src/lib.rs @@ -5022,7 +5022,7 @@ fn declare_fn<'c>( fn declare_imported_fn<'c>( ctx: &'c Context, module: &Module<'c>, - imp: &align_sema::hir::ImportedFn, + imp: &align_mir::ImportedFn, struct_types: &[StructType<'c>], enum_types: &[StructType<'c>], tagged_types: &[StructType<'c>], diff --git a/crates/align_interface/src/codec.rs b/crates/align_interface/src/codec.rs index 0b7d8d8d..84f48446 100644 --- a/crates/align_interface/src/codec.rs +++ b/crates/align_interface/src/codec.rs @@ -538,3 +538,20 @@ pub fn deserialize(bytes: &[u8]) -> Result { } Ok(summary) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn invalid_effect_tag_is_rejected_before_sema() { + let mut reader = Reader::new(&[3]); + assert_eq!( + read_effect(&mut reader), + Err(DecodeError::BadTag { + what: "effect", + tag: 3, + }) + ); + } +} diff --git a/crates/align_mir/src/lib.rs b/crates/align_mir/src/lib.rs index cf121f17..b8a917ea 100644 --- a/crates/align_mir/src/lib.rs +++ b/crates/align_mir/src/lib.rs @@ -77,6 +77,19 @@ pub type ValueId = u32; pub type Slot = u32; pub type BlockId = u32; +/// A bodyless cross-unit declaration after HIR header validation. The checked-HIR `FnEffect` seed +/// is intentionally absent: it is consumed by sema/MIR validation and is not part of the imported +/// Align ABI, structural MIR bytes, implementation hash, or object-cache input. +#[derive(Clone, Debug)] +pub struct ImportedFn { + pub name: String, + pub params: Vec, + pub param_modes: Vec, + pub ret: Ty, + pub return_borrow: hir::ReturnBorrowSummary, + pub return_region: hir::ReturnRegionSummary, +} + #[derive(Clone, Debug)] pub struct Program { pub fns: Vec, @@ -88,7 +101,7 @@ pub struct Program { /// `declare` for each so a `Rvalue::Call` keyed by the mangled `module$name` resolves at link /// time. **Empty in the whole-program path** (byte-identity), populated only by per-unit /// lowering. - pub imported_fns: Vec, + pub imported_fns: Vec, /// External libraries to link (`-l`), passed through from HIR; consumed by the driver. pub link_libs: Vec, /// Struct layouts, indexed by the id in [`Ty::Struct`]; codegen builds LLVM struct @@ -129,7 +142,7 @@ pub struct Function { pub blocks: Vec, pub entry: BlockId, /// M15 S2: whether this function gets `external` linkage under separate compilation (a non-entry - /// `pub` user function; see [`hir::Fn::exportable`]). **Always `false` from whole-program + /// `pub` user function; see [`hir::FnOrigin::is_exportable`]). **Always `false` from whole-program /// lowering** ([`lower_program`]) so the default object is byte-identical to today; set from HIR /// only by per-unit lowering ([`lower_program_per_unit`]). pub exportable: bool, @@ -1523,7 +1536,7 @@ pub fn lower_program_located(program: &hir::Program, sm: &SourceMap) -> Program } /// M15 S2 per-unit lowering: like [`lower_program`], but honors the separate-compilation visibility -/// model — a non-entry `pub` user function ([`hir::Fn::exportable`]) gets `external` linkage in the +/// model — a non-entry `pub` user function ([`hir::FnOrigin::is_exportable`]) gets `external` linkage in the /// object (so a dependent unit's object resolves the cross-unit call), and the unit's imported /// `pub` declarations ([`hir::Program::imported_fns`]) are carried through as bodyless external /// declares. The whole-program [`lower_program`] forces every function `internal` and drops any @@ -1566,6 +1579,7 @@ fn hir_program_is_valid(program: &hir::Program) -> bool { && validate_hir::global_type_metadata_is_valid(program) && validate_hir::type_placement_metadata_is_valid(program) && validate_hir::nominal_link_metadata_is_valid(program) + && validate_hir::declaration_header_metadata_is_valid(program) } fn empty_program() -> Program { @@ -1604,7 +1618,7 @@ fn lower_program_unchecked( ); // Separate-compilation visibility (per-unit lowering only); whole-program lowering keeps // every function `internal` for byte-identity. - mf.exportable = per_unit && f.exportable; + mf.exportable = per_unit && f.origin.is_exportable(); simplify_known_drop_flags(&mut mf); fuse_builder_writes(&mut mf); mf @@ -1624,7 +1638,22 @@ fn lower_program_unchecked( externs: program.externs.clone(), // Cross-unit `pub` callee declares are a per-unit-only concern; the whole-program path has // every callee body in `fns`, so its declare list is empty (byte-identity). - imported_fns: if per_unit { program.imported_fns.clone() } else { Vec::new() }, + imported_fns: if per_unit { + program + .imported_fns + .iter() + .map(|import| ImportedFn { + name: import.name.clone(), + params: import.params.clone(), + param_modes: import.param_modes.clone(), + ret: import.ret, + return_borrow: import.return_borrow.clone(), + return_region: import.return_region.clone(), + }) + .collect() + } else { + Vec::new() + }, link_libs, structs: program.structs.clone(), enums: program.enums.clone(), diff --git a/crates/align_mir/src/validate_hir.rs b/crates/align_mir/src/validate_hir.rs index e6792d91..b8e3bfa8 100644 --- a/crates/align_mir/src/validate_hir.rs +++ b/crates/align_mir/src/validate_hir.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use align_sema::{Layout, PrimScalar, Scalar, Ty, hir}; +use align_span::Span; /// Validate the program-global HIR type domain before MIR construction. pub(crate) fn global_type_metadata_is_valid(program: &hir::Program) -> bool { @@ -27,6 +28,414 @@ pub(crate) fn nominal_link_metadata_is_valid(program: &hir::Program) -> bool { NominalLinkValidator::new(program).validate() } +/// Validate body-independent declaration and header records before MIR copies HIR. +/// +/// This pass intentionally does not inspect function bodies or `drop_individual_exprs`. Body +/// ownership/effect replay belongs to am-b4; the fields validated here are the producer-owned +/// declaration, signature, origin, local-header, and structural drop-set facts that do not require +/// expression semantics. +pub(crate) fn declaration_header_metadata_is_valid(program: &hir::Program) -> bool { + DeclarationValidator::new(program).validate() +} + +struct DeclarationValidator<'a> { + program: &'a hir::Program, + placement: PlacementValidator<'a>, +} + +impl<'a> DeclarationValidator<'a> { + fn new(program: &'a hir::Program) -> Self { + Self { + program, + placement: PlacementValidator::new(program), + } + } + + fn validate(&self) -> bool { + self.externs_valid() + && self.imported_functions_valid() + && self.function_types_valid() + && self.stored_functions_valid() + } + + fn externs_valid(&self) -> bool { + let mut names = HashSet::new(); + for function in &self.program.externs { + if function.name == "main" + || !valid_declaration_name(&function.name) + || !names.insert(function.name.as_str()) + || function.params.len() != function.param_modes.len() + || !function + .param_modes + .iter() + .all(|mode| *mode == align_ast::ParamMode::ByValue) + || !function + .params + .iter() + .all(|&ty| self.placement.ffi_parameter_ok(ty)) + || !self.placement.ffi_return_ok(function.ret) + || !summary_is_none(&function.return_borrow, &function.return_region) + { + return false; + } + } + true + } + + fn imported_functions_valid(&self) -> bool { + let mut names = HashSet::new(); + for function in &self.program.imported_fns { + if function.name == "main" + || !valid_declaration_name(&function.name) + || !names.insert(function.name.as_str()) + || !self.source_signature_valid( + &function.params, + &function.param_modes, + function.ret, + &function.return_borrow, + &function.return_region, + ) + { + return false; + } + // `FnEffect` is a closed Rust enum. The producer has already normalized an absent + // external-map entry to `Impure`; no body effect is inferred at this boundary. + match function.effect { + align_sema::FnEffect::Pure + | align_sema::FnEffect::Impure + | align_sema::FnEffect::Unknown => {} + } + } + true + } + + fn function_types_valid(&self) -> bool { + for (id, function) in self.program.fn_types.iter().enumerate() { + let Ok(id) = u32::try_from(id) else { + return false; + }; + let allow_param = self.placement.is_abstract(Node::Fn(id)); + if function.params.iter().any(|(mode, scalar)| { + !mode_is_valid(*mode, align_sema::scalar_to_ty(*scalar), true) + || !self.placement.scalar_ok( + *scalar, + ScalarPlacement::FnParameter { allow_param }, + ) + }) || !self.placement.resolve_type_ok(function.ret, allow_param) + || !summary_valid( + self.program, + &function.return_borrow, + &function.return_region, + &function + .params + .iter() + .map(|(_, scalar)| align_sema::scalar_to_ty(*scalar)) + .collect::>(), + ) + { + return false; + } + } + true + } + + fn stored_functions_valid(&self) -> bool { + let mut names = HashSet::new(); + for function in &self.program.fns { + if !valid_declaration_name(&function.name) + || !names.insert(function.name.as_str()) + || !valid_span(function.span) + || !self.parameter_vector_valid(function) + || !self.function_signature_valid(function) + || !self.origin_valid(function) + || !self.locals_valid(function) + || !self.drop_sets_valid(function) + || !self.main_valid(function) + { + return false; + } + } + true + } + + fn origin_valid(&self, function: &hir::Fn) -> bool { + match function.origin { + hir::FnOrigin::Source { .. } | hir::FnOrigin::Monomorph => true, + hir::FnOrigin::Lifted { capture_count } => { + usize::try_from(capture_count).is_ok_and(|count| count <= function.params.len()) + && function + .param_modes + .iter() + .all(|mode| *mode == align_ast::ParamMode::ByValue) + && summary_is_none( + &function.return_borrow, + &function.return_region, + ) + } + } + } + + fn parameter_vector_valid(&self, function: &hir::Fn) -> bool { + if function.params.len() != function.param_modes.len() { + return false; + } + let mut seen = HashSet::new(); + for (&local_id, &mode) in function.params.iter().zip(&function.param_modes) { + if !seen.insert(local_id) + || !mode_is_valid( + mode, + function + .locals + .get(local_id as usize) + .map(|local| local.ty) + .unwrap_or(Ty::Error), + true, + ) + { + return false; + } + let Some(local) = function.locals.get(local_id as usize) else { + return false; + }; + if local.id != local_id { + return false; + } + } + true + } + + fn locals_valid(&self, function: &hir::Fn) -> bool { + let parameter_ids: HashSet = function.params.iter().copied().collect(); + let lifted = matches!(function.origin, hir::FnOrigin::Lifted { .. }); + let mut names = HashSet::new(); + for (index, local) in function.locals.iter().enumerate() { + let Ok(index) = u32::try_from(index) else { + return false; + }; + if local.id != index + || !valid_local_name(&local.name) + || !names.insert(local.name.as_str()) + || local.align.is_some_and(|align| { + !align.is_power_of_two() || align > (1u32 << 29) + }) + { + return false; + } + let is_signature_parameter = parameter_ids.contains(&local.id); + if is_signature_parameter { + if !valid_member_name(&local.name) + || local.is_param == lifted + || local.align.is_some() + { + return false; + } + } else if local.is_param { + return false; + } + if local.align.is_some() + && (local.is_param + || !matches!(local.ty, Ty::Array(Scalar::Int(_) | Scalar::Float(_), _))) + { + return false; + } + } + true + } + + fn function_signature_valid(&self, function: &hir::Fn) -> bool { + let parameter_types: Vec = function + .params + .iter() + .filter_map(|&id| function.locals.get(id as usize).map(|local| local.ty)) + .collect(); + parameter_types.len() == function.params.len() + && parameter_types + .iter() + .all(|&ty| self.placement.source_function_type_ok(ty, true, false)) + && self + .placement + .source_function_type_ok(function.ret, false, true) + && summary_valid( + self.program, + &function.return_borrow, + &function.return_region, + ¶meter_types, + ) + } + + fn drop_sets_valid(&self, function: &hir::Fn) -> bool { + let valid_set = |ids: &[hir::LocalId]| { + ids.windows(2).all(|pair| pair[0] < pair[1]) + && ids.iter().all(|&id| (id as usize) < function.locals.len()) + }; + valid_set(&function.drop_locals) + && valid_set(&function.drop_individual_locals) + && function + .drop_individual_locals + .iter() + .all(|id| function.drop_locals.binary_search(id).is_ok()) + } + + fn main_valid(&self, function: &hir::Fn) -> bool { + if function.name != "main" { + return true; + } + if !matches!( + function.origin, + hir::FnOrigin::Source { is_entry: true, .. } + ) { + return false; + } + let exact_i32 = Ty::Int(align_sema::IntTy { + bits: 32, + signed: true, + }); + let error_id = self.builtin_error_id(); + let result = error_id.map(|id| Ty::Result(Scalar::Unit, Scalar::Enum(id))); + let return_ok = function.ret == Ty::Unit + || function.ret == exact_i32 + || result == Some(function.ret); + let no_args = function.params.is_empty(); + let argv = function.params.len() == 1 + && function.param_modes == [align_ast::ParamMode::ByValue] + && function + .locals + .get(function.params[0] as usize) + .is_some_and(|local| local.ty == Ty::DynArray(Scalar::Str)); + return_ok && (no_args || (argv && result == Some(function.ret))) + } + + fn builtin_error_id(&self) -> Option { + let mut found = None; + for (id, definition) in self.program.enums.iter().enumerate() { + if definition.name == "Error" + && definition.source_name == "Error" + && builtin_error_shape(definition) + { + if found.is_some() { + return None; + } + found = Some(u32::try_from(id).ok()?); + } + } + found + } + + fn source_signature_valid( + &self, + params: &[Ty], + modes: &[align_ast::ParamMode], + ret: Ty, + borrow: &hir::ReturnBorrowSummary, + region: &hir::ReturnRegionSummary, + ) -> bool { + params.len() == modes.len() + && modes + .iter() + .zip(params) + .all(|(&mode, &ty)| { + mode_is_valid(mode, ty, true) + && self.placement.source_function_type_ok(ty, true, false) + }) + && self.placement.source_function_type_ok(ret, false, true) + && summary_valid(self.program, borrow, region, params) + } +} + +fn valid_declaration_name(name: &str) -> bool { + !name.is_empty() && !name.as_bytes().contains(&0) +} + +fn valid_local_name(name: &str) -> bool { + valid_declaration_name(name) +} + +fn valid_span(span: Span) -> bool { + span.lo <= span.hi +} + +fn mode_is_valid(mode: align_ast::ParamMode, ty: Ty, allow_out: bool) -> bool { + match mode { + align_ast::ParamMode::ByValue => true, + align_ast::ParamMode::Out => allow_out && matches!(ty, Ty::Slice(_)), + align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut => false, + } +} + +fn summary_is_none( + borrow: &hir::ReturnBorrowSummary, + region: &hir::ReturnRegionSummary, +) -> bool { + matches!(borrow, hir::ReturnBorrowSummary::None) + && matches!(region, hir::ReturnRegionSummary::None) +} + +fn summary_valid( + program: &hir::Program, + borrow: &hir::ReturnBorrowSummary, + region: &hir::ReturnRegionSummary, + params: &[Ty], +) -> bool { + match (borrow, region) { + (hir::ReturnBorrowSummary::None, hir::ReturnRegionSummary::None) => true, + ( + hir::ReturnBorrowSummary::Roots { + params: borrow_params, + captures: borrow_captures, + }, + hir::ReturnRegionSummary::Roots { + params: region_params, + captures: region_captures, + }, + ) => { + !borrow_params.is_empty() + && borrow_params == region_params + && borrow_captures.is_empty() + && region_captures.is_empty() + && borrow_params.windows(2).all(|pair| pair[0] < pair[1]) + && borrow_params.iter().all(|&id| { + params + .get(id as usize) + .is_some_and(|&ty| { + align_sema::ty_may_borrow( + ty, + &program.structs, + &program.tuples, + &program.enums, + &program.tagged_types, + ) + }) + }) + } + _ => false, + } +} + +fn builtin_error_shape(definition: &hir::EnumDef) -> bool { + if definition.variants.len() != 5 { + return false; + } + let names = ["NotFound", "Invalid", "Denied", "Timeout", "Code"]; + definition + .variants + .iter() + .zip(names) + .all(|(variant, name)| { + variant.name == name + && variant.field_base == 1 + && match name { + "Code" => { + variant.payload.as_slice() + == [Scalar::Int(align_sema::IntTy { + bits: 32, + signed: true, + })] + } + _ => variant.payload.is_empty(), + } + }) +} + #[derive(Clone, Copy, PartialEq, Eq)] enum NominalKind { Struct, diff --git a/crates/align_mir/src/validate_hir_tests.rs b/crates/align_mir/src/validate_hir_tests.rs index 0a99516a..f62677e9 100644 --- a/crates/align_mir/src/validate_hir_tests.rs +++ b/crates/align_mir/src/validate_hir_tests.rs @@ -8,6 +8,644 @@ use align_sema::{ }; use std::cell::Cell; +fn declaration_header_program() -> hir::Program { + let mut program = baseline_program(); + let slice_i32 = Ty::Slice(scalar_int(32)); + program.externs.push(hir::ExternFn { + name: "c_read".to_string(), + params: vec![int(64)], + param_modes: vec![align_ast::ParamMode::ByValue], + ret: int(64), + return_borrow: ReturnBorrowSummary::None, + return_region: ReturnRegionSummary::None, + }); + program.imported_fns.push(ImportedFn { + name: "dep$read".to_string(), + params: vec![slice_i32], + param_modes: vec![align_ast::ParamMode::Out], + ret: slice_i32, + return_borrow: ReturnBorrowSummary::Roots { + params: vec![0], + captures: Vec::new(), + }, + return_region: ReturnRegionSummary::Roots { + params: vec![0], + captures: Vec::new(), + }, + effect: FnEffect::Pure, + }); + let span = align_span::Span::new(0, 0, 0); + program.fns.push(hir::Fn { + name: "worker".to_string(), + origin: hir::FnOrigin::Source { + is_entry: false, + is_public: true, + }, + params: vec![0], + param_modes: vec![align_ast::ParamMode::ByValue], + ret: Ty::Str, + return_borrow: ReturnBorrowSummary::Roots { + params: vec![0], + captures: Vec::new(), + }, + return_region: ReturnRegionSummary::Roots { + params: vec![0], + captures: Vec::new(), + }, + locals: vec![hir::Local { + id: 0, + name: "value".to_string(), + ty: Ty::Str, + is_mut: false, + is_param: true, + align: None, + }, hir::Local { + id: 1, + name: "unused".to_string(), + ty: Ty::Str, + is_mut: false, + is_param: false, + align: None, + }], + body: hir::Block { + stmts: Vec::new(), + value: Some(Box::new(hir::Expr { + kind: hir::ExprKind::Local(0), + ty: Ty::Str, + span, + })), + }, + span, + drop_locals: Vec::new(), + drop_individual_locals: Vec::new(), + drop_individual_exprs: Default::default(), + }); + program +} + +fn fn_type_header_program() -> hir::Program { + let mut program = baseline_program(); + program.fn_types[0] = FnTy { + params: vec![ + (align_ast::ParamMode::ByValue, Scalar::Str), + (align_ast::ParamMode::ByValue, Scalar::Str), + ], + ret: Ty::Str, + return_borrow: ReturnBorrowSummary::Roots { + params: vec![0, 1], + captures: Vec::new(), + }, + return_region: ReturnRegionSummary::Roots { + params: vec![0, 1], + captures: Vec::new(), + }, + effect: Cell::new(FnEffect::Pure), + }; + program +} + +fn summary_header_program() -> hir::Program { + let mut program = declaration_header_program(); + program.fns[0].params = vec![0, 1]; + program.fns[0].param_modes = vec![ + align_ast::ParamMode::ByValue, + align_ast::ParamMode::ByValue, + ]; + program.fns[0].locals[1].ty = Ty::Str; + program.fns[0].locals[1].is_param = true; + program.fns[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![0, 1], + captures: Vec::new(), + }; + program.fns[0].return_region = ReturnRegionSummary::Roots { + params: vec![0, 1], + captures: Vec::new(), + }; + program +} + +fn main_header_program(params: Vec, param_modes: Vec, ret: Ty) -> hir::Program { + let mut program = baseline_program(); + push_builtin_error(&mut program); + let span = align_span::Span::new(0, 0, 0); + let local_params = params + .iter() + .enumerate() + .map(|(id, &ty)| hir::Local { + id: id as u32, + name: format!("arg{id}"), + ty, + is_mut: false, + is_param: true, + align: None, + }) + .collect(); + program.fns.push(hir::Fn { + name: "main".to_string(), + origin: hir::FnOrigin::Source { + is_entry: true, + is_public: false, + }, + params: (0..params.len()).map(|id| id as u32).collect(), + param_modes, + ret, + return_borrow: ReturnBorrowSummary::None, + return_region: ReturnRegionSummary::None, + locals: local_params, + body: hir::Block { + stmts: Vec::new(), + value: None, + }, + span, + drop_locals: Vec::new(), + drop_individual_locals: Vec::new(), + drop_individual_exprs: Default::default(), + }); + program +} + +fn assert_header_rejected(label: &str, program: &hir::Program) { + assert!( + validate_hir::global_type_metadata_is_valid(program), + "{label}: header fixture must remain graph-valid" + ); + assert!( + validate_hir::type_placement_metadata_is_valid(program), + "{label}: header fixture must remain placement-valid" + ); + assert!( + validate_hir::nominal_link_metadata_is_valid(program), + "{label}: header fixture must remain nominal/link-valid" + ); + assert!( + !validate_hir::declaration_header_metadata_is_valid(program), + "{label}: declaration/header validator accepted malformed metadata" + ); + let source_map = SourceMap::new(); + for lowered in [ + lower_program(program), + lower_program_located(program, &source_map), + lower_program_per_unit(program), + lower_program_per_unit_located(program, &source_map), + ] { + assert!(is_empty(&lowered), "{label}: invalid header published MIR"); + } +} + +#[test] +fn malformed_hir_declaration_header_metadata_fails_closed() { + let base = declaration_header_program(); + assert_one_header_mutation("extern-name", &base, |program| { + program.externs[0].name.push('\0'); + }); + assert_one_header_mutation("extern-main", &base, |program| { + program.externs[0].name = "main".to_string(); + }); + assert_one_header_mutation("extern-arity", &base, |program| { + program.externs[0].param_modes.clear(); + }); + assert_one_header_mutation("extern-mode", &base, |program| { + program.externs[0].param_modes[0] = align_ast::ParamMode::Out; + }); + assert_one_header_mutation("extern-summary", &base, |program| { + program.externs[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![0], + captures: Vec::new(), + }; + }); + assert_one_header_mutation("extern-duplicate-name", &base, |program| { + program.externs.push(program.externs[0].clone()); + }); + assert_one_header_mutation("import-name", &base, |program| { + program.imported_fns[0].name.clear(); + }); + assert_one_header_mutation("import-main", &base, |program| { + program.imported_fns[0].name = "main".to_string(); + }); + assert_one_header_mutation("import-mode", &base, |program| { + program.imported_fns[0].param_modes[0] = align_ast::ParamMode::Borrow; + }); + assert_one_header_mutation("import-summary-order", &base, |program| { + program.imported_fns[0].return_region = ReturnRegionSummary::None; + }); + assert_one_header_mutation("import-summary-captures", &base, |program| { + program.imported_fns[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![0], + captures: vec![0], + }; + }); + assert_one_header_mutation("import-summary-range", &base, |program| { + program.imported_fns[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![1], + captures: Vec::new(), + }; + program.imported_fns[0].return_region = ReturnRegionSummary::Roots { + params: vec![1], + captures: Vec::new(), + }; + }); + assert_one_header_mutation("import-summary-non-borrowable", &base, |program| { + program.imported_fns[0].params[0] = int(64); + program.imported_fns[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![0], + captures: Vec::new(), + }; + program.imported_fns[0].return_region = ReturnRegionSummary::Roots { + params: vec![0], + captures: Vec::new(), + }; + }); + assert_one_header_mutation("import-duplicate-name", &base, |program| { + program.imported_fns.push(program.imported_fns[0].clone()); + }); + assert_one_header_mutation("stored-name", &base, |program| { + program.fns[0].name.push('\0'); + }); + assert_one_header_mutation("stored-duplicate-name", &base, |program| { + program.fns.push(program.fns[0].clone()); + }); + assert_one_header_mutation("stored-span", &base, |program| { + program.fns[0].span = align_span::Span::new(0, 2, 1); + }); + assert_one_header_mutation("stored-parameter-id", &base, |program| { + program.fns[0].params[0] = 1; + }); + assert_one_header_mutation("stored-parameter-mode", &base, |program| { + program.fns[0].param_modes[0] = align_ast::ParamMode::BorrowMut; + }); + assert_one_header_mutation("stored-parameter-name", &base, |program| { + program.fns[0].locals[0].name = "bad-name".to_string(); + }); + assert_one_header_mutation("stored-local-id", &base, |program| { + program.fns[0].locals[0].id = 1; + }); + assert_one_header_mutation("stored-local-name", &base, |program| { + program.fns[0].locals[0].name.clear(); + }); + assert_one_header_mutation("stored-local-parameter-bit", &base, |program| { + program.fns[0].locals[0].is_param = false; + }); + assert_one_header_mutation("stored-local-alignment", &base, |program| { + program.fns[0].locals.push(hir::Local { + id: 2, + name: "aligned".to_string(), + ty: int(32), + is_mut: false, + is_param: false, + align: Some(3), + }); + }); + assert_one_header_mutation("stored-drop-order", &base, |program| { + program.fns[0].drop_locals = vec![0, 0]; + }); + assert_one_header_mutation("stored-drop-subset", &base, |program| { + program.fns[0].drop_individual_locals = vec![0]; + program.fns[0].drop_locals.clear(); + }); + assert_one_header_mutation("stored-drop-range", &base, |program| { + program.fns[0].drop_locals = vec![2]; + }); + assert_one_header_mutation("stored-individual-drop-range", &base, |program| { + program.fns[0].drop_individual_locals = vec![2]; + }); + assert_one_header_mutation("lifted-origin", &base, |program| { + program.fns[0].origin = hir::FnOrigin::Lifted { capture_count: 2 }; + }); + assert_one_header_mutation("lifted-origin-mode", &base, |program| { + program.fns[0].origin = hir::FnOrigin::Lifted { capture_count: 0 }; + program.fns[0].locals[0].is_param = false; + program.fns[0].param_modes[0] = align_ast::ParamMode::Out; + }); + assert_one_header_mutation("lifted-origin-summary", &base, |program| { + program.fns[0].origin = hir::FnOrigin::Lifted { capture_count: 0 }; + program.fns[0].locals[0].is_param = false; + }); + + let fn_base = fn_type_header_program(); + assert!(validate_hir::declaration_header_metadata_is_valid(&fn_base)); + assert_one_header_mutation("fn-type-mode", &fn_base, |program| { + program.fn_types[0].params[0].0 = align_ast::ParamMode::Borrow; + }); + assert_one_header_mutation("fn-type-summary-order", &fn_base, |program| { + program.fn_types[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![1, 0], + captures: Vec::new(), + }; + program.fn_types[0].return_region = ReturnRegionSummary::Roots { + params: vec![1, 0], + captures: Vec::new(), + }; + }); + assert_one_header_mutation("fn-type-summary-duplicate", &fn_base, |program| { + program.fn_types[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![0, 0], + captures: Vec::new(), + }; + program.fn_types[0].return_region = ReturnRegionSummary::Roots { + params: vec![0, 0], + captures: Vec::new(), + }; + }); + assert_one_header_mutation("fn-type-summary-range", &fn_base, |program| { + program.fn_types[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![2], + captures: Vec::new(), + }; + program.fn_types[0].return_region = ReturnRegionSummary::Roots { + params: vec![2], + captures: Vec::new(), + }; + }); + assert_one_header_mutation("fn-type-summary-captures", &fn_base, |program| { + program.fn_types[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![0, 1], + captures: vec![0], + }; + program.fn_types[0].return_region = ReturnRegionSummary::Roots { + params: vec![0, 1], + captures: vec![0], + }; + }); + + let summary_base = summary_header_program(); + assert!(validate_hir::declaration_header_metadata_is_valid(&summary_base)); + assert_one_header_mutation("stored-summary-order", &summary_base, |program| { + program.fns[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![1, 0], + captures: Vec::new(), + }; + program.fns[0].return_region = ReturnRegionSummary::Roots { + params: vec![1, 0], + captures: Vec::new(), + }; + }); + assert_one_header_mutation("stored-summary-range", &summary_base, |program| { + program.fns[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![2], + captures: Vec::new(), + }; + program.fns[0].return_region = ReturnRegionSummary::Roots { + params: vec![2], + captures: Vec::new(), + }; + }); + assert_one_header_mutation("stored-summary-empty", &summary_base, |program| { + program.fns[0].return_borrow = ReturnBorrowSummary::Roots { + params: Vec::new(), + captures: Vec::new(), + }; + program.fns[0].return_region = ReturnRegionSummary::Roots { + params: Vec::new(), + captures: Vec::new(), + }; + }); + assert_one_header_mutation("stored-summary-non-borrowable", &summary_base, |program| { + program.fns[0].locals[1].ty = Ty::String; + program.fns[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![1], + captures: Vec::new(), + }; + program.fns[0].return_region = ReturnRegionSummary::Roots { + params: vec![1], + captures: Vec::new(), + }; + }); + assert_one_header_mutation("stored-summary-captures", &summary_base, |program| { + program.fns[0].return_borrow = ReturnBorrowSummary::Roots { + params: vec![0, 1], + captures: vec![0], + }; + program.fns[0].return_region = ReturnRegionSummary::Roots { + params: vec![0, 1], + captures: vec![0], + }; + }); +} + +#[test] +fn main_header_abi_matrix_is_exhaustive() { + let result = Ty::Result(Scalar::Unit, Scalar::Enum(1)); + let argv = Ty::DynArray(Scalar::Str); + for (label, params, modes, ret) in [ + ("main-unit", Vec::new(), Vec::new(), Ty::Unit), + ("main-i32", Vec::new(), Vec::new(), int(32)), + ("main-result", Vec::new(), Vec::new(), result), + ( + "main-argv-result", + vec![argv], + vec![align_ast::ParamMode::ByValue], + result, + ), + ] { + let program = main_header_program(params, modes, ret); + assert!( + validate_hir::global_type_metadata_is_valid(&program) + && validate_hir::type_placement_metadata_is_valid(&program) + && validate_hir::nominal_link_metadata_is_valid(&program) + && validate_hir::declaration_header_metadata_is_valid(&program), + "{label}: valid main ABI rejected" + ); + } + + assert_header_rejected( + "main-no-arg-unsigned-i32", + &main_header_program( + Vec::new(), + Vec::new(), + Ty::Int(IntTy { + bits: 32, + signed: false, + }), + ), + ); + assert_header_rejected( + "main-no-arg-float", + &main_header_program(Vec::new(), Vec::new(), Ty::Float(FloatTy { bits: 64 })), + ); + assert_header_rejected( + "main-argv-unit", + &main_header_program(vec![argv], vec![align_ast::ParamMode::ByValue], Ty::Unit), + ); + assert_header_rejected( + "main-argv-i32", + &main_header_program(vec![argv], vec![align_ast::ParamMode::ByValue], int(32)), + ); + assert_header_rejected( + "main-argv-mode", + &main_header_program(vec![argv], vec![align_ast::ParamMode::Out], result), + ); + assert_header_rejected( + "main-argv-type", + &main_header_program( + vec![Ty::DynArray(Scalar::Int(IntTy { bits: 64, signed: true }))], + vec![align_ast::ParamMode::ByValue], + result, + ), + ); + assert_header_rejected( + "main-argument-count", + &main_header_program( + vec![argv, argv], + vec![align_ast::ParamMode::ByValue, align_ast::ParamMode::ByValue], + result, + ), + ); + + let source_non_entry = main_header_program(Vec::new(), Vec::new(), Ty::Unit); + assert_one_header_mutation("main-non-entry-origin", &source_non_entry, |program| { + program.fns[0].origin = hir::FnOrigin::Source { + is_entry: false, + is_public: true, + }; + }); + assert_one_header_mutation("main-monomorph-origin", &source_non_entry, |program| { + program.fns[0].origin = hir::FnOrigin::Monomorph; + }); + assert_one_header_mutation("main-lifted-origin", &source_non_entry, |program| { + program.fns[0].origin = hir::FnOrigin::Lifted { capture_count: 0 }; + }); + + let valid_error = main_header_program(vec![argv], vec![align_ast::ParamMode::ByValue], result); + assert_one_header_mutation("main-error-code-width", &valid_error, |program| { + program.enums[1].variants[4].payload = vec![Scalar::Int(IntTy { + bits: 64, + signed: true, + })]; + }); + assert_one_header_mutation("main-error-variant-order", &valid_error, |program| { + program.enums[1].variants.swap(0, 1); + }); + assert_one_header_mutation("main-error-variant-name", &valid_error, |program| { + program.enums[1].variants[0].name = "Unknown".to_string(); + }); + assert_one_header_mutation("main-error-name", &valid_error, |program| { + program.enums[1].name = "OtherError".to_string(); + }); + assert_one_header_mutation("main-error-source-name", &valid_error, |program| { + program.enums[1].source_name = "OtherError".to_string(); + }); + assert_one_header_mutation("main-error-variant-count", &valid_error, |program| { + program.enums[1].variants.pop(); + }); + assert_one_header_mutation("main-error-extra-variant", &valid_error, |program| { + program.enums[1].variants.insert(4, EnumVariant { + name: "Extra".to_string(), + payload: Vec::new(), + field_base: 1, + }); + }); +} + +#[test] +fn valid_hir_declaration_header_preflight_is_mir_identity() { + let mut base = declaration_header_program(); + for effect in [FnEffect::Pure, FnEffect::Impure, FnEffect::Unknown] { + base.imported_fns[0].effect = effect; + assert!(validate_hir::declaration_header_metadata_is_valid(&base)); + let source_map = SourceMap::new(); + let checked = lower_program_per_unit(&base); + let unchecked = lower_program_unchecked(&base, None, true); + assert_eq!(format!("{checked:#?}"), format!("{unchecked:#?}")); + let located = lower_program_per_unit_located(&base, &source_map); + let located_unchecked = lower_program_unchecked( + &base, + Some(Rc::new(SourceLines::from_map(&source_map))), + true, + ); + assert_eq!(format!("{located:#?}"), format!("{located_unchecked:#?}")); + assert_eq!(checked.imported_fns.len(), 1); + assert_eq!(checked.imported_fns[0].name, "dep$read"); + } + + let mut lifted = declaration_header_program(); + lifted.fns[0].origin = hir::FnOrigin::Lifted { capture_count: 0 }; + lifted.fns[0].locals[0].is_param = false; + lifted.fns[0].return_borrow = ReturnBorrowSummary::None; + lifted.fns[0].return_region = ReturnRegionSummary::None; + assert!(validate_hir::declaration_header_metadata_is_valid(&lifted)); +} + +#[test] +fn valid_header_does_not_consume_body_facts() { + let mut program = declaration_header_program(); + program.fns[0] + .drop_individual_exprs + .insert(align_span::Span::new(999, 4, 4), false); + assert!(validate_hir::declaration_header_metadata_is_valid(&program)); + assert!(!is_empty(&lower_program(&program))); + + let mut body_local_type = declaration_header_program(); + body_local_type.fns[0].locals.push(hir::Local { + id: 2, + name: "task_value".to_string(), + ty: Ty::Task(Scalar::Int(IntTy { bits: 64, signed: true })), + is_mut: false, + is_param: false, + align: None, + }); + assert!(validate_hir::declaration_header_metadata_is_valid(&body_local_type)); +} + +#[test] +fn deep_hir_header_type_dag_is_stack_bounded() { + let mut program = baseline_program(); + program.structs = (0..4_096) + .map(|index| StructDef { + name: format!("HeaderDeep{index}"), + source_name: format!("HeaderDeep{index}"), + fields: vec![FieldDef { + name: "next".to_string(), + ty: if index == 4_095 { + Ty::Str + } else { + Ty::Struct(index + 1) + }, + }], + align: None, + c_repr: false, + }) + .collect(); + program.imported_fns.push(ImportedFn { + name: "deep$header".to_string(), + params: vec![Ty::Struct(0)], + param_modes: vec![align_ast::ParamMode::ByValue], + ret: Ty::Struct(0), + return_borrow: ReturnBorrowSummary::Roots { + params: vec![0], + captures: Vec::new(), + }, + return_region: ReturnRegionSummary::Roots { + params: vec![0], + captures: Vec::new(), + }, + effect: FnEffect::Unknown, + }); + assert!(validate_hir::declaration_header_metadata_is_valid(&program)); + assert!(!is_empty(&lower_program_per_unit(&program))); + + let mut malformed = program.clone(); + malformed.imported_fns.push(ImportedFn { + name: String::new(), + params: Vec::new(), + param_modes: Vec::new(), + ret: Ty::Unit, + return_borrow: ReturnBorrowSummary::None, + return_region: ReturnRegionSummary::None, + effect: FnEffect::Impure, + }); + assert_header_rejected("deep-header-later-sibling", &malformed); +} + +fn assert_one_header_mutation( + label: &str, + base: &hir::Program, + mutate: impl FnOnce(&mut hir::Program), +) { + let mut program = base.clone(); + mutate(&mut program); + assert_header_rejected(label, &program); +} + fn int(bits: u8) -> Ty { Ty::Int(IntTy { bits, signed: true }) } @@ -119,6 +757,7 @@ fn imported_fn(name: &str, params: Vec, ret: Ty) -> ImportedFn { ret, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + effect: FnEffect::Pure, } } @@ -183,6 +822,7 @@ fn with_return(ty: Ty) -> hir::Program { ret: ty, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + effect: FnEffect::Pure, }); program } @@ -201,7 +841,9 @@ fn is_empty(program: &Program) -> bool { fn assert_rejected(label: &str, program: &hir::Program) { assert!( !validate_hir::global_type_metadata_is_valid(program) - || !validate_hir::type_placement_metadata_is_valid(program), + || !validate_hir::type_placement_metadata_is_valid(program) + || !validate_hir::nominal_link_metadata_is_valid(program) + || !validate_hir::declaration_header_metadata_is_valid(program), "{label}: validator accepted malformed metadata" ); let source_map = SourceMap::new(); @@ -716,6 +1358,14 @@ fn assert_accepted_impl(label: &str, program: &hir::Program, owner: Option hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: int(64), @@ -782,7 +1432,6 @@ fn with_unary_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -852,7 +1501,7 @@ fn with_mixed_eager_body_depth(depth: usize) -> hir::Program { .push(imported_fn("dep$id_i64", vec![int(64)], int(64))); program.fns.push(hir::Fn { name: "deep_mixed_eager".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret, @@ -867,7 +1516,6 @@ fn with_mixed_eager_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -879,7 +1527,7 @@ fn with_str_trim_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_str_trim".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: Ty::Str, @@ -894,7 +1542,6 @@ fn with_str_trim_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -979,7 +1626,7 @@ fn with_path_string_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_path_string".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: Ty::String, @@ -994,7 +1641,6 @@ fn with_path_string_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs, - exportable: false, }); program } @@ -1021,7 +1667,7 @@ fn with_reader_buffered_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_reader_buffered".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: Ty::Reader, @@ -1036,7 +1682,6 @@ fn with_reader_buffered_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs, - exportable: false, }); program } @@ -1101,7 +1746,7 @@ fn with_bytes_str_cycle_body_depth(depth: usize) -> hir::Program { assert_eq!(expr_depth, target_expr_depth); program.fns.push(hir::Fn { name: "deep_bytes_str_cycle".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: result_ty, @@ -1116,7 +1761,6 @@ fn with_bytes_str_cycle_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -1192,7 +1836,7 @@ fn with_regex_string_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_regex_string".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], ret: Ty::String, @@ -1214,7 +1858,6 @@ fn with_regex_string_body_depth(depth: usize) -> hir::Program { drop_locals: vec![0], drop_individual_locals: vec![0], drop_individual_exprs, - exportable: false, }); program } @@ -1262,7 +1905,7 @@ fn with_template_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_template".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: Ty::String, @@ -1277,7 +1920,6 @@ fn with_template_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs, - exportable: false, }); program } @@ -1302,7 +1944,7 @@ fn with_file_body_depth(depth: usize) -> hir::Program { }; program.fns.push(hir::Fn { name: "deep_file".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: result_ty, @@ -1317,7 +1959,6 @@ fn with_file_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs, - exportable: false, }); program } @@ -1346,7 +1987,7 @@ fn with_array_builder_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_array_builder".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], ret: Ty::Unit, @@ -1368,7 +2009,6 @@ fn with_array_builder_body_depth(depth: usize) -> hir::Program { drop_locals: vec![0], drop_individual_locals: vec![0], drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -1397,7 +2037,7 @@ fn with_process_command_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_process_command".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], // `command` is a body-produced builder, not a source-nameable header type. Keep the @@ -1422,7 +2062,6 @@ fn with_process_command_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs, - exportable: false, }); program } @@ -1450,7 +2089,7 @@ fn with_http_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_http".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), // `http_request` is a body-produced builder, not a source-nameable header type. Keep the @@ -1468,7 +2107,6 @@ fn with_http_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs, - exportable: false, }); program } @@ -1519,7 +2157,7 @@ fn with_block_stmt_body_depth(depth: usize) -> hir::Program { .push(imported_fn("dep$block_stmt_sentinel", Vec::new(), Ty::Unit)); program.fns.push(hir::Fn { name: "deep_block_stmt".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: Ty::Unit, @@ -1534,7 +2172,6 @@ fn with_block_stmt_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -1588,7 +2225,7 @@ fn with_match_arm_body_depth(depth: usize) -> hir::Program { .push(imported_fn("dep$wildcard_sentinel", Vec::new(), Ty::Unit)); program.fns.push(hir::Fn { name: "deep_match_arm".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: expr.ty, @@ -1603,7 +2240,6 @@ fn with_match_arm_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -1674,7 +2310,7 @@ fn with_if_branch_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_if_branch".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret, @@ -1689,7 +2325,6 @@ fn with_if_branch_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -1764,7 +2399,7 @@ fn with_binary_match_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_binary_match".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret, @@ -1779,7 +2414,6 @@ fn with_binary_match_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -1830,7 +2464,7 @@ fn with_conditional_operand_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_conditional_operand".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: Ty::Bool, @@ -1845,7 +2479,6 @@ fn with_conditional_operand_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -1898,7 +2531,7 @@ fn with_scoped_control_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_scoped_control".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret, @@ -1913,7 +2546,6 @@ fn with_scoped_control_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -1960,7 +2592,7 @@ fn with_loop_body_depth(depth: usize) -> hir::Program { let mut program = baseline_program(); program.fns.push(hir::Fn { name: "deep_loop".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: int(64), @@ -1975,7 +2607,6 @@ fn with_loop_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } @@ -2031,7 +2662,7 @@ fn with_stage_body_depth(depth: usize) -> hir::Program { .push(imported_fn("dep$stage_id", vec![int(64), int(64)], int(64))); program.fns.push(hir::Fn { name: "deep_stage".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], ret: int(64), @@ -2053,7 +2684,6 @@ fn with_stage_body_depth(depth: usize) -> hir::Program { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }); program } diff --git a/crates/align_sema/src/hir.rs b/crates/align_sema/src/hir.rs index 6c8edecd..63f2f6b0 100644 --- a/crates/align_sema/src/hir.rs +++ b/crates/align_sema/src/hir.rs @@ -122,6 +122,9 @@ pub struct ImportedFn { pub ret: Ty, pub return_borrow: ReturnBorrowSummary, pub return_region: ReturnRegionSummary, + /// The normalized cross-unit effect fact. This is checked-HIR transport only; MIR strips it + /// after declaration validation because the six-field imported ABI record is unchanged. + pub effect: crate::FnEffect, } #[derive(Clone, Debug)] @@ -248,14 +251,25 @@ pub struct FieldDef { pub ty: Ty, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum FnOrigin { + Source { is_entry: bool, is_public: bool }, + Monomorph, + Lifted { capture_count: u32 }, +} + +impl FnOrigin { + pub fn is_exportable(self) -> bool { + matches!(self, Self::Source { is_entry: false, is_public: true }) + } +} + #[derive(Clone, Debug)] pub struct Fn { pub name: String, - /// `None` for a source-named function or generic monomorph. A lifted lambda records - /// `Some(capture_count)`, including `Some(0)` for a non-capturing lambda. L2b-a1 uses the - /// explicit origin bit to defer the distinct capture-root domain; L2b-b will use the count to - /// split explicit parameters from trailing capture parameters. - pub lifted_capture_count: Option, + /// The producer origin of this stored function. This is HIR-only metadata: MIR derives its + /// existing per-unit exportability bit from it and does not persist the origin itself. + pub origin: FnOrigin, /// Parameter locals, in declaration order. Each is also present in `locals`. pub params: Vec, /// Parameter access modes, parallel to [`Self::params`]. @@ -279,13 +293,6 @@ pub struct Fn { /// lowering uses the per-branch facts to build a runtime ownership join; direct local moves /// still forward their path-local flag instead of consulting this conservative table. pub drop_individual_exprs: std::collections::HashMap, - /// M15 S2: this is a non-entry, non-generic `pub` **user** function — a candidate external - /// symbol under separate compilation (its `module$name` mangling is globally collision-free). - /// The *fact* is mode-independent; whether it actually gets `external` linkage is decided at - /// lowering (whole-program lowering forces every function `internal` for byte-identity; per-unit - /// lowering honors this bit). `false` for the entry unit's functions (nothing imports the - /// entry), monomorphs, and lifted lambdas. - pub exportable: bool, } #[derive(Clone, Debug)] diff --git a/crates/align_sema/src/hir_depth.rs b/crates/align_sema/src/hir_depth.rs index b8f2efea..9523e9bb 100644 --- a/crates/align_sema/src/hir_depth.rs +++ b/crates/align_sema/src/hir_depth.rs @@ -1201,7 +1201,7 @@ mod tests { hir::Program { fns: vec![hir::Fn { name: "deep".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: Vec::new(), param_modes: Vec::new(), ret: int_ty(), @@ -1216,7 +1216,6 @@ mod tests { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }], externs: Vec::new(), link_libs: Vec::new(), @@ -1257,7 +1256,7 @@ mod tests { hir::Program { fns: vec![hir::Fn { name: "deep_move".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], ret: result_ty, @@ -1279,7 +1278,6 @@ mod tests { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }], externs: Vec::new(), link_libs: Vec::new(), @@ -1323,7 +1321,7 @@ mod tests { hir::Program { fns: vec![hir::Fn { name: "deep_move_call".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], ret: Ty::String, @@ -1345,7 +1343,6 @@ mod tests { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }], externs: Vec::new(), link_libs: Vec::new(), @@ -1391,7 +1388,7 @@ mod tests { hir::Program { fns: vec![hir::Fn { name: "deep_move_shape".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], ret, @@ -1413,7 +1410,6 @@ mod tests { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }], externs: Vec::new(), link_libs: Vec::new(), @@ -1833,7 +1829,7 @@ mod tests { hir::Program { fns: vec![hir::Fn { name: "deep_move_control".to_string(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], ret, @@ -1855,7 +1851,6 @@ mod tests { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: Default::default(), - exportable: false, }], externs: Vec::new(), link_libs: Vec::new(), diff --git a/crates/align_sema/src/lib.rs b/crates/align_sema/src/lib.rs index 6ffdd245..47c0d5aa 100644 --- a/crates/align_sema/src/lib.rs +++ b/crates/align_sema/src/lib.rs @@ -4479,6 +4479,10 @@ pub fn check_program_with_interface_facts( if !is_generic && matches!(f.vis, ast::Vis::Pub) { let mangled = mangle_fn(module, is_entry, &f.name.name); if let Some(sig) = sigs.get(&mangled) { + let effect = external_effects + .get(&mangled) + .copied() + .unwrap_or(FnEffect::Impure); imported_fns.push(hir::ImportedFn { name: mangled, params: sig.params.clone(), @@ -4486,6 +4490,7 @@ pub fn check_program_with_interface_facts( ret: sig.ret, return_borrow: sig.return_borrow.clone(), return_region: sig.return_region.clone(), + effect, }); } } @@ -4545,7 +4550,10 @@ pub fn check_program_with_interface_facts( // under separate compilation (its `module$name` mangling is collision-free). The entry // unit's functions are never imported (nothing imports the entry), so they stay // internal — which also keeps a single-file (N=1) build byte-identical to today. - checked.exportable = !is_entry && matches!(f.vis, ast::Vis::Pub); + checked.origin = hir::FnOrigin::Source { + is_entry, + is_public: matches!(f.vis, ast::Vis::Pub), + }; worklist.extend(cx.instantiations); fns.push(checked); fns.extend(lifted); @@ -4605,6 +4613,7 @@ pub fn check_program_with_interface_facts( ); let mut checked = cx.check_fn(decl); checked.name = mangled; + checked.origin = hir::FnOrigin::Monomorph; worklist.extend(cx.instantiations); // A lambda / pipeline inside a generic function is already rejected when the template is // checked in Pass 2, so a monomorph never has lifted helpers here. @@ -4885,7 +4894,7 @@ fn infer_return_provenance( // Lifted lambda parameters append captures after explicit arguments. Treating those slots // as ordinary named parameters would publish the wrong domain and disagree with the // closure's explicit-parameter signature. L2b-b infers both domains atomically. - if function.lifted_capture_count.is_some() { + if matches!(function.origin, hir::FnOrigin::Lifted { .. }) { continue; } @@ -4997,7 +5006,7 @@ fn check_parallelism( .fns .iter() .filter(|function| { - function.exportable + function.origin.is_exportable() && function.params.iter().any(|local| { function .locals @@ -5197,7 +5206,7 @@ impl EffectOpenWorld<'_> { match self { Self::Closed => false, Self::Export(name) => { - function.exportable && function.name == name + function.origin.is_exportable() && function.name == name } } } @@ -5542,7 +5551,7 @@ pub fn fn_effects( .fns .iter() .filter(|function| { - function.exportable + function.origin.is_exportable() && function.params.iter().any(|local| { function .locals @@ -19169,7 +19178,7 @@ impl<'a, 't> Checker<'a, 't> { Fn { name: f.name.name.clone(), - lifted_capture_count: None, + origin: hir::FnOrigin::Source { is_entry: false, is_public: false }, params, param_modes: f.params.iter().map(|p| p.mode).collect(), ret: self.finalize(ret), @@ -19181,7 +19190,6 @@ impl<'a, 't> Checker<'a, 't> { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: std::collections::HashMap::new(), - exportable: false, } } @@ -23521,6 +23529,7 @@ impl<'a, 't> Checker<'a, 't> { .collect(); // Swap in fresh per-function state; the lambda is a separate function body. + let saved_lifted_len = self.lifted.len(); let saved_locals = std::mem::take(&mut self.locals); let saved_scope = std::mem::take(&mut self.scope); let saved_int_vars = std::mem::take(&mut self.int_vars); @@ -23584,6 +23593,34 @@ impl<'a, 't> Checker<'a, 't> { // Collect captures: each becomes a trailing parameter of the lifted function, and the // enclosing local is passed at the call site. Slice ③ supports copy-value captures only. let captured = self.capture.take().unwrap().captured; + let capture_count = match u32::try_from(captured.len()) { + Ok(count) => count, + Err(_) => { + self.diags.error( + "a lambda captures more values than the compiler can represent".to_string(), + span, + ); + self.locals = saved_locals; + self.scope = saved_scope; + self.int_vars = saved_int_vars; + self.int_parent = saved_int_parent; + self.float_vars = saved_float_vars; + self.float_parent = saved_float_parent; + self.ret_hint = saved_ret; + self.arena_depth = saved_arena; + self.unsafe_depth = saved_unsafe_depth; + self.extern_boundary_error = saved_extern_boundary_error || extern_boundary_error; + self.task_group_depth = saved_tg_depth; + self.wait_state = saved_wait_state; + self.task_group_fallible = saved_tg_fallible; + self.loops = saved_loops; + self.slice_bases = saved_bases; + self.buffered_readers = saved_buffered_readers; + self.capture = saved_capture; + self.lifted.truncate(saved_lifted_len); + return None; + } + }; let mut locals = std::mem::take(&mut self.locals); for l in &mut locals { l.ty = self.finalize(l.ty); @@ -23605,7 +23642,7 @@ impl<'a, 't> Checker<'a, 't> { let name = format!("{}$lambda{}", self.cur_fn, self.lifted.len()); self.lifted.push(hir::Fn { name: name.clone(), - lifted_capture_count: Some(captured.len()), + origin: hir::FnOrigin::Lifted { capture_count }, params: param_ids, param_modes: vec![ ast::ParamMode::ByValue; @@ -23620,7 +23657,6 @@ impl<'a, 't> Checker<'a, 't> { drop_locals: Vec::new(), drop_individual_locals: Vec::new(), drop_individual_exprs: std::collections::HashMap::new(), - exportable: false, }); // Restore the enclosing function's state. @@ -34884,6 +34920,7 @@ fn main() -> i32 = 0 "\ fn identity(value: T) -> T = value fn named(value: i64) -> i64 = value +pub fn exported(value: i64) -> i64 = value fn main() -> i32 { captured := 2 plain := [1, 2].map(fn value { value + 1 }).sum() @@ -34899,22 +34936,63 @@ fn main() -> i32 { .iter() .find(|function| function.name == "named") .expect("ordinary named function") - .lifted_capture_count, - None + .origin, + hir::FnOrigin::Source { + is_entry: true, + is_public: false, + } + ); + assert_eq!( + program + .fns + .iter() + .find(|function| function.name == "exported") + .expect("public source function") + .origin, + hir::FnOrigin::Source { + is_entry: true, + is_public: true, + } + ); + assert!(! + program + .fns + .iter() + .find(|function| function.name == "exported") + .expect("public source function") + .origin + .is_exportable() + ); + let main = program + .fns + .iter() + .find(|function| function.name == "main") + .expect("entry source function"); + assert_eq!( + main.origin, + hir::FnOrigin::Source { + is_entry: true, + is_public: false, + } ); + assert!(!main.origin.is_exportable()); assert_eq!( program .fns .iter() .find(|function| function.name.starts_with("identity$")) .expect("generic monomorph") - .lifted_capture_count, - None + .origin, + hir::FnOrigin::Monomorph ); + assert!(!hir::FnOrigin::Monomorph.is_exportable()); let mut lifted = program .fns .iter() - .filter_map(|function| function.lifted_capture_count) + .filter_map(|function| match function.origin { + hir::FnOrigin::Lifted { capture_count } => Some(capture_count), + _ => None, + }) .collect::>(); lifted.sort_unstable(); assert_eq!( @@ -34922,6 +35000,116 @@ fn main() -> i32 { vec![0, 1], "non-capturing and one-capture lambdas keep distinct explicit metadata" ); + assert!( + program.fns.iter().filter(|function| matches!( + function.origin, + hir::FnOrigin::Lifted { .. } + )).all(|function| !function.origin.is_exportable()) + ); + } + + #[test] + fn imported_effect_facts_are_normalized_and_stripped() { + let mut diagnostics = Diagnostics::new(); + let lib_source = "module lib\npub fn read(value: i64) -> i64 = value\n"; + let main_source = "module main\nimport lib\nfn main() -> i32 = 0\n"; + let lib_tokens = tokenize(1, lib_source, &mut diagnostics); + let lib = parse_file(lib_tokens, &mut diagnostics); + let main_tokens = tokenize(0, main_source, &mut diagnostics); + let main = parse_file(main_tokens, &mut diagnostics); + assert!( + !diagnostics.has_errors(), + "fixture parse failed: {:?}", + diagnostics.iter().map(|diagnostic| &diagnostic.message).collect::>() + ); + let modules = [ + Module { + path: "lib".to_string(), + file: &lib, + is_entry: false, + interface_only: true, + }, + Module { + path: "main".to_string(), + file: &main, + is_entry: true, + interface_only: false, + }, + ]; + + for (provided, expected) in [ + (Some(FnEffect::Pure), FnEffect::Pure), + (Some(FnEffect::Impure), FnEffect::Impure), + (Some(FnEffect::Unknown), FnEffect::Unknown), + (None, FnEffect::Impure), + ] { + let mut external_effects = HashMap::new(); + if let Some(effect) = provided { + external_effects.insert("lib$read".to_string(), effect); + } + let mut diagnostics = Diagnostics::new(); + let program = check_program_with_interface_facts( + &modules, + &external_effects, + &HashMap::new(), + &mut diagnostics, + ); + assert!( + !diagnostics.has_errors(), + "imported effect fixture failed for {provided:?}: {:?}", + diagnostics.iter().map(|diagnostic| &diagnostic.message).collect::>() + ); + let imported = program + .imported_fns + .iter() + .find(|function| function.name == "lib$read") + .expect("imported declaration"); + assert_eq!(imported.effect, expected, "provided effect: {provided:?}"); + } + } + + #[test] + fn non_entry_public_function_origin_is_exportable() { + let mut diagnostics = Diagnostics::new(); + let lib_source = "module lib\npub fn exported(value: i64) -> i64 = value\n"; + let main_source = "module main\nimport lib\nfn main() -> i32 = 0\n"; + let lib_tokens = tokenize(1, lib_source, &mut diagnostics); + let lib = parse_file(lib_tokens, &mut diagnostics); + let main_tokens = tokenize(0, main_source, &mut diagnostics); + let main = parse_file(main_tokens, &mut diagnostics); + let modules = [ + Module { + path: "lib".to_string(), + file: &lib, + is_entry: false, + interface_only: false, + }, + Module { + path: "main".to_string(), + file: &main, + is_entry: true, + interface_only: false, + }, + ]; + let program = check_program(&modules, &mut diagnostics); + assert!( + !diagnostics.has_errors(), + "fixture failed: {:?}", + diagnostics.iter().map(|diagnostic| &diagnostic.message).collect::>() + ); + let function = program + .fns + .iter() + .find(|function| function.name == "lib$exported") + .expect("non-entry public source function"); + assert_eq!( + function.origin, + hir::FnOrigin::Source { + is_entry: false, + is_public: true, + } + ); + assert!(function.origin.is_exportable()); } #[test] @@ -35303,7 +35491,9 @@ fn rejected_heap_scan() -> i64 = loop { let lambda = program .fns .iter() - .find(|function| function.lifted_capture_count == Some(0)) + .find(|function| { + matches!(function.origin, hir::FnOrigin::Lifted { capture_count: 0 }) + }) .expect("lifted rejected lambda"); let rejected_lambda = &lambda.body.stmts[0]; assert_eq!(int_break(rejected_lambda), (13, false)); diff --git a/docs/impl/07-roadmap.md b/docs/impl/07-roadmap.md index fb3ddfb0..d650af5f 100644 --- a/docs/impl/07-roadmap.md +++ b/docs/impl/07-roadmap.md @@ -3131,7 +3131,7 @@ per-unit codegen + N-object link until S2b flips them): `build_per_unit` walks the DAG bottom-up producing one `PerUnitArtifact` (per-unit MIR + summary + dep hashes) per cleanly-checked unit; a new `alignc build-per-unit` verb links the N objects. Visibility model wired end-to-end: a non-entry `pub` user fn gets `external` -linkage (`hir::Fn.exportable` → `mir::Function.exportable`, set only by `lower_program_per_unit`; +linkage (`hir::FnOrigin::is_exportable()` → `mir::Function.exportable`, set only by `lower_program_per_unit`; whole-program `lower_program` forces every fn internal for byte-identity), the entry unit's fns stay internal (nothing imports the entry — this is also what makes N=1 byte-identical), and every non-generic `pub` declaration from an interface-only dependency becomes an external Align-ABI diff --git a/docs/impl/17-library-boundary-prerequisites.md b/docs/impl/17-library-boundary-prerequisites.md index df98d1ef..8f3efcc1 100644 --- a/docs/impl/17-library-boundary-prerequisites.md +++ b/docs/impl/17-library-boundary-prerequisites.md @@ -2258,10 +2258,13 @@ CFG is emitted. ledger. The inventory and PR ranges below are routing information only; they do not replace its envelope order, child order, result/type relation, ownership correlation, or owner tests. -Am-h validates, in order, externs, imports, stored functions, then locals: +Am-h validates, in order, externs, imports, function-value header records, stored functions, then +locals: - modes are parallel to parameters; extern modes are `ByValue`; stored/import modes are `ByValue` or `Out`, with `Out` only on `Slice`; `Borrow`/`BorrowMut` remain disabled; +- function-value `FnTy` records retain the graph-valid/type-placement contract already owned by + am-g-t/am-p; am-h owns their mode cardinality/class and return-borrow/region summary shape; - return borrow and region summaries are identical. `None` is canonical for no roots. `Roots { params, captures }` has a non-empty, strictly increasing, in-range `params` vector whose referenced parameter types are borrow-capable, and an empty `captures` vector before L2b-b; @@ -2300,9 +2303,12 @@ Am-h validates, in order, externs, imports, stored functions, then locals: consumed by the existing effect fixed point. Am-h replaces MIR's current `Vec` with `Vec` in the exact existing field order and strips `effect` only after validation. Both structs derive the same `ImportedFn { ... }` structural - Debug bytes for those six fields, so valid per-unit codegen input, `impl_hash`, interface-summary - bytes/hash, and cache behavior remain byte-identical. An owner test compares the old unchecked - MIR rendering/hash and new validated MIR rendering/hash for Pure, Impure, and Unknown imports; + Debug bytes for those six fields, so validated per-unit codegen input has the same six-field + imported-record identity and no new effect byte can enter MIR. This slice does not independently + re-measure interface-summary bytes, `impl_hash`, link/cache keys, or cache hit behavior; those + remain the existing interface/hash/cache owner tests' responsibility. The am-h owner test + compares the validated MIR rendering and six-field imported record for Pure, Impure, and Unknown + imports; - `params` and `param_modes` have equal length. Each parameter id is unique, in range, and refers to the local with that id and the signature type. A source function or generic monomorph marks those locals `is_param: true`; a lifted function deliberately marks both explicit and trailing captured @@ -2439,6 +2445,39 @@ stored field are owner tests; multi-invalid precedence follows the universal ord | L2b-a2-am-b4 | Add body-derived Drop/ownership and effect correlation, assert the full inventory, activate all body validation globally, and prove valid identity/four-entrypoint parity. | `malformed_hir_body_metadata_fails_closed`, `malformed_hir_effect_metadata_fails_closed`, `valid_hir_body_preflight_is_mir_identity`, maximum-depth and deep type-DAG stack owners, whole/per-unit codegen; `mir-body-validation` | | L2b-a2-am-c | Land typed program/runtime/generated call targets, encoded Align symbols, native registry, generated identities, and all name/collision validation atomically across sema/MIR/codegen and whole/per-unit paths. | callable namespace/collision/golden-symbol suites, deep semantic→bytes→semantic plus malformed deep-reference/truncation owners, and per-unit link parity; `mir-callable-namespace-validation` plus unchanged runtime-call cost | +#### Am-h implementation closure matrix + +This matrix is authoritative before the am-h declaration/header validator and its normalized +imported-effect transport are implemented. The slice is one vertical boundary: it publishes no +body-derived ownership or effect facts, and it does not activate the dormant body validators owned +by am-b1 through am-b4. The expected boundary is approximately 750–950 hand-written changed +lines, including the producer migration, all consumers, the MIR transport, validator, focused +owners, and benchmark. It is intentionally one PR: a producer-only split would require a +temporary legacy `Fn`/imported-record representation or would publish an unvalidated effect field; +a validator-only split cannot distinguish source, monomorph, and lifted headers while the current +overloaded pair remains; and a MIR-only split cannot prove the validation-only effect is stripped. +The final author matrix-to-diff pass must record the measured count and preserve this rationale if +the estimate is exceeded. + +| Cell | Required am-h closure | Exact owner evidence | +|---|---|---| +| formation and validation order | The common HIR preflight validates extern declarations first, imported declarations second, function-value header records third, stored function headers fourth, and locals/header-adjacent sets fifth. Each record is checked in ledger order: identity/name, arity and ids, modes, producer-owned type placement, summaries, origin/role, and structural sets. On the first invalid field it returns the canonical empty MIR program; no later record is inspected for a competing diagnostic. Every malformed mutation is tested alone; later-sibling precedence is covered separately. | `malformed_hir_declaration_header_metadata_fails_closed` mutates each owned header field one at a time and verifies canonical-empty parity through all four entrypoints; `main_header_abi_matrix_is_exhaustive` closes the main/error Cartesian cells; `deep_hir_header_type_dag_is_stack_bounded` covers malformed later-sibling rejection. | +| extern headers | Extern names are nonempty, NUL-free, unique in the existing extern namespace, and not the logical `main` name. `params` and `param_modes` have equal length; modes are exactly `ByValue`; parameter and return types must satisfy the existing C-boundary placement validator owned by am-p; return-borrow and return-region summaries are canonical `None` for the current ABI. No native side effect or link action occurs during validation. | Extern name/arity/mode/summary/duplicate mutations; extern-`main` and non-`ByValue` twins; am-p's C-boundary type-placement owner; all four lowering entrypoints and per-unit/whole MIR identity checks. | +| imported headers and effect transport | Every imported non-generic public function has the exact HIR header `name, params, param_modes, ret, return_borrow, return_region, effect`, and an exact `main` name is not an imported logical entry. Parameter/return type formation and placement remain am-p-owned; am-h applies the source-signature, mode, and summary boundary to those types. The producer copies `external_effects[canonical_name]` when present and normalizes an absent compatibility-map entry to `Impure`; `Pure`, `Impure`, and `Unknown` remain distinct. The typed compatibility map has no fourth valid value: the interface decoder's `read_effect` rejects an invalid tag before sema, while a handcrafted HIR can only carry the three enum variants. Imported headers cannot carry generic/source-body/origin records. Validation checks the normalized effect but does not infer an imported body effect. | `imported_effect_facts_are_normalized_and_stripped` covers present `Pure`/`Impure`/`Unknown` and absent→`Impure`; `invalid_effect_tag_is_rejected_before_sema` rejects the fourth value; generic-import rejection and malformed name/mode/summary mutations cover the header; am-p's type-placement owner and interface producer/consumer parity cover the canonical key. | +| MIR imported record and artifact identity | After validation, MIR stores a distinct six-field `mir::ImportedFn` (`name, params, param_modes, ret, return_borrow, return_region`) without the validation-only `effect`. The am-h owner guarantees the same six-field structural Debug/record identity and per-unit imported declaration ordering; it does not independently re-measure interface-summary bytes, `impl_hash`, link/cache keys, or cache hit behavior. Whole-program MIR keeps its existing empty imported list; per-unit MIR copies the validated declarations in source/interface order. Existing interface/hash/cache owners remain responsible for those artifact identities. | `valid_hir_declaration_header_preflight_is_mir_identity` compares the six-field MIR record and structural Debug for Pure, Impure, and Unknown imports; existing downstream interface summary/hash and per-unit cache owners cover their artifact identities. | +| stored function header and `FnOrigin` | Replace the overloaded `lifted_capture_count`/`exportable` pair with `FnOrigin::{Source { is_entry, is_public }, Monomorph, Lifted { capture_count }}`. Source declarations set entry and visibility explicitly; concrete generic instantiations are `Monomorph`; every lambda is `Lifted`, including capture count zero. A lifted count is at most `params.len()`, all lifted parameter modes are `ByValue`, and lifted return summaries are `None`. Exportability is derived exactly from `Source { is_entry: false, is_public: true }`; it is not an independent mutable fact. `FnOrigin` is HIR-only metadata: it is not serialized into interface records, MIR structural Debug, `impl_hash`, object-cache keys, or runtime ABI. The existing derived MIR `Function.exportable` bit remains the only per-unit linkage input. | `lifted_function_origin_metadata_is_explicit` covers entry-unit source visibility, generic monomorph, and zero/nonzero-capture lambdas; `non_entry_public_function_origin_is_exportable` covers the actual external-unit producer; `valid_hir_declaration_header_preflight_is_mir_identity` proves the origin is absent from the MIR imported record. | +| main header and ABI role | Only a non-generic `FnOrigin::Source { is_entry: true, ... }` named `main` may be the logical entry. Its parameters are either empty or exactly one `ByValue array`; its return is exactly `Unit`, signed `i32`, or `Result`, with the argv form restricted to the Result form. The builtin `Error` name/source-name, variant names/order, field bases, and payload shape are exact. Monomorphs, lifted functions, imported functions, and externs cannot become main. | No-arg/argv × Unit/i32/Result matrix, every parameter/return/Error identity and shape mutation, generic/lifted/imported/extern main twins, exact diagnostic order, and whole/per-unit ABI/codegen owners. | +| parameters, locals, and summaries | Parameter vectors and modes have equal length. Every parameter id is unique and in range, points to a local with the same id and signature type, and has the source/monomorph `is_param` role; lifted capture locals remain non-parameters. Local ids are ordinal, names are nonempty and NUL-free, parameter names are ASCII source identifiers, and non-parameter fixed int/float array alignment is either absent or a power of two no greater than `2^29`. Return-borrow/region summaries are canonical `None` or have sorted unique in-range roots that reference borrow-capable parameter types; captures are empty before am-b. FnTy type placements are owned by am-p; FnTy modes and summaries use the same header rules. | Deep valid/malformed signature and summary twins; duplicate/out-of-range/id-type/name/ASCII/alignment/root-order/root-duplicate/root-range/non-borrowable-root/capture mutations; FnTy mode/summary mutations; all four entrypoints and source/monomorph/lifted producers. | +| drop-set structure | `drop_locals` and `drop_individual_locals` are sorted, unique, in range, and the individual set is a subset of the broad set. am-h deliberately does not read `drop_individual_exprs` at all: its `HashMap` keys, bool values, duplicate-span insertion semantics, and correlation to expression ownership are wholly owned by am-b4, whose exact rule is in `docs/impl/19-hir-validation-ledger.md` §Universal record order and ownership correlation. No am-h path creates, removes, replaces, moves, nulls, or Drops a source value. | `malformed_hir_declaration_header_metadata_fails_closed` mutates sortedness, duplicates, range, and subset; `valid_header_does_not_consume_body_facts` proves an arbitrary `drop_individual_exprs` entry and compiler-only local type do not change am-h acceptance. | +| body/control boundary and precedence | am-h does not traverse or reinterpret statements, expressions, or control joins. `if`, `match`, `else`, `?`, `map_err`, branch joins, loop joins, early exits, and malformed body paths remain dormant until am-b1 through am-b4; stored-body effect, return-provenance, and exact drop-expression facts are never read by this pass. The common sequence is depth/global/placement/nominal validation, then am-h header order, then (only after a successful header pass) am-b4 body replay. Thus an earlier graph/placement/nominal error wins over an am-h error; an am-h header error returns canonical-empty before any body validator can report; a valid header does not claim to accept malformed body metadata until am-b4 activates. | `valid_header_does_not_consume_body_facts` proves the deferred body boundary; the existing global/placement/nominal malformed-HIR suites prove earlier-preflight precedence, and the deferred am-b1–b4 owner names remain unchanged. | +| generic, interface, whole-program, and per-unit construction | Source declarations, generic declarations, monomorphs, imported declarations, and externs follow their existing producer paths with the new origin/effect fields. Whole-program HIR contains no imported declarations; interface-only/per-unit HIR carries imported headers and their normalized effects. All four public lowerers—`lower_program`, `lower_program_located`, `lower_program_per_unit`, and `lower_program_per_unit_located`—run the same validation order and return either the existing owned MIR or canonical all-empty MIR. | Generic/monomorph source twins, interface import twins, whole/per-unit and located/non-located malformed/valid matrices, and continuation codegen tests with unchanged output. | +| ownership, cleanup, and allocation | Validation uses compiler-only worklist/map allocation and borrows the input HIR; it adds no runtime allocation, FFI call, link action, source nulling, Drop, cleanup, or artifact side effect. The direct four-entrypoint observation is an all-empty MIR result with no body/codegen call. Accepted per-unit inputs reach codegen with the same six-field MIR record; `FnEffect` and `FnOrigin` are Copy metadata. MIR strips only the validation-only imported effect and otherwise preserves ownership/cleanup records. Artifact/cache side-effect observation remains owned by the existing driver per-unit tests. | `valid_hir_declaration_header_preflight_is_mir_identity`, the four-entrypoint canonical-empty assertions in `malformed_hir_declaration_header_metadata_fails_closed`, existing per-unit artifact/cache tests, and the downstream Move/Copy/drop-set identity checks. | +| implementation consumers | Every former `lifted_capture_count` and `exportable` consumer is replaced or intentionally derived from `FnOrigin`: source construction, lambda construction, monomorph construction, return-provenance inference, open-world effect analysis, MIR exportability, and interface/per-unit lowering. No compatibility alias or parallel old/new path remains. | Repository search with zero stale field consumers; source/monomorph/lifted effect and return-provenance owners; per-unit export and whole-program internalization parity. | +| deep graph and malformed precedence | Header signatures, parameter types, and borrow/region summaries may form finite but depth-unbounded type DAGs. The prior global and placement walks are explicit and stack-bounded; am-h's summary check reuses the cycle-safe borrow classifier. A later imported sibling is rejected after the malformed header record, without publishing partial MIR. | `deep_hir_header_type_dag_is_stack_bounded` covers a valid 4,096-node imported signature and a malformed later sibling; `ty_may_borrow_is_cycle_safe_for_header_mediated_nominals` covers legal header-mediated cycles. | +| field-to-owner closure | The malformed matrix covers every am-h-owned extern field (name, duplicate/name, arity/modes, summaries), imported field (including duplicate/name, signature boundary, summary, and effect transport), stored `Fn` field (name, duplicate/name, origin, params, modes, return placement delegated to am-p, summaries, span, and drop sets), local id/name/parameter-bit/alignment, every main ABI/Error identity and shape discriminator, and the FnTy mode/summary records. Local type graph/placement validity remains am-g-t/am-p-owned; local mutability and expression ownership remain body-owned. Each am-h mutation is paired with canonical-empty/identity outcome and entrypoint parity. | `malformed_hir_declaration_header_metadata_fails_closed`, `main_header_abi_matrix_is_exhaustive`, `lifted_function_origin_metadata_is_explicit`, `non_entry_public_function_origin_is_exportable`, `imported_effect_facts_are_normalized_and_stripped`, `valid_header_does_not_consume_body_facts`, `deep_hir_header_type_dag_is_stack_bounded`, and `valid_hir_declaration_header_preflight_is_mir_identity` own the named submatrices; am-p owns the complete extern/import/local type-placement cells. | +| benchmark and regression boundary | Add the `mir-header-validation` benchmark row for valid and malformed header preflight. The benchmark measures compiler-side validation only and does not change runtime behavior or add a new persisted artifact field. The existing placement and nominal/link benchmark rows and all continuation rows remain unchanged. | `bench/library_boundary` header fixture and README row; benchmark build/run when `llvm-config-22` is available; focused owner suite plus unchanged downstream codegen/runtime rows. | +| review and split gate | The matrix receives one fresh independent adversarial review before coding. The planned PR boundary is exactly the producer `FnOrigin`/import-effect migration, every sema consumer, the distinct MIR imported record, the common header validator, focused owner tests, and the benchmark. It is not split because the no-legacy/no-parallel-path rule makes a producer-only intermediate either expose an unvalidated imported effect or retain the overloaded origin pair, while the validator and MIR conversion need the same exact field order and fail-closed boundary. If the measured hand-written diff exceeds the 750–950 estimate, record the count and this boundary proof in `HANDOFF.md` before opening the PR; above 1,000 also record why no safe split exists. | Matrix-review log bound to the pre-implementation base; final author matrix-to-diff pass; focused owners, `scripts/test-pr.sh`, applicable Clippy, and the standard pre/post-PR attestations. | + #### Am-f implementation closure matrix This matrix is authoritative before the return-completeness producer correction begins.