From 1d5664f4b1f239f14afddf8db955991e35a56b34 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Wed, 5 Aug 2026 20:02:37 +0900 Subject: [PATCH 1/2] feat(compiler): close borrow ownership boundary --- HANDOFF.md | 27 +- bench/library_boundary/Cargo.lock | 1 + bench/library_boundary/Cargo.toml | 1 + bench/library_boundary/README.md | 16 + bench/library_boundary/run.sh | 7 + bench/library_boundary/src/main.rs | 170 +- crates/align_ast/src/lib.rs | 6 +- crates/align_codegen_llvm/src/lib.rs | 1004 +++++++- crates/align_driver/tests/borrowed_params.rs | 237 ++ .../tests/interface_param_modes.rs | 42 +- .../align_driver/tests/move_return_cleanup.rs | 119 + .../align_driver/tests/return_provenance.rs | 210 +- crates/align_interface/src/codec.rs | 27 +- crates/align_interface/src/lib.rs | 448 +++- crates/align_interface/tests/summary.rs | 106 +- crates/align_mir/src/canonical_graph.rs | 130 +- crates/align_mir/src/generated_id.rs | 16 +- crates/align_mir/src/lib.rs | 421 ++- crates/align_mir/src/print.rs | 43 +- crates/align_mir/src/source_shape.rs | 8 + crates/align_mir/src/validate_hir.rs | 274 +- crates/align_mir/src/validate_hir_tests.rs | 165 +- crates/align_parser/src/lib.rs | 71 +- crates/align_sema/src/hir.rs | 13 + crates/align_sema/src/hir_depth.rs | 22 + crates/align_sema/src/lib.rs | 2279 +++++++++++++++-- crates/align_sema/src/replay_clone.rs | 5 + 27 files changed, 5392 insertions(+), 476 deletions(-) create mode 100644 crates/align_driver/tests/borrowed_params.rs create mode 100644 crates/align_driver/tests/move_return_cleanup.rs diff --git a/HANDOFF.md b/HANDOFF.md index ab3ace99..25386365 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -5,21 +5,21 @@ 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-05._ The C-A canonical callable capability is complete -through am-c3. MIR now separates typed program and runtime direct targets, LLVM -program identities are encoded consistently across whole-program, per-unit, -export, main-wrapper, and ThinLTO paths, and every generated callable family uses -canonical collected identity plus deterministic collision probing. No public -`pkg.db` surface exists yet. +_Last updated: 2026-08-05._ The C-B borrow/ownership capability is complete +through L2e. Direct, captured, imported, and function-value returns preserve +exact owner provenance; recursively Move returns carry a path-selected cleanup +bit; and shared/exclusive parameters preserve caller ownership, replacement, +generation invalidation, and whole/per-unit ABI parity. No public `pkg.db` +surface exists yet. The remaining compiler plan uses consumer-complete capability waves rather than one PR per dormant acceptance cell: ```text C-A canonical callable closure complete through c3 -C-B borrow/ownership closure af/ar/ap/t/b + L2c/L2d/L2e +C-B borrow/ownership closure complete through L2e -after C-B, in parallel: +next independent waves: F-A native resources L3 F-B region materialization L4 + L6 F-C static artifacts L5 @@ -924,12 +924,11 @@ was rerun after #636. #637-#644 passed their focused and PR CI gates. ## Next work -C-A closes the canonical callable plan from c2a2b through c3 as one consumer-complete wave. The -next implementation is C-B: direct, captured, and imported return-provenance closure through -L2b-b, followed in the same ownership capability by Move-return cleanup and shared/exclusive -borrow consumers through L2e. Reopen the C-B closure matrix before coding and preserve the exact -control-flow/type-reconciliation matrix in the repository instructions. F-A/F-B/F-C begin only -after C-B. +C-B closes direct, captured, imported, and function-value return provenance together with +path-selected Move-return cleanup and shared/exclusive borrow consumers through L2e. The next +independent implementation waves are F-A native resources (L3), F-B region materialization +(L4 + L6), and F-C static artifacts (L5). Each reopens its owning closure matrix before coding; +F-D package integration begins only after F-A/F-B and the complete F-C prerequisite gate. The query-centered `pkg.db` design and its general library-boundary prerequisites are specified in `docs/impl/pkg-design/db.md` and `docs/impl/17-library-boundary-prerequisites.md`; the feasibility diff --git a/bench/library_boundary/Cargo.lock b/bench/library_boundary/Cargo.lock index c6747987..2a862dc6 100644 --- a/bench/library_boundary/Cargo.lock +++ b/bench/library_boundary/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "align_driver", "align_interface", "align_mir", + "align_sema", "align_span", ] diff --git a/bench/library_boundary/Cargo.toml b/bench/library_boundary/Cargo.toml index 5eeebc0b..4be44ea7 100644 --- a/bench/library_boundary/Cargo.toml +++ b/bench/library_boundary/Cargo.toml @@ -8,6 +8,7 @@ publish = false align_driver = { path = "../../crates/align_driver" } align_interface = { path = "../../crates/align_interface" } align_mir = { path = "../../crates/align_mir" } +align_sema = { path = "../../crates/align_sema" } align_span = { path = "../../crates/align_span" } [workspace] diff --git a/bench/library_boundary/README.md b/bench/library_boundary/README.md index dc2e86a3..e3c7d8b0 100644 --- a/bench/library_boundary/README.md +++ b/bench/library_boundary/README.md @@ -5,6 +5,10 @@ This cumulative harness owns the measurements named by ```text bench/library_boundary/run.sh interface +bench/library_boundary/run.sh provenance +bench/library_boundary/run.sh move-return +bench/library_boundary/run.sh shared-borrow +bench/library_boundary/run.sh exclusive-borrow ``` `interface` builds a deterministic 512-function exported surface with explicit L2a parameter modes @@ -44,5 +48,17 @@ a 256-function, high-CFG fixture with three expression-valued branches per funct L2b-b adds the `indirect-return` row to the same group after target-relative function-value provenance lands. +`move-return` builds four optimized executables around the same 100,000-call loop and reports +nanoseconds per call. `copy-return-control` is the value-only control; `move-return-none`, +`move-return-some`, and `move-return-err` exercise the dynamic cleanup-bit ABI with the clear +`Option` path, owned `Option` payload, and owned `Result` error payload respectively. Process launch +cost is amortized across the inner loop; the rows are comparative evidence, not timing assertions. + +`shared-borrow` compares the `by-value-call-control` row with repeated zero-allocation +`shared-borrow-call` inspection of one owned string. `exclusive-borrow` compares a by-value Copy +update (`exclusive-copy-control`) with an in-place Copy update (`exclusive-copy-call`) and records +the allocation-and-Drop cost of replacing a Move string through the caller cleanup-bit ABI +(`exclusive-move-replace`). Each row uses the same 100,000-call inner loop. + The benchmark is a regression tracker, not a timing assertion. Record the command, compiler commit, host, and output when comparing changes. diff --git a/bench/library_boundary/run.sh b/bench/library_boundary/run.sh index 531c7206..b5a9d903 100755 --- a/bench/library_boundary/run.sh +++ b/bench/library_boundary/run.sh @@ -2,6 +2,13 @@ set -euo pipefail repo_root="$(cd "$(dirname "$0")/../.." && pwd)" +if [[ "${1:-}" == "move-return" || "${1:-}" == "shared-borrow" || "${1:-}" == "exclusive-borrow" ]]; then + "$repo_root/scripts/cargo.sh" build \ + --quiet \ + --release \ + --manifest-path "$repo_root/Cargo.toml" \ + -p align_runtime +fi exec "$repo_root/scripts/cargo.sh" run \ --quiet \ --release \ diff --git a/bench/library_boundary/src/main.rs b/bench/library_boundary/src/main.rs index ee681a1b..08a43f09 100644 --- a/bench/library_boundary/src/main.rs +++ b/bench/library_boundary/src/main.rs @@ -45,6 +45,7 @@ fn interface_fixture() -> InterfaceSummary { ret: named("i64"), return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: align_sema::hir::ReturnCleanupAbi::None, effect: Effect::Pure, generic_body: None, }); @@ -212,6 +213,7 @@ fn import_validation_fixture() -> InterfaceSummary { params: vec![0], captures: Vec::new(), }, + return_cleanup: align_sema::hir::ReturnCleanupAbi::None, effect: Effect::Pure, generic_body: None, }) @@ -493,16 +495,182 @@ fn run_provenance() { println!("mir-continuation-lowering\t{milliseconds:.3}\tms/lower\t{block_count}\tblocks"); } +fn return_fixture(kind: &str, calls: u64) -> String { + let (producer, consume) = match kind { + "copy-return-control" => ( + "fn produce(value: i64) -> i64 = value\n", + "fn consume(value: i64) -> i64 = value\n", + ), + "move-return-none" => ( + "fn produce(_: i64) -> Option = None\n", + "fn consume(value: Option) -> i64 = match value { Some(text) => text.len() None => 0 }\n", + ), + "move-return-some" => ( + "fn produce(_: i64) -> Option = Some(\"owned\".clone())\n", + "fn consume(value: Option) -> i64 = match value { Some(text) => text.len() None => 0 }\n", + ), + "move-return-err" => ( + "fn produce(_: i64) -> Result = Err(\"owned\".clone())\n", + "fn consume(value: Result) -> i64 = match value { Ok(text) => text.len() Err(text) => text.len() }\n", + ), + _ => unreachable!("closed benchmark row"), + }; + format!( + "{producer}{consume}\ + fn main() -> i32 {{\n\ + \u{20}\u{20}mut index: i64 := 0\n\ + \u{20}\u{20}mut total: i64 := 0\n\ + \u{20}\u{20}loop {{\n\ + \u{20}\u{20}\u{20}\u{20}if index == {calls} {{ break }}\n\ + \u{20}\u{20}\u{20}\u{20}total = total + consume(produce(index))\n\ + \u{20}\u{20}\u{20}\u{20}index = index + 1\n\ + \u{20}\u{20}}}\n\ + \u{20}\u{20}if total < 0 {{ return 1 }}\n\ + \u{20}\u{20}return 0\n\ + }}\n" + ) +} + +fn compile_call_fixture( + directory: &std::path::Path, + row: &str, + source: &str, +) -> std::path::PathBuf { + let mut source_map = align_span::SourceMap::new(); + let checked = align_driver::check(&mut source_map, &format!("{row}.align"), source); + assert!( + !checked.diags.has_errors(), + "{row} fixture must check:\n{}", + align_driver::format_diagnostics(&source_map, &checked.diags) + ); + let mir = align_driver::lower_to_mir(&checked.hir); + let object = directory.join(format!("{row}.o")); + let executable = directory.join(row); + align_driver::emit_object_file( + &mir, + &object, + align_driver::BuildTarget::Baseline, + align_driver::Profile::Release, + &[], + false, + ) + .unwrap_or_else(|error| panic!("{row} object emission failed: {error}")); + align_driver::link_executable( + &object, + &executable, + &mir.link_libs, + align_driver::Profile::Release, + ) + .unwrap_or_else(|error| panic!("{row} link failed: {error}")); + executable +} + +fn run_call_rows( + group: &str, + rows: &[&str], + calls: u64, + fixture: fn(&str, u64) -> String, +) { + assert!(align_driver::backend_available(), "{group} benchmark requires the LLVM backend"); + let unique = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock") + .as_nanos(); + let directory = std::env::temp_dir().join(format!( + "align-library-boundary-{group}-{}-{unique}", std::process::id() + )); + std::fs::create_dir(&directory).expect("create benchmark directory"); + struct Cleanup(std::path::PathBuf); + impl Drop for Cleanup { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + let _cleanup = Cleanup(directory.clone()); + + for &row in rows { + let source = fixture(row, calls); + let executable = compile_call_fixture(&directory, row, &source); + let minimum = Duration::from_millis(750); + let start = Instant::now(); + let mut processes = 0_u64; + while start.elapsed() < minimum { + let status = std::process::Command::new(&executable) + .status() + .unwrap_or_else(|error| panic!("{row} execution failed: {error}")); + assert!(status.success(), "{row} returned {status}"); + processes += 1; + } + let elapsed = start.elapsed(); + let nanoseconds = elapsed.as_secs_f64() * 1_000_000_000.0 + / (calls * processes) as f64; + println!("{row}\t{nanoseconds:.3}\tns/call\t{processes}\tprocesses"); + } +} + +fn run_move_return() { + const ROWS: [&str; 4] = [ + "copy-return-control", + "move-return-none", + "move-return-some", + "move-return-err", + ]; + run_call_rows("move-return", &ROWS, 100_000, return_fixture); +} + +fn borrow_fixture(kind: &str, calls: u64) -> String { + match kind { + "by-value-call-control" => format!( + "fn inspect(value: i64) -> i64 = value + 1\n\ + fn main() -> i32 {{ mut index: i64 := 0; mut total: i64 := 0; loop {{ if index == {calls} {{ break }}; total = total + inspect(index); index = index + 1 }}; if total < 0 {{ return 1 }}; return 0 }}\n" + ), + "shared-borrow-call" => format!( + "fn inspect(borrow value: string) -> i64 = value.len()\n\ + fn main() -> i32 {{ value := \"shared\".clone(); mut index: i64 := 0; mut total: i64 := 0; loop {{ if index == {calls} {{ break }}; total = total + inspect(value); index = index + 1 }}; if total < 0 {{ return 1 }}; return 0 }}\n" + ), + "exclusive-copy-control" => format!( + "fn increment(value: i64) -> i64 = value + 1\n\ + fn main() -> i32 {{ mut index: i64 := 0; mut value: i64 := 0; loop {{ if index == {calls} {{ break }}; value = increment(value); index = index + 1 }}; if value != {calls} {{ return 1 }}; return 0 }}\n" + ), + "exclusive-copy-call" => format!( + "fn increment(borrow mut value: i64) {{ value = value + 1 }}\n\ + fn main() -> i32 {{ mut index: i64 := 0; mut value: i64 := 0; loop {{ if index == {calls} {{ break }}; increment(value); index = index + 1 }}; if value != {calls} {{ return 1 }}; return 0 }}\n" + ), + "exclusive-move-replace" => format!( + "fn replace(borrow mut value: string) {{ value = \"replacement\".clone() }}\n\ + fn main() -> i32 {{ mut index: i64 := 0; mut value := \"initial\".clone(); loop {{ if index == {calls} {{ break }}; replace(value); index = index + 1 }}; if value.len() != 11 {{ return 1 }}; return 0 }}\n" + ), + _ => unreachable!("closed borrowed-call benchmark row"), + } +} + +fn run_shared_borrow() { + const ROWS: [&str; 2] = ["by-value-call-control", "shared-borrow-call"]; + run_call_rows("shared-borrow", &ROWS, 100_000, borrow_fixture); +} + +fn run_exclusive_borrow() { + const ROWS: [&str; 3] = [ + "exclusive-copy-control", + "exclusive-copy-call", + "exclusive-move-replace", + ]; + run_call_rows("exclusive-borrow", &ROWS, 100_000, borrow_fixture); +} + fn main() { match std::env::args().nth(1).as_deref() { Some("interface") => run_interface(), Some("provenance") => run_provenance(), + Some("move-return") => run_move_return(), + Some("shared-borrow") => run_shared_borrow(), + Some("exclusive-borrow") => run_exclusive_borrow(), Some(other) => { eprintln!("unknown library-boundary benchmark group `{other}`"); std::process::exit(2); } None => { - eprintln!("usage: run.sh interface|provenance"); + eprintln!("usage: run.sh interface|provenance|move-return|shared-borrow|exclusive-borrow"); std::process::exit(2); } } diff --git a/crates/align_ast/src/lib.rs b/crates/align_ast/src/lib.rs index 997e5a35..ef2a07f2 100644 --- a/crates/align_ast/src/lib.rs +++ b/crates/align_ast/src/lib.rs @@ -164,10 +164,8 @@ pub struct Param { pub ty: Type, } -/// Function parameter ownership/access mode. L2a represents every settled mode end to end while -/// the parser still produces only [`ByValue`](ParamMode::ByValue) and the already-shipped -/// [`Out`](ParamMode::Out). `Borrow` and `BorrowMut` remain reserved until their complete semantic -/// slices land. +/// Function parameter ownership/access mode. The parser treats `out`, `borrow`, and `borrow mut` +/// contextually so the same words remain available as ordinary identifiers and type names. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ParamMode { ByValue, diff --git a/crates/align_codegen_llvm/src/lib.rs b/crates/align_codegen_llvm/src/lib.rs index 4f75d8bc..7f1be0c4 100644 --- a/crates/align_codegen_llvm/src/lib.rs +++ b/crates/align_codegen_llvm/src/lib.rs @@ -685,6 +685,7 @@ struct ProgramSignature { ret: Ty, borrow: hir::ReturnBorrowSummary, region: hir::ReturnRegionSummary, + cleanup: hir::ReturnCleanupAbi, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -1034,6 +1035,7 @@ fn build_module<'c>( &enum_types, &tagged_types, &tuple_types, + program, exports, ); program_funcs.insert(f.name.clone(), fv); @@ -1058,6 +1060,7 @@ fn build_module<'c>( &enum_types, &tagged_types, &tuple_types, + program, ); program_funcs.insert(imp.name.clone(), fv); } @@ -1177,6 +1180,7 @@ fn build_module<'c>( }; let thunk = module.add_function(emitted_name, thunk_ty, None); mark_nounwind(ctx, thunk); + mark_borrow_param_contracts_at(ctx, thunk, &declaration.signature.modes, 1); mark_private_helper(thunk); let bb = ctx.append_basic_block(thunk, "entry"); let tb = ctx.create_builder(); @@ -1233,6 +1237,7 @@ fn build_module<'c>( ret: declaration.signature.ret, borrow: declaration.signature.borrow.clone(), region: declaration.signature.region.clone(), + cleanup: declaration.signature.cleanup, }; let id = GeneratedId::Closure { lifted: lifted.clone(), @@ -1266,6 +1271,7 @@ fn build_module<'c>( }; let thunk = module.add_function(emitted_name, thunk_ty, None); mark_nounwind(ctx, thunk); + mark_borrow_param_contracts_at(ctx, thunk, explicit_modes, 1); mark_private_helper(thunk); let bb = ctx.append_basic_block(thunk, "entry"); let tb = ctx.create_builder(); @@ -1453,6 +1459,7 @@ fn build_module<'c>( f, func, slots: HashMap::new(), + borrow_mut_cleanup_ptrs: HashMap::new(), values: HashMap::new(), stack_header_slots: stack_headers.slots, stack_header_new_values: stack_headers.new_values, @@ -1512,6 +1519,7 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { ret: Ty, borrow: &'a hir::ReturnBorrowSummary, region: &'a hir::ReturnRegionSummary, + cleanup: hir::ReturnCleanupAbi, allow_out: bool, allow_return_roots: bool, } @@ -1528,6 +1536,7 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { ret, borrow, region, + cleanup, allow_out, allow_return_roots, } = facts; @@ -1539,10 +1548,26 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { ))); } type_graph.check_ty(ret)?; + let expected_cleanup = if align_sema::needs_drop_flag( + ret, + &program.structs, + &program.tuples, + &program.enums, + &program.tagged_types, + ) { + hir::ReturnCleanupAbi::DynamicBit + } else { + hir::ReturnCleanupAbi::None + }; + if cleanup != expected_cleanup { + return Err(CodegenError::Lowering(format!( + "{owner} return cleanup ABI disagrees with its return type" + ))); + } for &ty in param_types { type_graph.check_ty(ty)?; } - for mode in modes { + for (&mode, &ty) in modes.iter().zip(param_types) { match mode { align_ast::ParamMode::ByValue => {} align_ast::ParamMode::Out if allow_out => {} @@ -1551,11 +1576,20 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { "{owner} uses `out` in a function-value ABI" ))); } - align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut => { - return Err(CodegenError::Lowering(format!( - "{owner} uses parameter mode {mode:?} before its ABI is enabled" - ))); + align_ast::ParamMode::Borrow => { + if !align_sema::needs_drop_flag( + ty, + &program.structs, + &program.tuples, + &program.enums, + &program.tagged_types, + ) { + return Err(CodegenError::Lowering(format!( + "{owner} uses {mode:?} with a non-Move parameter type" + ))); + } } + align_ast::ParamMode::BorrowMut => {} } } let validate_summary = @@ -1580,11 +1614,6 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { "{owner} has an out-of-range {kind} parameter root" ))); } - if !captures.is_empty() { - return Err(CodegenError::Lowering(format!( - "{owner} has {kind} capture roots before L2b-b" - ))); - } Ok(()) }; if let hir::ReturnBorrowSummary::Roots { params, captures } = borrow { @@ -1617,7 +1646,7 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { || !matches!(region, hir::ReturnRegionSummary::None)) { return Err(CodegenError::Lowering(format!( - "{owner} has return roots before function-value provenance lands in L2b-b" + "{owner} cannot carry return provenance across an unanalyzed extern boundary" ))); } if let hir::ReturnBorrowSummary::Roots { params, .. } = borrow { @@ -1634,13 +1663,19 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { } for &root in params { let ty = param_types[root as usize]; - if !align_sema::ty_may_borrow( - ty, - &program.structs, - &program.tuples, - &program.enums, - &program.tagged_types, - ) { + let borrowed_owner = matches!( + modes[root as usize], + align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut + ); + if !borrowed_owner + && !align_sema::ty_may_borrow( + ty, + &program.structs, + &program.tuples, + &program.enums, + &program.tagged_types, + ) + { return Err(CodegenError::Lowering(format!( "{owner} return provenance root {root} has type {ty:?}, which cannot supply a borrow" ))); @@ -1739,10 +1774,19 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { Ok(()) } + type NamedSignature<'a> = ( + Vec, + Ty, + &'a [align_ast::ParamMode], + &'a hir::ReturnBorrowSummary, + &'a hir::ReturnRegionSummary, + hir::ReturnCleanupAbi, + ); + fn named_signature<'a>( program: &'a Program, name: &ProgramCall, - ) -> Option<(Vec, Ty, &'a [align_ast::ParamMode])> { + ) -> Option> { if let Some(function) = program.fns.iter().find(|function| &function.name == name) { return Some(( function @@ -1752,6 +1796,9 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { .collect::>>()?, function.ret, &function.param_modes, + &function.return_borrow, + &function.return_region, + function.return_cleanup, )); } if let Some(function) = program @@ -1763,6 +1810,9 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { function.params.clone(), function.ret, &function.param_modes, + &function.return_borrow, + &function.return_region, + function.return_cleanup, )); } program @@ -1770,12 +1820,15 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { .iter() .find(|function| &function.name == name) .map(|function| { - ( - function.params.clone(), - function.ret, - function.param_modes.as_slice(), - ) - }) + ( + function.params.clone(), + function.ret, + function.param_modes.as_slice(), + &function.return_borrow, + &function.return_region, + function.return_cleanup, + ) + }) } /// Validate every MIR type reference and the complete inline layout graph before any semantic @@ -2336,6 +2389,42 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { }) }) .collect::, _>>()?; + if !f.borrow_mut_cleanup_slots.is_empty() + && f.borrow_mut_cleanup_slots.len() != f.params.len() + { + return Err(CodegenError::Lowering(format!( + "function `{}` has a malformed BorrowMut cleanup vector", + f.name + ))); + } + for (index, (mode, ty)) in f + .param_modes + .iter() + .zip(¶m_types) + .enumerate() + { + let expected = *mode == align_ast::ParamMode::BorrowMut + && align_sema::needs_drop_flag( + *ty, + &program.structs, + &program.tuples, + &program.enums, + &program.tagged_types, + ); + let cleanup = f + .borrow_mut_cleanup_slots + .get(index) + .copied() + .flatten(); + if cleanup.is_some() != expected + || cleanup.is_some_and(|slot| f.slots.get(slot as usize) != Some(&Ty::Bool)) + { + return Err(CodegenError::Lowering(format!( + "function `{}` parameter {index} has malformed BorrowMut cleanup storage", + f.name + ))); + } + } check_signature_facts( SignatureFacts { owner: &format!("function `{}`", f.name), @@ -2344,6 +2433,7 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { ret: f.ret, borrow: &f.return_borrow, region: &f.return_region, + cleanup: f.return_cleanup, allow_out: true, allow_return_roots: true, }, @@ -2366,7 +2456,7 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { }; match rvalue { Rvalue::FnAddr { target, signature } => { - let Some((param_types, ret, modes)) = + let Some((param_types, ret, modes, borrow, region, cleanup)) = named_signature(program, target) else { return Err(callable_target_error(target)); @@ -2379,16 +2469,18 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { ret, borrow: &signature.return_borrow, region: &signature.return_region, + cleanup: signature.return_cleanup, allow_out: false, - allow_return_roots: false, + allow_return_roots: true, }, program, &mut type_graph, )?; - // L2b-a1 deliberately leaves function-value return summaries at `None`; - // indirect calls retain the all-compatible-input fallback. L2b-b attaches - // target-relative roots and restores exact summary equality here. - if signature.param_modes != modes { + if signature.param_modes != modes + || &signature.return_borrow != borrow + || &signature.return_region != region + || signature.return_cleanup != cleanup + { return Err(callable_target_error(target)); } } @@ -2459,8 +2551,9 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { ret: target.ret, borrow: &signature.return_borrow, region: &signature.return_region, + cleanup: signature.return_cleanup, allow_out: false, - allow_return_roots: false, + allow_return_roots: true, }, program, &mut type_graph, @@ -2468,35 +2561,80 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { if signature.param_modes != target_modes || signature.return_borrow != target.return_borrow || signature.return_region != target.return_region + || signature.return_cleanup != target.return_cleanup { return Err(callable_target_error(lifted)); } } Rvalue::CallIndirect { + args, param_tys, ret_ty, signature, .. - } => check_signature_facts( - SignatureFacts { + } => { + if !operands_match_modes(args, &signature.param_modes, param_tys, program) { + return Err(callable_metadata_error()); + } + check_signature_facts(SignatureFacts { owner: "indirect call", modes: &signature.param_modes, param_types: param_tys, ret: *ret_ty, borrow: &signature.return_borrow, region: &signature.return_region, + cleanup: signature.return_cleanup, allow_out: false, - allow_return_roots: false, - }, - program, - &mut type_graph, - )?, + allow_return_roots: true, + }, program, &mut type_graph)?; + } + Rvalue::CallIndirectWithCleanup(call) => { + let align_mir::IndirectCallWithCleanup { + param_tys, + args, + ret_ty, + signature, + cleanup, + .. + } = call.as_ref(); + if f.value_tys.get(*cleanup as usize) != Some(&Ty::Bool) { + return Err(callable_metadata_error()); + } + if !operands_match_modes(args, &signature.param_modes, param_tys, program) { + return Err(callable_metadata_error()); + } + check_signature_facts( + SignatureFacts { + owner: "indirect call", + modes: &signature.param_modes, + param_types: param_tys, + ret: *ret_ty, + borrow: &signature.return_borrow, + region: &signature.return_region, + cleanup: signature.return_cleanup, + allow_out: false, + allow_return_roots: true, + }, + program, + &mut type_graph, + )?; + } _ => {} } } } } for ext in &program.externs { + if ext + .param_modes + .iter() + .any(|mode| *mode != align_ast::ParamMode::ByValue) + { + return Err(CodegenError::Lowering(format!( + "extern function `{}` uses a non-by-value parameter mode at the C boundary", + ext.name + ))); + } check_signature_facts( SignatureFacts { owner: &format!("extern function `{}`", ext.name), @@ -2505,6 +2643,7 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { ret: ext.ret, borrow: &ext.return_borrow, region: &ext.return_region, + cleanup: ext.return_cleanup, allow_out: false, allow_return_roots: false, }, @@ -2526,6 +2665,7 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { ret: import.ret, borrow: &import.return_borrow, region: &import.return_region, + cleanup: import.return_cleanup, allow_out: true, allow_return_roots: true, }, @@ -2613,6 +2753,7 @@ fn program_signature(function: &Function) -> Result Result canonical_metadata(CanonicalTy::from_program(ty, program)) } +fn source_ty_matches(actual: Ty, expected: Ty, program: &Program) -> Result { + if actual == expected { + return Ok(true); + } + Ok(canonical_ty(actual, program)? == canonical_ty(expected, program)?) +} + +fn source_tys_match( + actual: &[Ty], + expected: &[Ty], + program: &Program, +) -> Result { + if actual.len() != expected.len() { + return Ok(false); + } + for (&actual, &expected) in actual.iter().zip(expected) { + if !source_ty_matches(actual, expected, program)? { + return Ok(false); + } + } + Ok(true) +} + fn callable_metadata_error() -> CodegenError { CodegenError::Lowering("callable metadata invalid:InvalidGraph".to_owned()) } @@ -3037,9 +3202,47 @@ fn preflight_operand_ty(function: &Function, operand: &Operand) -> Option { .get(*index as usize) .and_then(|slot| function.slots.get(*slot as usize)) .copied(), + Operand::BorrowedPlace(place) => function + .slots + .get(place.slot as usize) + .map(|_| place.ty), + Operand::BorrowedCleanupArg(_) => Some(Ty::Bool), } } +fn operands_match_modes( + args: &[Operand], + modes: &[align_ast::ParamMode], + types: &[Ty], + program: &Program, +) -> bool { + args.len() == modes.len() + && modes.len() == types.len() + && args + .iter() + .zip(modes) + .zip(types) + .all(|((argument, mode), ty)| match (argument, mode) { + (Operand::BorrowedPlace(place), align_ast::ParamMode::Borrow) => { + place.cleanup.is_none() + } + (Operand::BorrowedPlace(place), align_ast::ParamMode::BorrowMut) => { + let move_pointee = align_sema::needs_drop_flag( + *ty, + &program.structs, + &program.tuples, + &program.enums, + &program.tagged_types, + ); + place.cleanup.is_some() == move_pointee + && (!move_pointee || place.path.is_empty()) + } + (Operand::BorrowedPlace(_), _) => false, + (_, align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut) => false, + _ => true, + }) +} + fn direct_runtime_key_is_valid(key: RuntimeKey, args: &[Ty], ret: Ty, program: &Program) -> bool { let i64_ty = Ty::Int(IntTy { bits: 64, @@ -3190,6 +3393,7 @@ fn callable_declarations( ret: function.ret, borrow: function.return_borrow.clone(), region: function.return_region.clone(), + cleanup: function.return_cleanup, }, )?; } @@ -3204,9 +3408,13 @@ fn callable_declarations( ret: function.ret, borrow: function.return_borrow.clone(), region: function.return_region.clone(), + cleanup: function.return_cleanup, }, )?; } + for declaration in declarations.values() { + canonical_signature(&declaration.signature, program)?; + } Ok(declarations) } @@ -3235,6 +3443,7 @@ fn callable_preflight( .is_some_and(|(args, ret)| { direct_runtime_key_is_valid(*key, args, ret, program) }) + || args.iter().any(|argument| matches!(argument, Operand::BorrowedPlace(_))) { return Err(CodegenError::Lowering( "callable metadata invalid:InvalidGraph".to_owned(), @@ -3250,8 +3459,65 @@ fn callable_preflight( .map(|operand| preflight_operand_ty(function, operand)) .collect::>>(); let result = function.value_tys.get(*value as usize).copied(); - if argument_types.as_deref() != Some(declaration.signature.params.as_slice()) - || result != Some(declaration.signature.ret) + let types_match = match argument_types.as_deref() { + Some(actual) => { + source_tys_match(actual, &declaration.signature.params, program)? + } + None => false, + }; + let result_matches = match result { + Some(actual) => { + source_ty_matches(actual, declaration.signature.ret, program)? + } + None => false, + }; + if !types_match + || !operands_match_modes( + args, + &declaration.signature.modes, + &declaration.signature.params, + program, + ) + || !result_matches + || declaration.signature.cleanup != hir::ReturnCleanupAbi::None + { + return Err(callable_target_error(target)); + } + } + Rvalue::CallWithCleanup(call) => { + let align_mir::DirectCallWithCleanup { target, args, cleanup } = call.as_ref(); + let Some(declaration) = declarations.get(target) else { + return Err(callable_target_error(target)); + }; + let argument_types = args + .iter() + .map(|operand| preflight_operand_ty(function, operand)) + .collect::>>(); + let result = function.value_tys.get(*value as usize).copied(); + let cleanup_ty = function.value_tys.get(*cleanup as usize).copied(); + let types_match = match argument_types.as_deref() { + Some(actual) => { + source_tys_match(actual, &declaration.signature.params, program)? + } + None => false, + }; + let result_matches = match result { + Some(actual) => { + source_ty_matches(actual, declaration.signature.ret, program)? + } + None => false, + }; + if !types_match + || !operands_match_modes( + args, + &declaration.signature.modes, + &declaration.signature.params, + program, + ) + || !result_matches + || cleanup_ty != Some(Ty::Bool) + || declaration.signature.cleanup + != hir::ReturnCleanupAbi::DynamicBit { return Err(callable_target_error(target)); } @@ -3264,6 +3530,7 @@ fn callable_preflight( || signature.param_modes != declaration.signature.modes || signature.return_borrow != declaration.signature.borrow || signature.return_region != declaration.signature.region + || signature.return_cleanup != declaration.signature.cleanup { return Err(callable_target_error(target)); } @@ -3321,6 +3588,7 @@ fn callable_preflight( || signature.param_modes != explicit_modes || signature.return_borrow != declaration.signature.borrow || signature.return_region != declaration.signature.region + || signature.return_cleanup != declaration.signature.cleanup || captures .iter() .zip(capture_tys) @@ -3334,6 +3602,7 @@ fn callable_preflight( ret: declaration.signature.ret, borrow: declaration.signature.borrow.clone(), region: declaration.signature.region.clone(), + cleanup: declaration.signature.cleanup, }; generated.push(GeneratedId::Closure { lifted: lifted.clone(), @@ -4309,6 +4578,43 @@ fn abi_map_ty<'c>( } } +fn align_return_type<'c>( + ctx: &'c Context, + value: BasicTypeEnum<'c>, + cleanup: hir::ReturnCleanupAbi, +) -> BasicTypeEnum<'c> { + match cleanup { + hir::ReturnCleanupAbi::None => value, + hir::ReturnCleanupAbi::DynamicBit => ctx + .struct_type(&[value, ctx.bool_type().into()], false) + .into(), + } +} + +fn abi_param_type<'c>( + ctx: &'c Context, + value: BasicTypeEnum<'c>, + mode: align_ast::ParamMode, + move_pointee: bool, +) -> BasicMetadataTypeEnum<'c> { + match mode { + align_ast::ParamMode::Borrow => { + ctx.ptr_type(AddressSpace::default()).into() + } + align_ast::ParamMode::BorrowMut if move_pointee => ctx + .struct_type( + &[ + ctx.ptr_type(AddressSpace::default()).into(), + ctx.ptr_type(AddressSpace::default()).into(), + ], + false, + ) + .into(), + align_ast::ParamMode::BorrowMut => ctx.ptr_type(AddressSpace::default()).into(), + align_ast::ParamMode::ByValue | align_ast::ParamMode::Out => value.into(), + } +} + // The type-table + `exports` parameters are each independently threaded through from `build_module` // (no natural grouping struct exists yet for "the type tables"); splitting them into a bag-of-fields // struct would obscure more than it clarifies for a single call site. @@ -4322,6 +4628,7 @@ fn declare_fn<'c>( enum_types: &[StructType<'c>], tagged_types: &[StructType<'c>], tuple_types: &[StructType<'c>], + program: &Program, exports: &[String], ) -> FunctionValue<'c> { let map = |ty: Ty| -> BasicTypeEnum<'c> { @@ -4330,15 +4637,31 @@ fn declare_fn<'c>( let param_types: Vec = f .params .iter() - .map(|s| map(f.slots[*s as usize]).into()) + .zip(&f.param_modes) + .map(|(s, mode)| { + let ty = f.slots[*s as usize]; + abi_param_type( + ctx, + map(ty), + *mode, + align_sema::needs_drop_flag( + ty, + &program.structs, + &program.tuples, + &program.enums, + &program.tagged_types, + ), + ) + }) .collect(); let fn_ty = if f.ret == Ty::Unit { ctx.void_type().fn_type(¶m_types, false) } else { - map(f.ret).fn_type(¶m_types, false) + align_return_type(ctx, map(f.ret), f.return_cleanup).fn_type(¶m_types, false) }; let fv = module.add_function(symbol, fn_ty, None); mark_nounwind(ctx, fv); + mark_borrow_param_contracts(ctx, fv, &f.param_modes); // Every Align program function is module-private (internal) EXCEPT: // - the C entry: an `-> i32` `main` keeps the symbol name `main` and IS the C entry (`crt0` // resolves it by name), so it must stay external. A `Result`- or `Unit`-returning main body @@ -4374,6 +4697,7 @@ fn declare_fn<'c>( /// value as their aggregate type; scalars/views via `abi_type`), so the call type matches the /// owning unit's definition and the linker binds them. Linkage stays external (an undefined symbol /// cannot be internal); `nounwind` matches the Align contract every program function carries. +#[allow(clippy::too_many_arguments)] // mirrors declare_fn across the same ABI type tables fn declare_imported_fn<'c>( ctx: &'c Context, module: &Module<'c>, @@ -4382,22 +4706,66 @@ fn declare_imported_fn<'c>( enum_types: &[StructType<'c>], tagged_types: &[StructType<'c>], tuple_types: &[StructType<'c>], + program: &Program, ) -> FunctionValue<'c> { let map = |ty: Ty| -> BasicTypeEnum<'c> { abi_map_ty(ctx, ty, struct_types, enum_types, tagged_types, tuple_types) }; - let param_types: Vec = - imp.params.iter().map(|&ty| map(ty).into()).collect(); + let param_types: Vec = imp + .params + .iter() + .zip(&imp.param_modes) + .map(|(&ty, &mode)| { + abi_param_type( + ctx, + map(ty), + mode, + align_sema::needs_drop_flag( + ty, + &program.structs, + &program.tuples, + &program.enums, + &program.tagged_types, + ), + ) + }) + .collect(); let fn_ty = if imp.ret == Ty::Unit { ctx.void_type().fn_type(¶m_types, false) } else { - map(imp.ret).fn_type(¶m_types, false) + align_return_type(ctx, map(imp.ret), imp.return_cleanup).fn_type(¶m_types, false) }; let fv = module.add_function(&encoded_program_symbol(&imp.name), fn_ty, None); mark_nounwind(ctx, fv); + mark_borrow_param_contracts(ctx, fv, &imp.param_modes); fv } +fn mark_borrow_param_contracts( + ctx: &Context, + function: FunctionValue<'_>, + modes: &[align_ast::ParamMode], +) { + mark_borrow_param_contracts_at(ctx, function, modes, 0); +} + +fn mark_borrow_param_contracts_at( + ctx: &Context, + function: FunctionValue<'_>, + modes: &[align_ast::ParamMode], + offset: u32, +) { + for (index, mode) in modes.iter().copied().enumerate() { + if mode != align_ast::ParamMode::Borrow { + continue; + } + let location = inkwell::attributes::AttributeLoc::Param(index as u32 + offset); + add_enum_attr(ctx, function, location, "nonnull"); + add_valued_enum_attr(ctx, function, location, "captures", CAPTURES_NONE); + add_enum_attr(ctx, function, location, "readonly"); + } +} + /// Mark a function `nounwind`: Align functions never unwind — errors are `Result` values and a /// fatal fault (`abort`) does not unwind (settled "no unwinding, immediate abort"; codegen emits /// plain `call`, never `invoke`). The attribute lets LLVM drop exception edges / unwind tables and @@ -4794,6 +5162,9 @@ struct FnGen<'c, 'a> { f: &'a Function, func: FunctionValue<'c>, slots: HashMap>, + /// Hidden caller cleanup-bit pointers for whole-Move `BorrowMut` parameters, keyed by their + /// logical parameter slots. + borrow_mut_cleanup_ptrs: HashMap>, values: HashMap>, /// Conservative whole-MIR proof for builder headers whose pointer never leaves its defining /// function/local. Each selected local gets one reusable 64-byte entry alloca; new/load value @@ -4985,6 +5356,11 @@ fn stack_header_plan(f: &Function) -> StackHeaderPlan { reject_header_operand(op, &load_defs, &owner, &mut bad); } } + Rvalue::CallWithCleanup(call) => { + for op in &call.args { + reject_header_operand(op, &load_defs, &owner, &mut bad); + } + } // A hoisted `str_finder` plan (doc-13 §6.6) never holds a builder header; its // needle/plan/haystack operands are `str` views / an opaque plan pointer. // Audit them anyway (a header can never be one, so this only ever passes) so @@ -5003,6 +5379,12 @@ fn stack_header_plan(f: &Function) -> StackHeaderPlan { reject_header_operand(op, &load_defs, &owner, &mut bad); } } + Rvalue::CallIndirectWithCleanup(call) => { + reject_header_operand(&call.callee, &load_defs, &owner, &mut bad); + for op in &call.args { + reject_header_operand(op, &load_defs, &owner, &mut bad); + } + } Rvalue::Closure { captures, .. } => { for op in captures { reject_header_operand(op, &load_defs, &owner, &mut bad); @@ -5029,8 +5411,15 @@ fn stack_header_plan(f: &Function) -> StackHeaderPlan { _ => {} } } - if let Term::Return(Some(op)) = &block.term { - reject_header_operand(op, &load_defs, &owner, &mut bad); + match &block.term { + Term::Return(Some(op)) => { + reject_header_operand(op, &load_defs, &owner, &mut bad); + } + Term::ReturnWithCleanup(returned) => { + reject_header_operand(&returned.0, &load_defs, &owner, &mut bad); + reject_header_operand(&returned.1, &load_defs, &owner, &mut bad); + } + _ => {} } } @@ -6053,6 +6442,43 @@ impl<'c, 'a> FnGen<'c, 'a> { let entry = self.blocks[self.f.entry as usize]; self.builder.position_at_end(entry); for (i, ty) in self.f.slots.iter().enumerate() { + if let Some(parameter) = self.f.params.iter().position(|slot| *slot as usize == i) + && matches!( + self.f.param_modes.get(parameter), + Some(align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut) + ) + { + let incoming = self + .func + .get_nth_param(parameter as u32) + .ok_or_else(|| self.err(format!("borrowed parameter {parameter} is missing")))?; + let pointer = if self + .f + .borrow_mut_cleanup_slots + .get(parameter) + .copied() + .flatten() + .is_some() + { + let aggregate = incoming.into_struct_value(); + let pointer = self + .builder + .build_extract_value(aggregate, 0, "borrow.mut.ptr") + .map_err(|error| self.err(error))? + .into_pointer_value(); + let cleanup = self + .builder + .build_extract_value(aggregate, 1, "borrow.mut.cleanup.ptr") + .map_err(|error| self.err(error))? + .into_pointer_value(); + self.borrow_mut_cleanup_ptrs.insert(i as Slot, cleanup); + pointer + } else { + incoming.into_pointer_value() + }; + self.slots.insert(i as Slot, pointer); + continue; + } let llty = self.llvm_type(*ty); let ptr = self .builder @@ -6133,6 +6559,16 @@ impl<'c, 'a> FnGen<'c, 'a> { } } Stmt::Store(slot, op) => { + let incoming_borrow = match op { + Operand::Arg(index) => matches!( + self.f.param_modes.get(*index as usize), + Some(align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut) + ), + _ => false, + }; + if incoming_borrow { + continue; + } let val = self.operand(op)?; let ptr = self.slots[slot]; self.builder.build_store(ptr, val).map_err(|e| self.err(e))?; @@ -6629,12 +7065,48 @@ impl<'c, 'a> FnGen<'c, 'a> { .map_err(|e| self.err(e))?; } Term::Return(Some(op)) => { + if self.f.return_cleanup != hir::ReturnCleanupAbi::None { + return Err(self.err("dynamic-cleanup function returned without cleanup bit")); + } + self.writeback_borrow_mut_cleanup()?; let v = self.operand(op)?; self.builder.build_return(Some(&v)).map_err(|e| self.err(e))?; } Term::Return(None) => { + if self.f.return_cleanup != hir::ReturnCleanupAbi::None { + return Err(self.err("dynamic-cleanup function returned no value or cleanup bit")); + } + self.writeback_borrow_mut_cleanup()?; self.builder.build_return(None).map_err(|e| self.err(e))?; } + Term::ReturnWithCleanup(returned) => { + let (value, cleanup) = returned.as_ref(); + if self.f.return_cleanup != hir::ReturnCleanupAbi::DynamicBit { + return Err(self.err("copy-return function carried an extra cleanup bit")); + } + self.writeback_borrow_mut_cleanup()?; + let value = self.operand(value)?; + let cleanup = self.operand(cleanup)?.into_int_value(); + let return_ty = align_return_type( + self.ctx, + self.llvm_type(self.f.ret), + hir::ReturnCleanupAbi::DynamicBit, + ) + .into_struct_type(); + let with_value = self + .builder + .build_insert_value(return_ty.const_zero(), value, 0, "ret.value") + .map_err(|e| self.err(e))? + .into_struct_value(); + let result = self + .builder + .build_insert_value(with_value, cleanup, 1, "ret.cleanup") + .map_err(|e| self.err(e))? + .into_struct_value(); + self.builder + .build_return(Some(&result)) + .map_err(|e| self.err(e))?; + } Term::Unreachable => { self.builder.build_unreachable().map_err(|e| self.err(e))?; } @@ -6642,6 +7114,35 @@ impl<'c, 'a> FnGen<'c, 'a> { Ok(()) } + fn writeback_borrow_mut_cleanup(&self) -> Result<(), CodegenError> { + for (index, cleanup_slot) in self.f.borrow_mut_cleanup_slots.iter().enumerate() { + let Some(cleanup_slot) = cleanup_slot else { continue }; + let parameter_slot = *self + .f + .params + .get(index) + .ok_or_else(|| self.err("borrowed cleanup parameter is missing"))?; + let destination = self + .borrow_mut_cleanup_ptrs + .get(¶meter_slot) + .copied() + .ok_or_else(|| self.err("borrowed cleanup writeback pointer is missing"))?; + let source = self + .slots + .get(cleanup_slot) + .copied() + .ok_or_else(|| self.err("borrowed cleanup proxy slot is missing"))?; + let value = self + .builder + .build_load(self.ctx.bool_type(), source, "borrow.cleanup.out") + .map_err(|error| self.err(error))?; + self.builder + .build_store(destination, value) + .map_err(|error| self.err(error))?; + } + Ok(()) + } + /// Lower an rvalue. Returns `None` for a value-less result (a void call). /// `result_ty` is the type of the value being defined (needed to build a bare `None`). fn gen_rvalue(&mut self, result_id: ValueId, rv: &Rvalue, result_ty: Ty) -> Result>, CodegenError> { @@ -10047,6 +10548,14 @@ impl<'c, 'a> FnGen<'c, 'a> { return Ok(call.try_as_basic_value().basic()); } Rvalue::Call(DirectCall::Program(name), args) => { + let declaration = self + .callable_preflight + .declarations + .get(name) + .ok_or_else(|| callable_target_error(name))?; + if declaration.signature.cleanup != hir::ReturnCleanupAbi::None { + return Err(self.err("dynamic-cleanup call omitted its cleanup result")); + } let callee = self .program_funcs .get(name) @@ -10113,6 +10622,46 @@ impl<'c, 'a> FnGen<'c, 'a> { } return Ok(cs.try_as_basic_value().basic()); } + Rvalue::CallWithCleanup(call) => { + let align_mir::DirectCallWithCleanup { target, args, cleanup } = call.as_ref(); + let declaration = self + .callable_preflight + .declarations + .get(target) + .ok_or_else(|| callable_target_error(target))?; + if declaration.signature.cleanup != hir::ReturnCleanupAbi::DynamicBit + || self.extern_abi.contains_key(target) + { + return Err(self.err("call carried an unexpected cleanup result")); + } + let callee = self + .program_funcs + .get(target) + .copied() + .ok_or_else(|| callable_target_error(target))?; + let argv = args + .iter() + .map(|operand| self.operand(operand).map(Into::into)) + .collect::>, _>>()?; + let returned = self + .builder + .build_call(callee, &argv, "call.cleanup") + .map_err(|error| self.err(error))? + .try_as_basic_value() + .basic() + .ok_or_else(|| self.err("dynamic-cleanup call returned void"))? + .into_struct_value(); + let value = self + .builder + .build_extract_value(returned, 0, "call.value") + .map_err(|error| self.err(error))?; + let flag = self + .builder + .build_extract_value(returned, 1, "call.cleanup.bit") + .map_err(|error| self.err(error))?; + self.values.insert(*cleanup, flag); + return Ok(Some(value)); + } Rvalue::FnAddr { target, .. } => { // A non-capturing function value: `{ thunk_ptr, null_env }`. let declaration = self @@ -10195,6 +10744,7 @@ impl<'c, 'a> FnGen<'c, 'a> { ret: declaration.signature.ret, borrow: declaration.signature.borrow.clone(), region: declaration.signature.region.clone(), + cleanup: declaration.signature.cleanup, }; let id = GeneratedId::Closure { lifted: lifted.clone(), @@ -10222,13 +10772,30 @@ impl<'c, 'a> FnGen<'c, 'a> { .into() } Rvalue::CallIndirect { callee, args, param_tys, ret_ty, .. } => { + let Rvalue::CallIndirect { signature, .. } = rv else { unreachable!() }; + if signature.return_cleanup != hir::ReturnCleanupAbi::None { + return Err(self.err("dynamic-cleanup indirect call omitted its cleanup result")); + } // Extract `{ fn_ptr, env_ptr }` and call with the env-ABI `fn(env, args)`. let clos = self.operand(callee)?.into_struct_value(); let fn_ptr = self.builder.build_extract_value(clos, 0, "cf").map_err(|e| self.err(e))?.into_pointer_value(); let env = self.builder.build_extract_value(clos, 1, "ce").map_err(|e| self.err(e))?; let mut param_meta: Vec = vec![self.ctx.ptr_type(AddressSpace::default()).into()]; - param_meta.extend(param_tys.iter().map(|t| BasicMetadataTypeEnum::from(self.llvm_type(*t)))); + param_meta.extend(param_tys.iter().zip(&signature.param_modes).map(|(ty, mode)| { + abi_param_type( + self.ctx, + self.llvm_type(*ty), + *mode, + align_sema::needs_drop_flag( + *ty, + &self.program.structs, + &self.program.tuples, + &self.program.enums, + &self.program.tagged_types, + ), + ) + })); let mut argv: Vec = vec![env.into()]; for o in args { argv.push(inkwell::values::BasicMetadataValueEnum::from(self.operand(o)?)); @@ -10251,6 +10818,86 @@ impl<'c, 'a> FnGen<'c, 'a> { .map_err(|e| self.err(e))?; return Ok(cs.try_as_basic_value().basic()); } + Rvalue::CallIndirectWithCleanup(call) => { + let align_mir::IndirectCallWithCleanup { + callee, + args, + param_tys, + ret_ty, + signature, + cleanup, + } = call.as_ref(); + if signature.return_cleanup != hir::ReturnCleanupAbi::DynamicBit + || *ret_ty == Ty::Unit + { + return Err(self.err("indirect call carried an unexpected cleanup result")); + } + let clos = self.operand(callee)?.into_struct_value(); + let fn_ptr = self + .builder + .build_extract_value(clos, 0, "cf") + .map_err(|e| self.err(e))? + .into_pointer_value(); + let env = self + .builder + .build_extract_value(clos, 1, "ce") + .map_err(|e| self.err(e))?; + let mut param_meta: Vec = + vec![self.ctx.ptr_type(AddressSpace::default()).into()]; + param_meta.extend( + param_tys + .iter() + .zip(&signature.param_modes) + .map(|(ty, mode)| { + abi_param_type( + self.ctx, + self.llvm_type(*ty), + *mode, + align_sema::needs_drop_flag( + *ty, + &self.program.structs, + &self.program.tuples, + &self.program.enums, + &self.program.tagged_types, + ), + ) + }), + ); + let mut argv: Vec = vec![env.into()]; + for operand in args { + argv.push(inkwell::values::BasicMetadataValueEnum::from( + self.operand(operand)?, + )); + } + let return_ty = align_return_type( + self.ctx, + self.llvm_type(*ret_ty), + hir::ReturnCleanupAbi::DynamicBit, + ); + let returned = self + .builder + .build_indirect_call( + return_ty.fn_type(¶m_meta, false), + fn_ptr, + &argv, + "icall.cleanup", + ) + .map_err(|e| self.err(e))? + .try_as_basic_value() + .basic() + .ok_or_else(|| self.err("dynamic-cleanup indirect call returned void"))? + .into_struct_value(); + let value = self + .builder + .build_extract_value(returned, 0, "icall.value") + .map_err(|e| self.err(e))?; + let flag = self + .builder + .build_extract_value(returned, 1, "icall.cleanup.bit") + .map_err(|e| self.err(e))?; + self.values.insert(*cleanup, flag); + return Ok(Some(value)); + } }; Ok(Some(v)) } @@ -12400,9 +13047,67 @@ impl<'c, 'a> FnGen<'c, 'a> { .copied() .ok_or_else(|| self.err(format!("parameter index {index} references missing slot {slot}"))) } + Operand::BorrowedPlace(place) => self.checked_borrowed_place_ty(place), + Operand::BorrowedCleanupArg(index) => { + self.f + .borrow_mut_cleanup_slots + .get(*index as usize) + .copied() + .flatten() + .map(|_| Ty::Bool) + .ok_or_else(|| self.err(format!("parameter {index} has no borrowed cleanup bit"))) + } } } + fn checked_borrowed_place_ty( + &self, + place: &align_mir::BorrowedPlace, + ) -> Result { + let mut ty = self + .f + .slots + .get(place.slot as usize) + .copied() + .ok_or_else(|| self.err(format!("borrowed place references missing slot {}", place.slot)))?; + for &field in &place.path { + let Ty::Struct(id) = ty else { + return Err(self.err("borrowed place field path crosses a non-struct type")); + }; + ty = self + .structs + .get(id as usize) + .and_then(|definition| definition.fields.get(field as usize)) + .map(|field| field.ty) + .ok_or_else(|| self.err("borrowed place field path is out of bounds"))?; + } + if ty != place.ty { + return Err(self.err("borrowed place type disagrees with its field path")); + } + if place + .cleanup + .is_some_and(|slot| self.f.slots.get(slot as usize) != Some(&Ty::Bool)) + { + return Err(self.err("borrowed place cleanup slot is missing or not bool")); + } + Ok(ty) + } + + fn borrowed_place_ptr( + &self, + place: &align_mir::BorrowedPlace, + ) -> Result, CodegenError> { + self.checked_borrowed_place_ty(place)?; + if place.path.is_empty() { + return self + .slots + .get(&place.slot) + .copied() + .ok_or_else(|| self.err(format!("borrowed place references missing slot {}", place.slot))); + } + self.field_path_ptr(place.slot, &place.path) + } + /// Read an operand's LLVM value. **Fallible on purpose:** the two non-constant forms look the /// value up in a table, and neither lookup is guaranteed by the type system alone. /// @@ -12431,6 +13136,54 @@ impl<'c, 'a> FnGen<'c, 'a> { .func .get_nth_param(*i) .ok_or_else(|| self.err(format!("parameter index {i} is out of range")))?, + Operand::BorrowedPlace(place) => { + let pointer = self.borrowed_place_ptr(place)?; + if let Some(cleanup) = place.cleanup { + let cleanup_pointer = self + .slots + .get(&cleanup) + .copied() + .ok_or_else(|| self.err(format!("borrowed cleanup slot {cleanup} is missing")))?; + let pair = self.ctx.struct_type( + &[ + self.ctx.ptr_type(AddressSpace::default()).into(), + self.ctx.ptr_type(AddressSpace::default()).into(), + ], + false, + ); + let with_pointer = self + .builder + .build_insert_value(pair.get_poison(), pointer, 0, "borrow.mut.pair.ptr") + .map_err(|error| self.err(error))?; + self.builder + .build_insert_value( + with_pointer, + cleanup_pointer, + 1, + "borrow.mut.pair.cleanup", + ) + .map_err(|error| self.err(error))? + .into_struct_value() + .into() + } else { + pointer.into() + } + } + Operand::BorrowedCleanupArg(index) => { + let slot = *self + .f + .params + .get(*index as usize) + .ok_or_else(|| self.err(format!("parameter index {index} is out of range")))?; + let pointer = self + .borrow_mut_cleanup_ptrs + .get(&slot) + .copied() + .ok_or_else(|| self.err(format!("parameter {index} has no cleanup pointer")))?; + self.builder + .build_load(self.ctx.bool_type(), pointer, "borrow.cleanup.in") + .map_err(|error| self.err(error))? + } }) } } @@ -12553,6 +13306,7 @@ mod tests { ret: Ty::Int(IntTy { bits: 64, signed: true }), return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }); let per_unit = emit_llvm_ir(&per_unit, &BuildTarget::Baseline, false, &[], None).unwrap(); assert_eq!(declarations(&per_unit), expected); @@ -12593,6 +13347,7 @@ mod tests { ret, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }; let mut internal_program = mir("fn main() -> i32 = 0\n"); internal_program.externs = vec![ @@ -12688,6 +13443,93 @@ mod tests { ); } + #[test] + fn malformed_c_extern_borrow_modes_fail_before_abi_lowering() { + for mode in [align_ast::ParamMode::Borrow, align_ast::ParamMode::BorrowMut] { + let mut program = mir( + "extern \"C\" fn consume(value: i64)\nfn main() -> i32 = 0\n", + ); + program.externs[0].param_modes[0] = mode; + let error = emit_llvm_ir(&program, &BuildTarget::Baseline, false, &[], None) + .expect_err("a C declaration cannot carry an Align borrow ABI"); + assert_lowering( + error, + "extern function `consume` uses a non-by-value parameter mode at the C boundary", + ); + } + } + + #[test] + fn malformed_borrowed_place_type_fails_before_llvm_call_emission() { + let mut program = mir( + "fn inspect(borrow value: string) -> i64 = value.len()\n\ + fn main() -> i32 { value := \"align\".clone(); return inspect(value) as i32 }\n", + ); + let mut changed = false; + for function in &mut program.fns { + for block in &mut function.blocks { + for statement in &mut block.stmts { + let Stmt::Let(_, Rvalue::Call(DirectCall::Program(target), args)) = statement + else { + continue; + }; + if target.as_str() == "inspect" + && let Operand::BorrowedPlace(place) = &mut args[0] + { + place.ty = Ty::Bool; + changed = true; + } + } + } + } + assert!(changed, "fixture must contain the borrowed call operand"); + let error = emit_llvm_ir(&program, &BuildTarget::Baseline, false, &[], None) + .expect_err("a borrowed place with a forged type must fail closed"); + assert_lowering(error, "callable target invalid:696e7370656374"); + } + + #[test] + fn malformed_move_borrow_mut_cleanup_fails_before_llvm_call_emission() { + let source = "fn replace(borrow mut value: string) { value = \"new\".clone() }\n\ + fn main() -> i32 { mut value := \"old\".clone(); replace(value); return 0 }\n"; + let mut callee = mir(source); + let replace = callee + .fns + .iter_mut() + .find(|function| function.name.as_str() == "replace") + .expect("replace function"); + replace.borrow_mut_cleanup_slots[0] = None; + let error = emit_llvm_ir(&callee, &BuildTarget::Baseline, false, &[], None) + .expect_err("a Move BorrowMut callee needs its cleanup proxy"); + assert_lowering( + error, + "function `replace` parameter 0 has malformed BorrowMut cleanup storage", + ); + + let mut caller = mir(source); + let mut changed = false; + for function in &mut caller.fns { + for block in &mut function.blocks { + for statement in &mut block.stmts { + let Stmt::Let(_, Rvalue::Call(DirectCall::Program(target), args)) = statement + else { + continue; + }; + if target.as_str() == "replace" + && let Operand::BorrowedPlace(place) = &mut args[0] + { + place.cleanup = None; + changed = true; + } + } + } + } + assert!(changed, "fixture must contain the exclusive borrowed call operand"); + let error = emit_llvm_ir(&caller, &BuildTarget::Baseline, false, &[], None) + .expect_err("a Move BorrowMut call needs the caller cleanup slot"); + assert_lowering(error, "callable target invalid:7265706c616365"); + } + #[test] fn runtime_abi_source_compatible_externs_receive_each_attribute_class() { let program = mir( @@ -13323,8 +14165,10 @@ mod tests { name: program_call("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, ret: i32_ty, slots, slot_align, @@ -13397,9 +14241,11 @@ mod tests { name: program_call("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i32_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![], slot_align: vec![], value_tys: vec![Ty::Tagged(7)], @@ -13437,9 +14283,11 @@ mod tests { name: program_call("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i32_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![], slot_align: vec![], value_tys: vec![Ty::Fn(0)], @@ -13451,11 +14299,12 @@ mod tests { lifted: program_call("unused"), captures: vec![], capture_tys: vec![Ty::Tagged(7)], - signature: align_mir::FnSignatureFacts { + signature: Box::new(align_mir::FnSignatureFacts { param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, - }, + return_cleanup: hir::ReturnCleanupAbi::None, + }), }, )], stmt_lines: vec![(0, 0)], @@ -13490,9 +14339,11 @@ mod tests { name: program_call("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i32_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![], slot_align: vec![], value_tys: vec![i32_ty], @@ -13537,9 +14388,11 @@ mod tests { name: program_call("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i32_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![], slot_align: vec![], value_tys: vec![Ty::Tagged(0)], @@ -13588,9 +14441,11 @@ mod tests { name: program_call("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i32_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![], slot_align: vec![], value_tys: vec![Ty::Tagged(0)], @@ -13664,9 +14519,11 @@ mod tests { name: program_call("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i32_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![], slot_align: vec![], value_tys: vec![], @@ -13771,6 +14628,7 @@ mod tests { .push(align_ast::ParamMode::ByValue); header_cycle.fns[0].slots.push(cyclic_ty); header_cycle.fns[0].ret = cyclic_ty; + header_cycle.fns[0].return_cleanup = hir::ReturnCleanupAbi::DynamicBit; header_cycle.fns[0].return_borrow = hir::ReturnBorrowSummary::Roots { params: vec![0], captures: vec![], @@ -13913,9 +14771,11 @@ mod tests { name: program_call("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i32_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![Ty::Tagged(0)], slot_align: vec![None], value_tys: vec![], @@ -13953,9 +14813,11 @@ mod tests { name: program_call("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i32_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![Ty::Tuple(0)], slot_align: vec![None], value_tys: vec![], @@ -13991,9 +14853,11 @@ mod tests { name: program_call("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i32_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![Ty::Tuple(0)], slot_align: vec![None], value_tys: vec![], @@ -14047,9 +14911,11 @@ mod tests { name: program_call("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i32_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![], slot_align: vec![], value_tys, @@ -14167,9 +15033,11 @@ mod tests { name: program_call("u"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: Ty::Unit, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![], slot_align: vec![], value_tys: vec![], @@ -14203,9 +15071,11 @@ mod tests { name: program_call("return_str"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: Ty::Str, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![i64_ty], slot_align: vec![None], value_tys: vec![Ty::Str], @@ -14252,9 +15122,11 @@ mod tests { name: program_call("return_i64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i64_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![i64_ty], slot_align: vec![None], value_tys: vec![i64_ty], @@ -14306,9 +15178,11 @@ mod tests { name: program_call("return_i64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i64_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![string_ty], slot_align: vec![None], value_tys: vec![], @@ -14357,9 +15231,11 @@ mod tests { name: program_call("return_i64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i64_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![i64_ty], slot_align: vec![None], value_tys: vec![], @@ -14402,9 +15278,11 @@ mod tests { name: program_call("return_i64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i64_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![i64_ty], slot_align: vec![None], value_tys: vec![], @@ -14448,9 +15326,11 @@ mod tests { name: program_call("keep"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: Ty::Bool, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![input_ty], slot_align: vec![None], value_tys: vec![], @@ -14462,9 +15342,11 @@ mod tests { name: program_call("finish"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i64_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![wrong_output_ty], slot_align: vec![None], value_tys: vec![], @@ -14515,9 +15397,11 @@ mod tests { name: program_call("finish"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i64_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![str_ty], slot_align: vec![None], value_tys: vec![], @@ -14572,9 +15456,11 @@ mod tests { name: program_call("return_i64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i64_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![i64_ty], slot_align: vec![None], value_tys: vec![], @@ -14613,9 +15499,11 @@ mod tests { name: program_call("return_i64"), params: vec![0, 1], param_modes: vec![align_ast::ParamMode::ByValue, align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i64_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![i64_ty, i64_ty], slot_align: vec![None, None], value_tys: vec![], @@ -14658,9 +15546,11 @@ mod tests { name: program_call("return_u64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i64_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![u64_ty], slot_align: vec![None], value_tys: vec![], @@ -14796,9 +15686,11 @@ mod tests { name: program_call(if count_arg { "allocation_probe" } else { "main" }), params: if count_arg { vec![0] } else { vec![] }, param_modes: if count_arg { vec![align_ast::ParamMode::ByValue] } else { vec![] }, + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: Ty::Int(IntTy { bits: 32, signed: true }), + return_cleanup: hir::ReturnCleanupAbi::None, slots: if count_arg { vec![Ty::Int(IntTy { bits: 64, signed: true })] } else { vec![] }, slot_align: if count_arg { vec![None] } else { vec![] }, value_tys: vec![value_ty], @@ -14851,9 +15743,11 @@ mod tests { name: program_call("arena_allocation_probe"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: Ty::Int(IntTy { bits: 32, signed: true }), + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![i64_ty], slot_align: vec![None], value_tys: vec![Ty::ArenaHandle, Ty::Box(Scalar::Str)], @@ -14895,9 +15789,11 @@ mod tests { name: program_call(if dynamic { "soa_allocation_probe" } else { "main" }), params: if dynamic { vec![0] } else { vec![] }, param_modes: if dynamic { vec![align_ast::ParamMode::ByValue] } else { vec![] }, + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: Ty::Int(IntTy { bits: 32, signed: true }), + return_cleanup: hir::ReturnCleanupAbi::None, slots: if dynamic { vec![i64_ty] } else { vec![] }, slot_align: if dynamic { vec![None] } else { vec![] }, value_tys: vec![Ty::ArenaHandle, Ty::Box(Scalar::Int(IntTy { bits: 8, signed: false }))], @@ -15696,9 +16592,11 @@ mod tests { name: program_call("future_wrapper"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: Ty::Unit, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![Ty::Builder], slot_align: vec![None], value_tys: vec![Ty::Builder, Ty::Builder, Ty::Unit], @@ -16170,9 +17068,11 @@ mod tests { name: program_call(name), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, ret: i64_ty, + return_cleanup: hir::ReturnCleanupAbi::None, slots: vec![i64_ty], slot_align: vec![None], value_tys: vec![i64_ty], @@ -16245,6 +17145,7 @@ mod tests { ret: i64_ty, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }); let conflict = emit_llvm_ir(&conflict, &BuildTarget::Baseline, false, &[], None) .expect_err("stored and extern declarations cannot share one logical target"); @@ -16296,6 +17197,7 @@ mod tests { ret: Ty::Unit, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }); let text = emit_llvm_ir(&fn_value, &BuildTarget::Baseline, false, &[], None) .expect("an occupied generated candidate must probe deterministically"); diff --git a/crates/align_driver/tests/borrowed_params.rs b/crates/align_driver/tests/borrowed_params.rs new file mode 100644 index 00000000..1de0c335 --- /dev/null +++ b/crates/align_driver/tests/borrowed_params.rs @@ -0,0 +1,237 @@ +//! L2d/L2e borrowed-parameter formation, ownership, ABI, aliasing, and per-unit parity. + +mod common; +use common::*; + +#[test] +fn shared_direct_call_keeps_move_owner_live() { + if !backend_available() { + return; + } + let source = "\ +fn size(borrow value: string) -> i64 = value.len() +fn main() -> i32 { + value := \"forty-two\".clone() + first := size(value) + second := size(value) + if first == 9 && second == 9 && value.len() == 9 { return 42 } + return 0 +} +"; + let output = build_and_run("borrow-shared-direct", source); + assert_eq!(output.status.code(), Some(42)); +} + +#[test] +fn shared_function_value_preserves_mode_and_owner() { + if !backend_available() { + return; + } + let source = "\ +fn size(borrow value: string) -> i64 = value.len() +fn apply(f: fn(borrow string) -> i64, borrow value: string) -> i64 = f(value) +fn main() -> i32 { + value := \"align\".clone() + f := size + if apply(f, value) == 5 && value.len() == 5 { return 42 } + return 0 +} +"; + let output = build_and_run("borrow-shared-indirect", source); + assert_eq!(output.status.code(), Some(42)); +} + +#[test] +fn shared_rejects_copy_temporary_move_and_aliasing() { + assert!(check_errs( + "borrow-shared-copy", + "fn inspect(borrow value: i64) -> i64 = value\nfn main() -> i32 = 0\n", + )); + assert!(check_errs( + "borrow-shared-temp", + "fn inspect(borrow value: string) -> i64 = value.len()\nfn main() -> i32 = inspect(\"x\".clone()) as i32\n", + )); + assert!(check_errs( + "borrow-shared-move", + "fn take(borrow value: string) -> string = value\nfn main() -> i32 = 0\n", + )); + assert!(check_errs( + "borrow-shared-alias", + "fn clash(borrow left: string, right: string) -> i64 = left.len() + right.len()\nfn main() -> i32 { value := \"x\".clone(); return clash(value, value) as i32 }\n", + )); +} + +#[test] +fn shared_returned_view_tracks_exact_owner_generation() { + let valid = "\ +fn view(borrow value: string) -> slice = value.bytes() +fn main() -> i32 { + value := \"align\".clone() + result := view(value) + if result.len() == 5 && value.len() == 5 { return 42 } + return 0 +} +"; + if backend_available() { + assert_eq!(build_and_run("borrow-shared-view", valid).status.code(), Some(42)); + } + let stale = "\ +fn view(borrow value: string) -> slice = value.bytes() +fn main() -> i32 { + value := \"align\".clone() + result := view(value) + moved := value + print(result) + print(moved) + return 0 +} +"; + assert!(check_errs("borrow-shared-view-stale", stale)); +} + +#[test] +fn shared_imported_call_matches_whole_program() { + let files = &[ + ( + "views.align", + "module views\npub fn size(borrow value: string) -> i64 = value.len()\npub fn view(borrow value: string) -> slice = value.bytes()\n", + ), + ( + "main.align", + "import views\nfn main() -> i32 { value := \"align\".clone(); bytes := views.view(value); if views.size(value) == 5 && bytes.len() == 5 && value.len() == 5 { return 42 }; return 0 }\n", + ), + ]; + let differential = diff_check_multi("borrow-shared-import", files, "main.align"); + assert_eq!(differential.whole_errors, differential.per_unit_errors); + assert!(!differential.whole_errors, "whole: {}\nper-unit: {}", differential.whole_diags, differential.per_unit_diags); + if backend_available() { + assert_eq!( + build_and_run_multi("borrow-shared-import-whole", files, "main.align") + .status + .code(), + Some(42), + ); + assert_eq!( + build_per_unit_multi("borrow-shared-import-per-unit", files, "main.align") + .link_and_run() + .status + .code(), + Some(42), + ); + } +} + +#[test] +fn exclusive_copy_and_field_updates_are_visible() { + if !backend_available() { + return; + } + let source = "\ +Counter { value: i64 } +fn increment(borrow mut value: i64) { value = value + 1 } +fn main() -> i32 { + mut scalar := 40 + mut counter := Counter { value: 1 } + increment(scalar) + increment(counter.value) + if scalar == 41 && counter.value == 2 { return 42 } + return 0 +} +"; + assert_eq!(build_and_run("borrow-exclusive-copy", source).status.code(), Some(42)); +} + +#[test] +fn exclusive_move_replacement_updates_caller_cleanup() { + if !backend_available() { + return; + } + let source = "\ +fn replace(borrow mut value: string) { value = \"replacement\".clone() } +fn leave(borrow mut value: string) { print(value.len()) } +fn main() -> i32 { + mut value := \"old\".clone() + leave(value) + replace(value) + if value.len() == 11 { return 42 } + return 0 +} +"; + assert_eq!(build_and_run("borrow-exclusive-move", source).status.code(), Some(42)); +} + +#[test] +fn exclusive_rejects_immutable_temporary_partial_move_and_stale_view() { + assert!(check_errs( + "borrow-exclusive-immutable", + "fn inc(borrow mut value: i64) { value = value + 1 }\nfn main() -> i32 { value := 1; inc(value); return 0 }\n", + )); + assert!(check_errs( + "borrow-exclusive-temp", + "fn inc(borrow mut value: i64) { value = value + 1 }\nfn main() -> i32 { inc(1 + 2); return 0 }\n", + )); + assert!(check_errs( + "borrow-exclusive-partial-move", + "Holder { value: string }\nfn replace(borrow mut value: string) { value = \"new\".clone() }\nfn main() -> i32 { mut holder := Holder { value: \"old\".clone() }; replace(holder.value); return 0 }\n", + )); + assert!(check_errs( + "borrow-exclusive-stale", + "fn replace(borrow mut value: string) { value = \"new\".clone() }\nfn main() -> i32 { mut value := \"old\".clone(); view := value.as_str(); replace(value); print(view); return 0 }\n", + )); +} + +#[test] +fn exclusive_all_peer_aliases_are_rejected() { + let cases = [ + "fn f(borrow mut a: string, b: string) {}", + "fn f(borrow mut a: string, borrow b: string) {}", + "fn f(borrow mut a: string, borrow mut b: string) {}", + ]; + for (index, declaration) in cases.iter().enumerate() { + let source = format!( + "{declaration}\nfn main() -> i32 {{ mut value := \"x\".clone(); f(value, value); return 0 }}\n" + ); + assert!(check_errs(&format!("borrow-exclusive-alias-{index}"), &source)); + } + + assert!(check_errs( + "borrow-exclusive-alias-copy-view", + "fn f(borrow mut owner: string, peer: str) {}\nfn main() -> i32 { mut owner := \"x\".clone(); peer := owner.as_str(); f(owner, peer); return 0 }\n", + )); + assert!(check_errs( + "borrow-exclusive-alias-copy-aggregate", + "View { text: str }\nfn f(borrow mut owner: string, peer: View) {}\nfn main() -> i32 { mut owner := \"x\".clone(); peer := View { text: owner.as_str() }; f(owner, peer); return 0 }\n", + )); +} + +#[test] +fn exclusive_function_value_and_imported_call_preserve_mode() { + let files = &[ + ( + "ops.align", + "module ops\npub fn increment(borrow mut value: i64) { value = value + 1 }\npub fn replace(borrow mut value: string) { value = \"replacement\".clone() }\n", + ), + ( + "main.align", + "import ops\nfn apply(f: fn(borrow mut i64) -> (), borrow mut value: i64) { f(value) }\nfn main() -> i32 { mut count := 40; mut value := \"old\".clone(); f := ops.increment; apply(f, count); ops.increment(count); ops.replace(value); if count == 42 && value.len() == 11 { return 42 }; return 0 }\n", + ), + ]; + let differential = diff_check_multi("borrow-exclusive-import", files, "main.align"); + assert_eq!(differential.whole_errors, differential.per_unit_errors); + assert!(!differential.whole_errors, "whole: {}\nper-unit: {}", differential.whole_diags, differential.per_unit_diags); + if backend_available() { + assert_eq!( + build_and_run_multi("borrow-exclusive-import-whole", files, "main.align") + .status + .code(), + Some(42), + ); + assert_eq!( + build_per_unit_multi("borrow-exclusive-import-per-unit", files, "main.align") + .link_and_run() + .status + .code(), + Some(42), + ); + } +} diff --git a/crates/align_driver/tests/interface_param_modes.rs b/crates/align_driver/tests/interface_param_modes.rs index a89d572b..6706049a 100644 --- a/crates/align_driver/tests/interface_param_modes.rs +++ b/crates/align_driver/tests/interface_param_modes.rs @@ -1,5 +1,5 @@ -//! L2a gate: parameter modes and explicit empty return-provenance summaries survive whole-program -//! and per-unit checking, HIR-to-MIR lowering, interface rendering, and imported declarations. +//! L2 signature gate: parameter modes and return-provenance summaries survive whole-program and +//! per-unit checking, HIR-to-MIR lowering, interface rendering, and imported declarations. mod common; use common::*; @@ -36,7 +36,7 @@ fn files() -> &'static [(&'static str, &'static str)] { #[test] fn whole_and_per_unit_interfaces_preserve_modes_and_explicit_none_summaries() { let checked = assert_same_verdict("l2a-interface-modes", files(), "main.align"); - assert!(!checked.diags.has_errors(), "L2a source must check"); + assert!(!checked.diags.has_errors(), "L2 source must check"); let buffer = checked .summaries @@ -184,17 +184,17 @@ fn malformed_mir_signature_facts_fail_before_llvm_emission() { "unexpected diagnostic: {error}" ); - let mut disabled_mode = buffer.mir.clone(); - let put = disabled_mode + let mut wrong_borrow_type = buffer.mir.clone(); + let put = wrong_borrow_type .fns .iter_mut() .find(|function| function.name.as_str() == "buffer$put") .expect("put MIR"); put.param_modes[0] = ParamMode::Borrow; - let error = emit_llvm_ir(&disabled_mode, BuildTarget::Baseline, false, &[], false) - .expect_err("disabled mode must fail"); + let error = emit_llvm_ir(&wrong_borrow_type, BuildTarget::Baseline, false, &[], false) + .expect_err("shared borrow of a Copy slice must fail"); assert!( - error.contains("before its ABI is enabled"), + error.contains("uses Borrow with a non-Move parameter type"), "unexpected diagnostic: {error}" ); @@ -215,20 +215,24 @@ fn malformed_mir_signature_facts_fail_before_llvm_emission() { "unexpected diagnostic: {error}" ); - let mut premature_capture = buffer.mir.clone(); - let put = premature_capture + let mut invalid_capture = buffer.mir.clone(); + let put = invalid_capture .fns .iter_mut() .find(|function| function.name.as_str() == "buffer$put") .expect("put MIR"); + put.return_borrow = ReturnBorrowSummary::Roots { + params: vec![], + captures: vec![0], + }; put.return_region = ReturnRegionSummary::Roots { params: vec![], captures: vec![0], }; - let error = emit_llvm_ir(&premature_capture, BuildTarget::Baseline, false, &[], false) - .expect_err("capture roots must remain disabled until L2b-b"); + let error = emit_llvm_ir(&invalid_capture, BuildTarget::Baseline, false, &[], false) + .expect_err("a non-borrowing return cannot carry capture roots"); assert!( - error.contains("capture roots before L2b-b"), + error.contains("cannot borrow"), "unexpected diagnostic: {error}" ); @@ -323,7 +327,7 @@ fn malformed_mir_signature_facts_fail_before_llvm_emission() { let error = emit_llvm_ir(&extern_mir, BuildTarget::Baseline, false, &[], false) .expect_err("unanalyzed extern roots must retain the conservative fallback"); assert!( - error.contains("function-value provenance lands in L2b-b"), + error.contains("cannot carry return provenance across an unanalyzed extern boundary"), "unexpected diagnostic: {error}" ); } @@ -396,8 +400,8 @@ fn main() -> i32 = apply(increment, 41) as i32 "unexpected diagnostic: {error}" ); - let mut premature_roots = mir; - 'functions: for function in &mut premature_roots.fns { + let mut invalid_roots = mir; + 'functions: for function in &mut invalid_roots.fns { for block in &mut function.blocks { for statement in &mut block.stmts { if let Stmt::Let(_, Rvalue::FnAddr { signature, .. }) = statement { @@ -414,10 +418,10 @@ fn main() -> i32 = apply(increment, 41) as i32 } } } - let error = emit_llvm_ir(&premature_roots, BuildTarget::Baseline, false, &[], false) - .expect_err("function-value roots must remain deferred until L2b-b"); + let error = emit_llvm_ir(&invalid_roots, BuildTarget::Baseline, false, &[], false) + .expect_err("a non-borrowing function-value return cannot carry roots"); assert!( - error.contains("function-value provenance lands in L2b-b"), + error.contains("cannot borrow"), "unexpected diagnostic: {error}" ); } diff --git a/crates/align_driver/tests/move_return_cleanup.rs b/crates/align_driver/tests/move_return_cleanup.rs new file mode 100644 index 00000000..b1b83813 --- /dev/null +++ b/crates/align_driver/tests/move_return_cleanup.rs @@ -0,0 +1,119 @@ +//! L2c owner: recursively Move returns carry one path-selected cleanup bit through every call ABI. + +mod common; +use common::*; + +const SOURCE: &str = r#" +fn owned(mode: i32) -> Option { + if mode == 0 { return None } + return Some("owned".clone()) +} + +fn fallible(mode: i32) -> Result { + if mode == 0 { return Ok("ok".clone()) } + return Err("err".clone()) +} + +fn invoke(handler: fn(i32) -> Result, mode: i32) -> Result = handler(mode) + +fn relay(mode: i32) -> Result { + value := fallible(mode)? + return Ok(value) +} + +fn keep_error(message: string) -> string = message +fn mapped(mode: i32) -> Result = fallible(mode).map_err(keep_error) +fn copy() -> i32 = 7 + +fn option_len(value: Option) -> i32 = match value { + Some(text) => text.len() as i32 + None => 0 +} + +fn result_len(value: Result) -> i32 = match value { + Ok(text) => text.len() as i32 + Err(text) => text.len() as i32 +} + +fn main() -> i32 = option_len(owned(1)) + + result_len(invoke(fallible, 0)) + + result_len(invoke(fallible, 1)) + + result_len(relay(0)) + + result_len(relay(1)) + + result_len(mapped(1)) + + copy() +"#; + +fn mir_text(source: &str) -> String { + let mut source_map = SourceMap::new(); + let checked = check(&mut source_map, "move-return-cleanup.align", source); + assert!( + !checked.diags.has_errors(), + "fixture must check:\n{}", + align_driver::format_diagnostics(&source_map, &checked.diags) + ); + align_mir::print::program_to_string(&lower_to_mir(&checked.hir)) +} + +#[test] +fn move_return_cleanup_is_explicit_in_direct_indirect_and_return_mir() { + let mir = mir_text(SOURCE); + assert!( + mir.contains("call_with_cleanup program owned") + && mir.contains("call_indirect_with_cleanup") + && mir.contains("return_with_cleanup"), + "Move-return value and cleanup bit must share every direct, indirect, and return edge:\n{mir}" + ); + assert!( + mir.contains("fn copy() -> i32 borrow=None region=None cleanup=None") + && mir.contains("call program copy()"), + "Copy returns must retain the value-only ABI:\n{mir}" + ); +} + +#[test] +fn move_return_cleanup_executes_none_some_try_and_map_err_paths() { + if !backend_available() { + return; + } + assert_eq!(build_and_run("move-return-cleanup", SOURCE).status.code(), Some(25)); +} + +#[test] +fn imported_move_return_cleanup_matches_whole_program_and_per_unit_abi() { + if !backend_available() { + return; + } + let files = &[ + ( + "values.align", + r#" +module values +pub fn owned(flag: bool) -> Option = + if flag { Some("cross".clone()) } else { None } +pub fn copy() -> i32 = 4 +"#, + ), + ( + "main.align", + r#" +import values +fn length(value: Option) -> i32 = match value { + Some(text) => text.len() as i32 + None => 0 +} +fn main() -> i32 = length(values.owned(true)) + length(values.owned(false)) + values.copy() +"#, + ), + ]; + let whole = build_and_run_multi("move-return-import-whole", files, "main.align"); + let per_unit = build_per_unit_multi("move-return-import-per-unit", files, "main.align"); + assert_eq!(whole.status.code(), Some(9)); + assert_eq!(per_unit.link_and_run().status.code(), Some(9)); + let main_mir = &per_unit.unit("main").mir; + assert!( + align_mir::print::program_to_string(main_mir) + .contains("call_with_cleanup program values$owned"), + "the importing unit must consume the producer's DynamicBit ABI" + ); +} diff --git a/crates/align_driver/tests/return_provenance.rs b/crates/align_driver/tests/return_provenance.rs index 8e708d7a..2657182c 100644 --- a/crates/align_driver/tests/return_provenance.rs +++ b/crates/align_driver/tests/return_provenance.rs @@ -1,5 +1,4 @@ -//! L2b-a1 gate: return-borrow/region summaries retain caller-relative parameter roots across -//! named, direct, recursive, and imported call paths while aggregates stay conservatively flat. +//! L2b return-provenance gate across direct/imported calls, projections, and function values. mod common; use common::*; @@ -22,6 +21,7 @@ fn direct_return_summaries_cover_scalar_recursion_and_flattened_aggregates() { module views pub BoxedView { value: str } pub Choice { First(str), Second(str) } +pub ViewError { Text(str), Fixed } pub fn second(first: str, second: str) -> str = second pub fn boxed(value: str, ignored: str) -> BoxedView = BoxedView { value: value } @@ -47,6 +47,29 @@ pub fn choose(first: str, second: str, take_first: bool) -> str { Second(_) => \"fixed\" } } +pub fn keep_view_error(value: ViewError) -> ViewError = value +pub fn fixed_view_error(_: ViewError) -> ViewError = ViewError.Fixed +pub fn map_ok(value: str, ignored: str) -> Result { + result: Result := Ok(value) + return result.map_err(keep_view_error) +} +pub fn map_error(value: str, ignored: str) -> Result { + result: Result := Err(ViewError.Text(value)) + return result.map_err(keep_view_error) +} +pub fn map_fixed_error(value: str) -> Result { + result: Result := Err(ViewError.Text(value)) + return result.map_err(fixed_view_error) +} +pub fn map_captured_error(value: str) -> Result { + result: Result := Err(ViewError.Fixed) + mapper := fn _: ViewError { ViewError.Text(value) } + return result.map_err(mapper) +} +pub fn map_unresolved( + result: Result, + mapper: fn(ViewError) -> ViewError, +) -> Result = result.map_err(mapper) ", ), ("main.align", "import views\nfn main() -> i32 = 0\n"), @@ -83,8 +106,8 @@ pub fn choose(first: str, second: str, take_first: bool) -> str { assert_eq!(find("loop_identity").return_borrow, roots(&[0], &[])); assert_eq!( find("propagate").return_borrow, - roots(&[0, 1], &[]), - "L2b-a1 conservatively flattens the implicit Err edge and continuing Ok value" + roots(&[1], &[]), + "the implicit Err edge owns its payload; only the continuing fallback view borrows" ); assert_eq!( find("consume_try_success").return_borrow, @@ -93,8 +116,20 @@ pub fn choose(first: str, second: str, take_first: bool) -> str { ); assert_eq!( find("choose").return_borrow, + roots(&[0], &[]), + "the selected First payload must not retain the inactive Second sibling" + ); + assert_eq!(find("map_ok").return_borrow, roots(&[0], &[])); + assert_eq!(find("map_error").return_borrow, roots(&[0], &[])); + assert_eq!( + find("map_fixed_error").return_borrow, + ReturnBorrowSummary::None + ); + assert_eq!(find("map_captured_error").return_borrow, roots(&[0], &[])); + assert_eq!( + find("map_unresolved").return_borrow, roots(&[0, 1], &[]), - "L2b-a1 deliberately retains the flattened sum-payload union" + "an unresolved mapper must retain both its compatible Result input and environment" ); assert_eq!( find("second").return_region, @@ -363,8 +398,8 @@ pub fn deferred_pipeline_projection(first: str, second: str) -> str { ] { assert_eq!( find(name).return_borrow, - roots(&[0, 1], &[]), - "{name} must retain both roots until array/pipeline projection lands" + roots(&[0], &[]), + "{name} must retain only the selected first parameter" ); } } @@ -1909,7 +1944,7 @@ fn main() -> i32 { } #[test] -fn lifted_closure_capture_roots_remain_deferred_to_l2b_b() { +fn lifted_closure_capture_roots_drive_indirect_results() { if !backend_available() { return; } @@ -1927,7 +1962,58 @@ fn main() -> i32 { } #[test] -fn named_function_value_summaries_remain_deferred_to_l2b_b() { +fn captured_indirect_results_resolve_to_outer_parameters() { + let files = &[ + ( + "views.align", + "\ +module views +pub Holder { callback: fn() -> str } +pub fn captured(value: str) -> str { + callback := fn { value } + return callback() +} +pub fn joined(left: str, right: str, choose: bool) -> str { + mut callback := fn { left } + if choose { callback = fn { right } } + return callback() +} +pub fn stored(value: str) -> str { + holder := Holder { callback: fn { value } } + return holder.callback() +} +", + ), + ("main.align", "import views\nfn main() -> i32 = 0\n"), + ]; + let checked = assert_same_verdict( + "l2b-captured-result-outer-summary", + files, + "main.align", + ); + assert!( + !checked.diags.has_errors(), + "a captured caller-owned parameter may flow through an indirect result" + ); + let summary = checked + .summaries + .iter() + .find(|summary| summary.unit == "views") + .expect("views summary"); + let find = |name: &str| { + summary + .fns + .iter() + .find(|function| function.name.as_str() == name) + .unwrap_or_else(|| panic!("{name} signature")) + }; + assert_eq!(find("captured").return_borrow, roots(&[0], &[])); + assert_eq!(find("joined").return_borrow, roots(&[0, 1], &[])); + assert_eq!(find("stored").return_borrow, roots(&[0], &[])); +} + +#[test] +fn named_function_value_summaries_drive_indirect_results() { if !backend_available() { return; } @@ -1944,3 +2030,109 @@ fn main() -> i32 { let output = build_and_run("l2b-a1-named-fn-value", src); assert_eq!(output.status.code(), Some(14)); } + +#[test] +fn named_function_value_result_keeps_the_selected_owner_live() { + let files = &[( + "main.align", + "\ +fn identity(value: str) -> str = value +fn consume(value: string) -> i64 = value.len() +fn main() -> i32 { + owned := \"function value\".clone() + view: str := owned + f := identity + result := f(view) + consume(owned) + return result.len() as i32 +} +", + )]; + let checked = assert_same_verdict("l2b-named-fn-value-owner", files, "main.align"); + assert!( + checked.diags.has_errors(), + "an indirect identity result must keep its selected argument owner live" + ); +} + +#[test] +fn closure_target_joins_keep_capture_slots_target_relative() { + if !backend_available() { + return; + } + let src = "\ +fn consume(value: string) -> i64 = value.len() +fn main(args: array) -> Result<(), Error> { + left_owner := \"left\".clone() + ignored_owner := \"ignored\".clone() + right_owner := \"right hand\".clone() + left: str := left_owner + ignored: str := ignored_owner + right: str := right_owner + mut f := fn { left } + if args.len() > 1 { + f = fn { ignored.len(); right } + } + result := f() + consume(ignored_owner) + print(result.len()) + return Ok(()) +} +"; + let left = build_and_run_args("l2b-target-relative-left", src, &[]); + assert_eq!(left.status.code(), Some(0)); + assert_eq!(String::from_utf8_lossy(&left.stdout), "4\n"); + let right = build_and_run_args("l2b-target-relative-right", src, &["right"]); + assert_eq!(right.status.code(), Some(0)); + assert_eq!(String::from_utf8_lossy(&right.stdout), "10\n"); +} + +#[test] +fn closure_target_join_keeps_every_selected_owner_live() { + let files = &[( + "main.align", + "\ +fn consume(value: string) -> i64 = value.len() +fn main(args: array) -> i32 { + left_owner := \"left\".clone() + right_owner := \"right\".clone() + left: str := left_owner + right: str := right_owner + mut f := fn { left } + if args.len() > 1 { f = fn { right } } + result := f() + consume(left_owner) + return result.len() as i32 +} +", + )]; + let checked = assert_same_verdict("l2b-closure-target-owner", files, "main.align"); + assert!( + checked.diags.has_errors(), + "a joined closure result must keep every runtime-selectable capture owner live" + ); +} + +#[test] +fn closure_capture_roots_survive_struct_storage_and_projection() { + let files = &[( + "main.align", + "\ +Holder { callback: fn() -> str } +fn consume(value: string) -> i64 = value.len() +fn main() -> i32 { + owned := \"stored closure\".clone() + view: str := owned + holder := Holder { callback: fn { view } } + result := holder.callback() + consume(owned) + return result.len() as i32 +} +", + )]; + let checked = assert_same_verdict("l2b-closure-field-owner", files, "main.align"); + assert!( + checked.diags.has_errors(), + "a closure projected from a struct field must retain its captured owner" + ); +} diff --git a/crates/align_interface/src/codec.rs b/crates/align_interface/src/codec.rs index 84f48446..95722a45 100644 --- a/crates/align_interface/src/codec.rs +++ b/crates/align_interface/src/codec.rs @@ -18,7 +18,7 @@ use crate::{ /// The interface-artifact format version. Bump on ANY encoding change; a bump invalidates every /// cached summary (an old version fails closed on read) and changes `interface_hash` (the version is /// part of the hashed surface). -pub const FORMAT_VERSION: u32 = 2; +pub const FORMAT_VERSION: u32 = 3; /// Narrow a length to the format's `u32` length-prefix width, or panic loudly. This is /// producer-side, compiler-internal data (interface surfaces built from the compiler's own source @@ -99,12 +99,14 @@ fn write_type(w: &mut Writer, t: &IType) { ret, return_borrow, return_region, + return_cleanup, } => { w.u8(2); w.seq(params, write_param); write_type(w, ret); write_return_borrow(w, return_borrow); write_return_region(w, return_region); + write_return_cleanup(w, *return_cleanup); } } } @@ -159,6 +161,13 @@ fn write_effect(w: &mut Writer, e: Effect) { }); } +fn write_return_cleanup(w: &mut Writer, value: align_sema::hir::ReturnCleanupAbi) { + w.u8(match value { + align_sema::hir::ReturnCleanupAbi::None => 0, + align_sema::hir::ReturnCleanupAbi::DynamicBit => 1, + }); +} + fn write_fn(w: &mut Writer, f: &IFnSig) { w.str(&f.name); write_type_params(w, &f.type_params); @@ -166,6 +175,7 @@ fn write_fn(w: &mut Writer, f: &IFnSig) { write_type(w, &f.ret); write_return_borrow(w, &f.return_borrow); write_return_region(w, &f.return_region); + write_return_cleanup(w, f.return_cleanup); write_effect(w, f.effect); w.opt_str(&f.generic_body); } @@ -357,7 +367,8 @@ fn read_type(r: &mut Reader<'_>) -> Result { let ret = Box::new(read_type(r)?); let return_borrow = read_return_borrow(r, params.len())?; let return_region = read_return_region(r, params.len())?; - Ok(IType::Fn { params, ret, return_borrow, return_region }) + let return_cleanup = read_return_cleanup(r)?; + Ok(IType::Fn { params, ret, return_borrow, return_region, return_cleanup }) } tag => Err(DecodeError::BadTag { what: "type", tag }), } @@ -453,6 +464,16 @@ fn read_effect(r: &mut Reader<'_>) -> Result { } } +fn read_return_cleanup( + r: &mut Reader<'_>, +) -> Result { + match r.u8()? { + 0 => Ok(align_sema::hir::ReturnCleanupAbi::None), + 1 => Ok(align_sema::hir::ReturnCleanupAbi::DynamicBit), + tag => Err(DecodeError::BadTag { what: "return cleanup ABI", tag }), + } +} + fn read_fn(r: &mut Reader<'_>) -> Result { let name = r.str()?; let type_params = read_type_params(r)?; @@ -460,6 +481,7 @@ fn read_fn(r: &mut Reader<'_>) -> Result { let ret = read_type(r)?; let return_borrow = read_return_borrow(r, params.len())?; let return_region = read_return_region(r, params.len())?; + let return_cleanup = read_return_cleanup(r)?; let effect = read_effect(r)?; let generic_body = r.opt_str()?; Ok(IFnSig { @@ -469,6 +491,7 @@ fn read_fn(r: &mut Reader<'_>) -> Result { ret, return_borrow, return_region, + return_cleanup, effect, generic_body, }) diff --git a/crates/align_interface/src/lib.rs b/crates/align_interface/src/lib.rs index 78c6cb92..77328506 100644 --- a/crates/align_interface/src/lib.rs +++ b/crates/align_interface/src/lib.rs @@ -95,6 +95,7 @@ pub enum IType { ret: Box, return_borrow: ReturnBorrowSummary, return_region: ReturnRegionSummary, + return_cleanup: align_sema::hir::ReturnCleanupAbi, }, } @@ -124,6 +125,7 @@ pub struct IFnSig { pub ret: IType, pub return_borrow: ReturnBorrowSummary, pub return_region: ReturnRegionSummary, + pub return_cleanup: align_sema::hir::ReturnCleanupAbi, /// The 3-valued effect bit (part of the interface — flipping Pure→Impure is an interface change). pub effect: Effect, /// For a generic `pub` template: the declaration's source text (the body is part of the @@ -231,6 +233,7 @@ fn convert_type(t: &align_ast::Type) -> IType { ret: Box::new(convert_type(ret)), return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: align_sema::hir::ReturnCleanupAbi::None, }, } } @@ -247,6 +250,72 @@ fn convert_ret(ret: &Option) -> IType { } } +fn apply_function_cleanup_metadata( + interface: &mut IType, + resolved: align_sema::Ty, + program: &align_sema::hir::Program, +) { + match (interface, resolved) { + ( + IType::Fn { + params, + ret, + return_cleanup, + .. + }, + align_sema::Ty::Fn(id), + ) => { + let Some(definition) = program.fn_types.get(id as usize) else { + return; + }; + *return_cleanup = definition.return_cleanup; + for (parameter, &(_, scalar)) in params.iter_mut().zip(&definition.params) { + apply_function_cleanup_metadata( + &mut parameter.ty, + align_sema::scalar_to_ty(scalar), + program, + ); + } + apply_function_cleanup_metadata(ret, definition.ret, program); + } + (IType::Tuple(elements), align_sema::Ty::Tuple(id)) => { + if let Some(definition) = program.tuples.get(id as usize) { + for (element, &scalar) in elements.iter_mut().zip(&definition.elems) { + apply_function_cleanup_metadata( + element, + align_sema::scalar_to_ty(scalar), + program, + ); + } + } + } + (IType::Named { args, .. }, resolved) => { + let actuals: Vec = match resolved { + align_sema::Ty::Option(value) + | align_sema::Ty::Box(value) + | align_sema::Ty::Slice(value) + | align_sema::Ty::DynArray(value) + | align_sema::Ty::ArrayBuilder(value) + | align_sema::Ty::Task(value) + | align_sema::Ty::Array(value, _) + | align_sema::Ty::Vec(value, _) + | align_sema::Ty::Mask(value, _) => { + vec![align_sema::scalar_to_ty(value)] + } + align_sema::Ty::Result(ok, err) => vec![ + align_sema::scalar_to_ty(ok), + align_sema::scalar_to_ty(err), + ], + _ => Vec::new(), + }; + for (argument, actual) in args.iter_mut().zip(actuals) { + apply_function_cleanup_metadata(argument, actual, program); + } + } + _ => {} + } +} + fn convert_type_params(tps: &[align_ast::TypeParam]) -> Vec { tps.iter() .map(|tp| ITypeParam { name: tp.name.name.clone(), bound: tp.bound.as_ref().map(|b| b.name.clone()) }) @@ -302,19 +371,22 @@ pub fn build_summaries_with_effects( .into_iter() .map(|(k, v)| (k, v.into())) .collect(); - let return_provenance: HashMap<&str, (&ReturnBorrowSummary, &ReturnRegionSummary)> = program + let return_provenance: HashMap< + &str, + (&ReturnBorrowSummary, &ReturnRegionSummary, align_sema::hir::ReturnCleanupAbi), + > = program .fns .iter() .map(|function| { ( function.name.as_str(), - (&function.return_borrow, &function.return_region), + (&function.return_borrow, &function.return_region, function.return_cleanup), ) }) .chain(program.imported_fns.iter().map(|function| { ( function.name.as_str(), - (&function.return_borrow, &function.return_region), + (&function.return_borrow, &function.return_region, function.return_cleanup), ) })) .collect(); @@ -348,28 +420,56 @@ pub fn build_summaries_with_effects( effects.get(&canonical).copied().unwrap_or(Effect::Impure) }; let canonical = mangle(&m.path, m.is_entry, &fd.name.name); - let (return_borrow, return_region) = if is_generic { - (ReturnBorrowSummary::None, ReturnRegionSummary::None) + let (return_borrow, return_region, return_cleanup) = if is_generic { + ( + ReturnBorrowSummary::None, + ReturnRegionSummary::None, + align_sema::hir::ReturnCleanupAbi::None, + ) } else { return_provenance .get(canonical.as_str()) - .map(|(borrow, region)| ((*borrow).clone(), (*region).clone())) - .unwrap_or((ReturnBorrowSummary::None, ReturnRegionSummary::None)) + .map(|(borrow, region, cleanup)| { + ((*borrow).clone(), (*region).clone(), *cleanup) + }) + .unwrap_or(( + ReturnBorrowSummary::None, + ReturnRegionSummary::None, + align_sema::hir::ReturnCleanupAbi::None, + )) }; + let mut params = fd + .params + .iter() + .map(|parameter| IParam { + mode: parameter.mode, + ty: convert_type(¶meter.ty), + }) + .collect::>(); + let mut ret = convert_ret(&fd.ret); + if !is_generic + && let Some(function) = + program.fns.iter().find(|function| function.name == canonical) + { + for (parameter, local) in params.iter_mut().zip(&function.params) { + if let Some(local) = function.locals.get(*local as usize) { + apply_function_cleanup_metadata( + &mut parameter.ty, + local.ty, + program, + ); + } + } + apply_function_cleanup_metadata(&mut ret, function.ret, program); + } fns.push(IFnSig { name: fd.name.name.clone(), type_params: convert_type_params(&fd.type_params), - params: fd - .params - .iter() - .map(|p| IParam { - mode: p.mode, - ty: convert_type(&p.ty), - }) - .collect(), - ret: convert_ret(&fd.ret), + params, + ret, return_borrow, return_region, + return_cleanup, effect, generic_body: is_generic.then(|| safe_slice(src, fd.span)), }); @@ -379,14 +479,33 @@ pub fn build_summaries_with_effects( align_ast::Item::Struct(sd) => { if is_pub(sd.vis) { let is_generic = !sd.type_params.is_empty(); + let mut fields = sd + .fields + .iter() + .map(|f| (f.name.name.clone(), convert_type(&f.ty))) + .collect::>(); + if !is_generic { + let canonical = mangle(&m.path, m.is_entry, &sd.name.name); + if let Some(definition) = program + .structs + .iter() + .find(|definition| definition.source_name == canonical) + { + for ((_, interface), resolved) in + fields.iter_mut().zip(&definition.fields) + { + apply_function_cleanup_metadata( + interface, + resolved.ty, + program, + ); + } + } + } structs.push(IStructDef { name: sd.name.name.clone(), type_params: convert_type_params(&sd.type_params), - fields: sd - .fields - .iter() - .map(|f| (f.name.name.clone(), convert_type(&f.ty))) - .collect(), + fields, align: sd.align, c_repr: sd.c_repr, generic_body: is_generic.then(|| safe_slice(src, sd.span)), @@ -397,16 +516,42 @@ pub fn build_summaries_with_effects( align_ast::Item::Enum(ed) => { if is_pub(ed.vis) { let is_generic = !ed.type_params.is_empty(); + let mut variants = ed + .variants + .iter() + .map(|v| { + ( + v.name.name.clone(), + v.payload.iter().map(convert_type).collect::>(), + ) + }) + .collect::>(); + if !is_generic { + let canonical = mangle(&m.path, m.is_entry, &ed.name.name); + if let Some(definition) = program + .enums + .iter() + .find(|definition| definition.source_name == canonical) + { + for ((_, interface_payload), resolved_variant) in + variants.iter_mut().zip(&definition.variants) + { + for (interface, &resolved) in + interface_payload.iter_mut().zip(&resolved_variant.payload) + { + apply_function_cleanup_metadata( + interface, + align_sema::scalar_to_ty(resolved), + program, + ); + } + } + } + } enums.push(IEnumDef { name: ed.name.name.clone(), type_params: convert_type_params(&ed.type_params), - variants: ed - .variants - .iter() - .map(|v| { - (v.name.name.clone(), v.payload.iter().map(convert_type).collect()) - }) - .collect(), + variants, generic_body: is_generic.then(|| safe_slice(src, ed.span)), }); } @@ -690,8 +835,8 @@ fn render_enum(e: &IEnumDef) -> String { out } -/// A decoded interface may contain parameter-mode tags reserved for later compiler slices. L2b -/// consumes canonical return summaries, while `Borrow`/`BorrowMut` remain disabled until L2d/L2e. +/// Semantic compatibility failures found after the canonical interface codec has decoded a +/// structurally valid summary. #[derive(Clone, Debug, PartialEq, Eq)] pub enum ImportCompatibilityError { ReservedLocalType(String), @@ -708,13 +853,14 @@ pub enum ImportCompatibilityError { GenericCLayoutUnsupported(String), GenericBodySyntax(String), GenericBodyMismatch(String), - UnsupportedParamMode(ParamMode), + BorrowParamNotMove, ReturnSummaryOnNonBorrowingType, ReturnSummaryRootCannotBorrow(u32), ReturnSummaryCaptureRoot, ReturnSummaryDisagreement, ReturnSummaryOnUnsupportedSignature, ReturnSummaryGenerativeCapabilityGraph, + ReturnCleanupMismatch, } impl std::fmt::Display for ImportCompatibilityError { @@ -769,8 +915,8 @@ impl std::fmt::Display for ImportCompatibilityError { "generic interface declaration `{name}` disagrees with its structured record" ) } - ImportCompatibilityError::UnsupportedParamMode(mode) => { - write!(f, "interface parameter mode {mode:?} is not supported by this compiler slice") + ImportCompatibilityError::BorrowParamNotMove => { + write!(f, "interface borrowed parameter does not have a provably Move type") } ImportCompatibilityError::ReturnSummaryOnNonBorrowingType => { write!( @@ -808,6 +954,9 @@ impl std::fmt::Display for ImportCompatibilityError { "return provenance capability validation found a generative recursive type graph" ) } + ImportCompatibilityError::ReturnCleanupMismatch => { + write!(f, "interface return-cleanup metadata disagrees with its return type") + } } } } @@ -991,9 +1140,35 @@ impl BorrowFacts { struct CapabilityAnalysis<'a> { index: LocalDefinitionIndex<'a>, borrow: Vec, + ownership: Vec, growth: Vec>, } +#[derive(Clone, PartialEq, Eq)] +struct OwnershipFacts { + intrinsic: bool, + unknown: bool, + params: Vec, +} + +impl OwnershipFacts { + fn empty(param_count: usize) -> Self { + Self { + intrinsic: false, + unknown: false, + params: vec![false; param_count], + } + } + + fn union(&mut self, other: &Self) { + self.intrinsic |= other.intrinsic; + self.unknown |= other.unknown; + for (current, incoming) in self.params.iter_mut().zip(&other.params) { + *current |= *incoming; + } + } +} + impl<'a> CapabilityAnalysis<'a> { fn new(index: LocalDefinitionIndex<'a>) -> Result { let borrow = index @@ -1006,17 +1181,126 @@ impl<'a> CapabilityAnalysis<'a> { .iter() .map(|definition| vec![true; definition.type_params().len()]) .collect(); + let ownership = index + .definitions + .iter() + .map(|definition| OwnershipFacts::empty(definition.type_params().len())) + .collect(); let mut analysis = Self { index, borrow, + ownership, growth, }; analysis.solve_borrow(); + analysis.solve_ownership(); analysis.solve_growth(); analysis.reject_generative_cycles()?; Ok(analysis) } + fn eval_ownership( + &self, + ty: &IType, + type_params: &[ITypeParam], + summaries: &[OwnershipFacts], + ) -> OwnershipFacts { + let mut result = OwnershipFacts::empty(type_params.len()); + let mut work = vec![ty]; + while let Some(current) = work.pop() { + match current { + IType::Tuple(elements) => work.extend(elements), + IType::Fn { .. } => {} + IType::Named { path, args } => { + if args.is_empty() + && let Some(index) = type_params + .iter() + .position(|parameter| parameter.name == *path) + { + result.params[index] = true; + continue; + } + match path.as_str() { + "Option" | "Result" => { + work.extend(args); + continue; + } + "array" | "array_builder" | "string" | "reader" | "writer" + | "buffer" | "file" | "regex" | "captures" | "tcp_conn" + | "tcp_listener" | "udp_socket" | "child" | "http_request_ctx" + | "response_builder" | "http_stream" => { + result.intrinsic = true; + continue; + } + _ => {} + } + if builtin_capability(path).is_some() { + continue; + } + if let Some(index) = self.index.local(path) { + let summary = &summaries[index]; + result.intrinsic |= summary.intrinsic; + result.unknown |= summary.unknown; + for (position, dependent) in summary.params.iter().copied().enumerate() { + if dependent + && let Some(argument) = args.get(position) + { + work.push(argument); + } + } + } else if path.contains('.') { + result.unknown = true; + } + } + } + } + result + } + + fn solve_ownership(&mut self) { + loop { + let mut changed = false; + for index in 0..self.index.definitions.len() { + let definition = self.index.definitions[index]; + let mut next = OwnershipFacts::empty(definition.type_params().len()); + for value in definition.values() { + next.union(&self.eval_ownership( + value, + definition.type_params(), + &self.ownership, + )); + } + if next != self.ownership[index] { + self.ownership[index] = next; + changed = true; + } + } + if !changed { + break; + } + } + } + + fn return_cleanup(&self, ty: &IType, type_params: &[ITypeParam]) -> Option { + let facts = self.eval_ownership(ty, type_params, &self.ownership); + if facts.unknown || facts.params.iter().any(|dependent| *dependent) { + None + } else if facts.intrinsic { + Some(align_sema::hir::ReturnCleanupAbi::DynamicBit) + } else { + Some(align_sema::hir::ReturnCleanupAbi::None) + } + } + + fn is_move(&self, ty: &IType, type_params: &[ITypeParam]) -> Option { + let facts = self.eval_ownership(ty, type_params, &self.ownership); + if facts.unknown { + None + } else { + Some(facts.intrinsic || facts.params.iter().any(|dependent| *dependent)) + } + } + fn eval_borrow( &self, ty: &IType, @@ -1413,13 +1697,6 @@ fn validate_import_shapes( Ok(()) } -fn validate_import_param_mode(param: &IParam) -> Result<(), ImportCompatibilityError> { - if matches!(param.mode, ParamMode::Borrow | ParamMode::BorrowMut) { - return Err(ImportCompatibilityError::UnsupportedParamMode(param.mode)); - } - Ok(()) -} - fn validate_import_summary_header( borrow: &ReturnBorrowSummary, region: &ReturnRegionSummary, @@ -1462,10 +1739,8 @@ fn validate_import_type_headers(ty: &IType) -> Result<(), ImportCompatibilityErr ret, return_borrow, return_region, + return_cleanup: _, } => { - for param in params { - validate_import_param_mode(param)?; - } validate_import_summary_header(return_borrow, return_region, false)?; work.push(ret); work.extend(params.iter().rev().map(|param| ¶m.ty)); @@ -1480,7 +1755,6 @@ fn validate_import_headers( ) -> Result<(), ImportCompatibilityError> { for function in &summary.fns { for param in &function.params { - validate_import_param_mode(param)?; validate_import_type_headers(¶m.ty)?; } validate_import_type_headers(&function.ret)?; @@ -1720,7 +1994,7 @@ fn validate_import_summaries( return Err(ImportCompatibilityError::ReturnSummaryCaptureRoot); } for &index in roots.0 { - let Some((_, &may_borrow)) = params + let Some((parameter, &may_borrow)) = params .get(index as usize) .zip(param_may_borrow.get(index as usize)) else { @@ -1728,7 +2002,9 @@ fn validate_import_summaries( index, )); }; - if !may_borrow { + if !may_borrow + && !matches!(parameter.mode, ParamMode::Borrow | ParamMode::BorrowMut) + { return Err(ImportCompatibilityError::ReturnSummaryRootCannotBorrow( index, )); @@ -1738,9 +2014,45 @@ fn validate_import_summaries( Ok(()) } -/// Validate that a decoded interface uses only the currently enabled semantic subset. Codec -/// validation has already proved canonical return summaries; this gate still rejects later borrow -/// parameter modes before reconstructing imported source. +fn validate_return_cleanup_metadata( + ty: &IType, + type_params: &[ITypeParam], + analysis: &CapabilityAnalysis<'_>, +) -> Result<(), ImportCompatibilityError> { + let mut work = vec![ty]; + while let Some(current) = work.pop() { + match current { + IType::Named { args, .. } => work.extend(args.iter().rev()), + IType::Tuple(elements) => work.extend(elements.iter().rev()), + IType::Fn { + params, + ret, + return_cleanup, + .. + } => { + for parameter in params { + if parameter.mode == ParamMode::Borrow + && analysis.is_move(¶meter.ty, type_params) != Some(true) + { + return Err(ImportCompatibilityError::BorrowParamNotMove); + } + } + if let Some(expected) = analysis.return_cleanup(ret, type_params) + && *return_cleanup != expected + { + return Err(ImportCompatibilityError::ReturnCleanupMismatch); + } + work.push(ret); + work.extend(params.iter().rev().map(|parameter| ¶meter.ty)); + } + } + } + Ok(()) +} + +/// Validate that a decoded interface uses the enabled semantic subset. Codec validation has +/// already proved canonical return summaries; this gate proves ownership-dependent mode facts +/// before reconstructing imported source. pub fn validate_for_import( summary: &InterfaceSummary, ) -> Result<(), ImportCompatibilityError> { @@ -1751,6 +2063,27 @@ pub fn validate_for_import( let analysis = CapabilityAnalysis::new(index)?; for function in &summary.fns { + for parameter in &function.params { + if parameter.mode == ParamMode::Borrow + && analysis.is_move(¶meter.ty, &function.type_params) != Some(true) + { + return Err(ImportCompatibilityError::BorrowParamNotMove); + } + } + if function.type_params.is_empty() + && let Some(expected) = analysis.return_cleanup(&function.ret, &[]) + && function.return_cleanup != expected + { + return Err(ImportCompatibilityError::ReturnCleanupMismatch); + } + for parameter in &function.params { + validate_return_cleanup_metadata( + ¶meter.ty, + &function.type_params, + &analysis, + )?; + } + validate_return_cleanup_metadata(&function.ret, &function.type_params, &analysis)?; validate_import_summaries( &function.params, &function.ret, @@ -1760,6 +2093,22 @@ pub fn validate_for_import( &function.type_params, )?; } + for structure in &summary.structs { + if structure.type_params.is_empty() { + for (_, field) in &structure.fields { + validate_return_cleanup_metadata(field, &[], &analysis)?; + } + } + } + for enumeration in &summary.enums { + if enumeration.type_params.is_empty() { + for (_, payload) in &enumeration.variants { + for ty in payload { + validate_return_cleanup_metadata(ty, &[], &analysis)?; + } + } + } + } Ok(()) } @@ -1873,6 +2222,7 @@ pub fn summary_return_provenance( ( function.return_borrow.clone(), function.return_region.clone(), + function.return_cleanup, ), ); } diff --git a/crates/align_interface/tests/summary.rs b/crates/align_interface/tests/summary.rs index 5a34eaab..56e979a0 100644 --- a/crates/align_interface/tests/summary.rs +++ b/crates/align_interface/tests/summary.rs @@ -749,6 +749,58 @@ fn parameter_mode_and_return_summaries_have_canonical_codec_identity() { ); } +#[test] +fn return_cleanup_metadata_is_exact_for_functions_and_nested_function_values() { + let mut summary = one( + "pub Route { owned: fn() -> string, copied: fn() -> i64 }\n\ + pub fn owned() -> string = \"owned\".clone()\n\ + fn main() -> i32 = 0\n", + ) + .remove(0); + let owned = summary.fns.iter().find(|function| function.name == "owned").unwrap(); + assert_eq!( + owned.return_cleanup, + align_sema::hir::ReturnCleanupAbi::DynamicBit + ); + let fields = &summary.structs.iter().find(|definition| definition.name == "Route").unwrap().fields; + assert!(matches!( + &fields[0].1, + IType::Fn { + return_cleanup: align_sema::hir::ReturnCleanupAbi::DynamicBit, + .. + } + )); + assert!(matches!( + &fields[1].1, + IType::Fn { + return_cleanup: align_sema::hir::ReturnCleanupAbi::None, + .. + } + )); + assert_eq!(validate_for_import(&summary), Ok(())); + + summary.fns.iter_mut().find(|function| function.name == "owned").unwrap().return_cleanup = + align_sema::hir::ReturnCleanupAbi::None; + assert_eq!( + validate_for_import(&summary), + Err(ImportCompatibilityError::ReturnCleanupMismatch) + ); + + let mut nested = one( + "pub Route { owned: fn() -> string, copied: fn() -> i64 }\n\ + fn main() -> i32 = 0\n", + ) + .remove(0); + let IType::Fn { return_cleanup, .. } = &mut nested.structs[0].fields[1].1 else { + panic!("expected copied function field"); + }; + *return_cleanup = align_sema::hir::ReturnCleanupAbi::DynamicBit; + assert_eq!( + validate_for_import(&nested), + Err(ImportCompatibilityError::ReturnCleanupMismatch) + ); +} + #[test] fn semantic_import_rejects_return_roots_incapable_of_borrowing() { let mut non_borrowing_return = @@ -961,6 +1013,7 @@ fn semantic_import_distinguishes_transformed_generic_cycle_instantiations() { let root = named("A", vec![named("i64", vec![])]); summary.fns[0].params[0].ty = root.clone(); summary.fns[0].ret = root; + summary.fns[0].return_cleanup = align_sema::hir::ReturnCleanupAbi::DynamicBit; assert_eq!( validate_for_import(&summary), Ok(()), @@ -999,6 +1052,7 @@ fn semantic_import_distinguishes_transformed_generic_cycle_instantiations() { ); finite.fns[0].params[0].ty = root.clone(); finite.fns[0].ret = root; + finite.fns[0].return_cleanup = align_sema::hir::ReturnCleanupAbi::None; assert_eq!( validate_for_import(&finite), Ok(()), @@ -1028,6 +1082,7 @@ fn semantic_import_distinguishes_transformed_generic_cycle_instantiations() { let root = named("FiniteConstant", vec![named("i64", vec![])]); finite_constant.fns[0].params[0].ty = root.clone(); finite_constant.fns[0].ret = root; + finite_constant.fns[0].return_cleanup = align_sema::hir::ReturnCleanupAbi::None; assert_eq!( validate_for_import(&finite_constant), Ok(()), @@ -1285,6 +1340,7 @@ fn semantic_import_growth_transport_distinguishes_exposure_and_convergence() { ret: Box::new(parameter("T")), return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: align_sema::hir::ReturnCleanupAbi::None, }, ), ] { @@ -1747,7 +1803,7 @@ fn semantic_import_type_shape_errors_are_exact_and_precede_headers() { Err(ImportCompatibilityError::UnresolvedBareType( "Missing".to_string() )), - "complete type shape precedes the later unsupported-mode header gate" + "complete type shape precedes ownership-dependent mode validation" ); let mut qualified_local = base.clone(); @@ -1786,6 +1842,7 @@ fn semantic_import_type_shape_errors_are_exact_and_precede_headers() { params: vec![0], captures: vec![], }, + return_cleanup: align_sema::hir::ReturnCleanupAbi::None, return_region: ReturnRegionSummary::None, }; assert_eq!( @@ -1968,6 +2025,7 @@ fn semantic_import_validates_nested_function_type_summaries() { params: vec![0], captures: vec![], }, + return_cleanup: align_sema::hir::ReturnCleanupAbi::None, }; assert_eq!( validate_for_import(&summary), @@ -2045,19 +2103,26 @@ fn semantic_import_rejects_generic_and_recursive_capability_summaries() { } #[test] -fn known_future_parameter_mode_round_trips_but_semantic_import_rejects() { - for mode in [ParamMode::Borrow, ParamMode::BorrowMut] { - let mut summary = - one("pub fn inspect(value: slice) -> i64 = value.len()\nfn main() -> i32 = 0\n").remove(0); - summary.fns[0].params[0].mode = mode; +fn borrowed_parameter_modes_round_trip_and_import() { + for source in [ + "pub fn inspect(borrow value: string) -> i64 = value.len()\nfn main() -> i32 = 0\n", + "pub fn increment(borrow mut value: i64) { value = value + 1 }\nfn main() -> i32 = 0\n", + ] { + let mut summary = one(source).remove(0); rehash(&mut summary); - let decoded = deserialize(&serialize(&summary)).expect("known future mode tag round-trips"); + let decoded = deserialize(&serialize(&summary)).expect("borrowed mode tag round-trips"); assert_eq!(decoded, summary); - assert_eq!( - validate_for_import(&decoded), - Err(ImportCompatibilityError::UnsupportedParamMode(mode)) - ); + assert_eq!(validate_for_import(&decoded), Ok(())); } + + let mut invalid = + one("pub fn inspect(value: slice) -> i64 = value.len()\nfn main() -> i32 = 0\n").remove(0); + invalid.fns[0].params[0].mode = ParamMode::Borrow; + rehash(&mut invalid); + assert_eq!( + validate_for_import(&invalid), + Err(ImportCompatibilityError::BorrowParamNotMove) + ); } #[test] @@ -2112,7 +2177,7 @@ fn parameter_mode_codec_has_a_byte_golden_and_rejects_unknown_tags() { let hex = surface.iter().map(|byte| format!("{byte:02x}")).collect::(); assert_eq!( hex, - "02000000040000006d61696e0100000007000000696e73706563740000000001000000010005000000736c6963650100000000030000006936340000000000030000006936340000000000000000000000000000000000000000" + "03000000040000006d61696e0100000007000000696e73706563740000000001000000010005000000736c696365010000000003000000693634000000000003000000693634000000000000000000000000000000000000000000" ); let mut artifact = serialize(&summary); @@ -2128,20 +2193,29 @@ fn parameter_mode_codec_has_a_byte_golden_and_rejects_unknown_tags() { Err(DecodeError::BadTag { what: "parameter mode", tag: 0xff }) ); - // This one-function surface ends with the function's borrow tag, region tag, effect, generic - // body option, then the three empty top-level type/const sequences (16 bytes total). + // This one-function surface ends with the function's borrow tag, region tag, cleanup ABI, + // effect, generic body option, then the empty top-level type/const sequences. let mut bad_borrow = serialize(&summary); - bad_borrow[surface.len() - 16] = 0xff; + bad_borrow[surface.len() - 17] = 0xff; assert_eq!( deserialize(&bad_borrow), Err(DecodeError::BadTag { what: "return-borrow summary", tag: 0xff }) ); let mut bad_region = serialize(&summary); - bad_region[surface.len() - 15] = 0xff; + bad_region[surface.len() - 16] = 0xff; assert_eq!( deserialize(&bad_region), Err(DecodeError::BadTag { what: "return-region summary", tag: 0xff }) ); + let mut bad_cleanup = serialize(&summary); + bad_cleanup[surface.len() - 15] = 0xff; + assert_eq!( + deserialize(&bad_cleanup), + Err(DecodeError::BadTag { + what: "return cleanup ABI", + tag: 0xff, + }) + ); } // ---- 5. capability set --------------------------------------------------------------------------- diff --git a/crates/align_mir/src/canonical_graph.rs b/crates/align_mir/src/canonical_graph.rs index 0214ddc4..a7fe104c 100644 --- a/crates/align_mir/src/canonical_graph.rs +++ b/crates/align_mir/src/canonical_graph.rs @@ -14,6 +14,7 @@ pub struct FunctionTypeDef { pub ret: Ty, pub return_borrow: hir::ReturnBorrowSummary, pub return_region: hir::ReturnRegionSummary, + pub return_cleanup: hir::ReturnCleanupAbi, } #[derive(Clone, Debug)] @@ -24,6 +25,7 @@ pub struct ProgramExtern { pub ret: Ty, pub return_borrow: hir::ReturnBorrowSummary, pub return_region: hir::ReturnRegionSummary, + pub return_cleanup: hir::ReturnCleanupAbi, } #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] @@ -176,6 +178,7 @@ impl SourceShapeView for CanonicalTypeView<'_> { ret: &definition.ret, return_borrow: &definition.return_borrow, return_region: &definition.return_region, + return_cleanup: definition.return_cleanup, }) } } @@ -359,14 +362,12 @@ impl<'a> GraphValidator<'a> { ret, return_borrow, return_region, + return_cleanup, } => { let count_ordinal = self.field_ordinal(); self.validate_count(params.len(), count_ordinal); - for &(mode, value) in params { - let mode_ordinal = self.field_ordinal(); - if !matches!(mode, ParamMode::ByValue | ParamMode::Out) { - self.candidate(mode_ordinal, CanonicalGraphError::InvalidGraph); - } + for &(_, value) in params { + self.field_ordinal(); self.scan_scalar(value, &mut references, None); } self.scan_ty(*ret, &mut references, None); @@ -375,6 +376,21 @@ impl<'a> GraphValidator<'a> { if !summaries_agree(return_borrow, return_region) { self.candidate(region_ordinal, CanonicalGraphError::InvalidSummary); } + let expected_cleanup = if align_sema::needs_drop_flag( + *ret, + self.view.structs, + self.view.tuples, + self.view.enums, + self.view.tagged_types, + ) { + hir::ReturnCleanupAbi::DynamicBit + } else { + hir::ReturnCleanupAbi::None + }; + let cleanup_ordinal = self.field_ordinal(); + if return_cleanup != expected_cleanup { + self.candidate(cleanup_ordinal, CanonicalGraphError::InvalidGraph); + } } } let end_ordinal = self.field_ordinal(); @@ -694,7 +710,7 @@ impl<'a> GraphValidator<'a> { let summary_ordinal = self.field_ordinal(); let count_ordinal = self.field_ordinal(); self.validate_count(roots.len(), count_ordinal); - if roots.is_empty() { + if roots.is_empty() && captures.is_empty() { self.candidate(count_ordinal, CanonicalGraphError::InvalidSummary); } let mut previous = None; @@ -707,11 +723,13 @@ impl<'a> GraphValidator<'a> { } let captures_count = self.field_ordinal(); self.validate_count(captures.len(), captures_count); - if !captures.is_empty() { - self.candidate(captures_count, CanonicalGraphError::InvalidSummary); - } - for _ in captures { - self.field_ordinal(); + let mut previous = None; + for &capture in captures { + let ordinal = self.field_ordinal(); + if previous.is_some_and(|value| value >= capture) { + self.candidate(ordinal, CanonicalGraphError::InvalidSummary); + } + previous = Some(capture); } summary_ordinal } @@ -908,22 +926,30 @@ impl CanonicalFnAbi { ret: Ty, borrow: &hir::ReturnBorrowSummary, region: &hir::ReturnRegionSummary, + cleanup: hir::ReturnCleanupAbi, program: &Program, ) -> Result { let count = checked_count(params.len())?; validate_function_summaries(borrow, region, params.len())?; - if params - .iter() - .any(|(mode, _)| !matches!(mode, ParamMode::ByValue | ParamMode::Out)) - { - return Err(CanonicalCodecError::InvalidGraph); - } - let mut canonical_params = Vec::with_capacity(params.len()); for &(mode, ty) in params { canonical_params.push((mode, CanonicalTy::from_program(ty, program)?)); } let canonical_ret = CanonicalTy::from_program(ret, program)?; + let expected_cleanup = if align_sema::needs_drop_flag( + ret, + &program.structs, + &program.tuples, + &program.enums, + &program.tagged_types, + ) { + hir::ReturnCleanupAbi::DynamicBit + } else { + hir::ReturnCleanupAbi::None + }; + if cleanup != expected_cleanup { + return Err(CanonicalCodecError::InvalidGraph); + } let mut out = Vec::new(); out.push(1); out.extend(count.to_le_bytes()); @@ -934,6 +960,7 @@ impl CanonicalFnAbi { out.extend(canonical_ret.as_bytes()); encode_borrow_summary(&mut out, borrow)?; encode_region_summary(&mut out, region)?; + encode_return_cleanup(&mut out, cleanup); Ok(Self(out.into_boxed_slice())) } @@ -967,9 +994,9 @@ fn validate_function_summaries( fn valid(roots: &[u32], captures: &[u32], params: usize) -> bool { u32::try_from(roots.len()).is_ok() && u32::try_from(captures.len()).is_ok() - && !roots.is_empty() - && captures.is_empty() + && (!roots.is_empty() || !captures.is_empty()) && roots.windows(2).all(|pair| pair[0] < pair[1]) + && captures.windows(2).all(|pair| pair[0] < pair[1]) && roots.iter().all(|&root| (root as usize) < params) } @@ -1261,6 +1288,7 @@ fn decode_node(cursor: &mut DecodeCursor<'_>) -> Result Err(CanonicalCodecError::UnknownTag), @@ -1608,15 +1636,13 @@ pub(super) fn canonical_fn_abi_record_len(bytes: &[u8]) -> Result, value: hir::ReturnCleanupAbi) { + out.push(match value { + hir::ReturnCleanupAbi::None => 0, + hir::ReturnCleanupAbi::DynamicBit => 1, + }); +} + +fn decode_return_cleanup( + cursor: &mut DecodeCursor<'_>, +) -> Result { + match cursor.byte()? { + 0 => Ok(hir::ReturnCleanupAbi::None), + 1 => Ok(hir::ReturnCleanupAbi::DynamicBit), + _ => Err(CanonicalCodecError::UnknownTag), + } +} + fn encode_borrow_summary( out: &mut Vec, value: &hir::ReturnBorrowSummary, @@ -2035,9 +2079,8 @@ fn encode_param_mode(out: &mut Vec, mode: ParamMode) -> Result<(), Canonical match mode { ParamMode::ByValue => out.push(0), ParamMode::Out => out.push(1), - ParamMode::Borrow | ParamMode::BorrowMut => { - return Err(CanonicalGraphError::InvalidGraph); - } + ParamMode::Borrow => out.push(2), + ParamMode::BorrowMut => out.push(3), } Ok(()) } @@ -2541,6 +2584,7 @@ mod tests { ret: definition.ret, return_borrow: definition.return_borrow.clone(), return_region: definition.return_region.clone(), + return_cleanup: definition.return_cleanup, }) .collect() } @@ -3153,10 +3197,11 @@ mod tests { Ty::Unit, &hir::ReturnBorrowSummary::None, &hir::ReturnRegionSummary::None, + hir::ReturnCleanupAbi::None, &program, ) .unwrap(); - assert_eq!(abi.as_bytes(), [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 56, 0, 0]); + assert_eq!(abi.as_bytes(), [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 56, 0, 0, 0]); assert_eq!(CanonicalFnAbi::decode(abi.as_bytes()).unwrap(), abi); let params = [(ParamMode::ByValue, Ty::Fn(0))]; @@ -3171,6 +3216,7 @@ mod tests { params: vec![0], captures: vec![], }, + hir::ReturnCleanupAbi::None, &program, ) .unwrap(); @@ -3236,39 +3282,39 @@ mod tests { error(&recursive, CanonicalCodecError::InvalidGraph); let duplicate_function = [ - 1, 2, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 52, 0, 0, 0, 0, + 1, 2, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 0, 52, 0, 0, 0, 0, ]; error(&duplicate_function, CanonicalCodecError::DuplicateMember); - let unreachable_function = [1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 56]; + let unreachable_function = [1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 0, 56]; error( &unreachable_function, CanonicalCodecError::NonCanonicalOrder, ); let invalid_summary = [ - 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 56, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, + 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 56, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, ]; error(&invalid_summary, CanonicalCodecError::InvalidSummary); let unit = [1, 0, 0, 0, 0, 56]; - let mut invalid_mode = vec![1, 1, 0, 0, 0, 2]; + let mut invalid_mode = vec![1, 1, 0, 0, 0, 4]; invalid_mode.extend(unit); invalid_mode.extend(unit); invalid_mode.extend([0, 0]); assert_eq!( CanonicalFnAbi::decode(&invalid_mode), - Err(CanonicalCodecError::InvalidGraph) + Err(CanonicalCodecError::UnknownTag) ); - let mut invalid_mode_then_truncated = vec![1, 1, 0, 0, 0, 2]; + let mut invalid_mode_then_truncated = vec![1, 1, 0, 0, 0, 4]; invalid_mode_then_truncated.extend(unit); assert_eq!( CanonicalFnAbi::decode(&invalid_mode_then_truncated), - Err(CanonicalCodecError::InvalidGraph) + Err(CanonicalCodecError::UnknownTag) ); let mut abi_trailing = vec![1, 0, 0, 0, 0]; abi_trailing.extend(unit); - abi_trailing.extend([0, 0, 0xff]); + abi_trailing.extend([0, 0, 0, 0xff]); assert_eq!( CanonicalFnAbi::decode(&abi_trailing), Err(CanonicalCodecError::TrailingBytes) @@ -3624,13 +3670,10 @@ mod tests { for value in [Ty::Param(0), Ty::IntVar(0), Ty::FloatVar(0), Ty::Error] { error!(encoded_ty(value), CanonicalGraphError::InvalidGraph); } - for mode in [ParamMode::Borrow, ParamMode::BorrowMut] { - error!( - encode_param_mode(&mut out, mode), - CanonicalGraphError::InvalidGraph - ); - assert_eq!(out, [0xa5, 0x5a]); - } + let mut modes = Vec::new(); + encode_param_mode(&mut modes, ParamMode::Borrow).unwrap(); + encode_param_mode(&mut modes, ParamMode::BorrowMut).unwrap(); + assert_eq!(modes, [2, 3]); error!( prim(&mut out, PrimScalar::Int(i(24))), CanonicalGraphError::InvalidWidth @@ -3669,6 +3712,7 @@ mod tests { ret: Ty::Unit, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }; assert_eq!(definition.params, [(ParamMode::Out, Scalar::Bool)]); assert_eq!(definition.ret, Ty::Unit); diff --git a/crates/align_mir/src/generated_id.rs b/crates/align_mir/src/generated_id.rs index 175f1f11..6f9886d4 100644 --- a/crates/align_mir/src/generated_id.rs +++ b/crates/align_mir/src/generated_id.rs @@ -546,8 +546,8 @@ mod tests { let bool_ty = ty("010000000002"); let i64_ty = ty("0100000000000140"); let slice_i64 = ty("01000000000d000140"); - let empty_abi = abi("01000000000100000000380000"); - let i64_abi = abi("010100000000010000000000014001000000000001400000"); + let empty_abi = abi("0100000000010000000038000000"); + let i64_abi = abi("01010000000001000000000001400100000000000140000000"); let goldens = [ ( @@ -555,7 +555,7 @@ mod tests { target: call("f"), signature: empty_abi.clone(), }, - "0100010000006601000000000100000000380000", + "010001000000660100000000010000000038000000", ), ( GeneratedId::Closure { @@ -563,7 +563,7 @@ mod tests { explicit_signature: empty_abi.clone(), captures: vec![bool_ty.clone()], }, - "0101010000006c0100000000010000000038000001000000010000000002", + "0101010000006c010000000001000000003800000001000000010000000002", ), ( GeneratedId::Task { @@ -598,7 +598,7 @@ mod tests { work_weight: 1, }); let expected = hex( - "01030001000000000d000140010000000000014001000000000001400100000066010100000000010000000000014001000000000001400000000000000000000001", + "01030001000000000d00014001000000000001400100000000000140010000006601010000000001000000000001400100000000000140000000000000000000000001", ); assert_eq!(roundtrip(parallel.clone()), expected); assert_eq!(GeneratedId::decode(&expected).unwrap(), parallel); @@ -644,7 +644,7 @@ mod tests { terminal_input: i64_ty.clone(), terminal_output: i64_ty.clone(), terminal: call("terminal"), - terminal_abi: abi("01000000000100000000380000"), + terminal_abi: abi("0100000000010000000038000000"), terminal_captures: vec![bool_ty.clone()], stages: stages.clone(), work_weight: 4, @@ -702,7 +702,7 @@ mod tests { terminal_input: ty("010000000038"), terminal_output: ty("010000000038"), terminal: call("f"), - terminal_abi: abi("01000000000100000000380000"), + terminal_abi: abi("0100000000010000000038000000"), terminal_captures: vec![], stages: vec![], work_weight: 3, @@ -717,7 +717,7 @@ mod tests { fn deep_generated_identity_codec_is_stack_bounded() { let value = GeneratedId::Closure { lifted: call("deep"), - explicit_signature: abi("01000000000100000000380000"), + explicit_signature: abi("0100000000010000000038000000"), captures: vec![ty("010000000038"); 4096], }; let bytes = value.to_canonical_bytes().unwrap(); diff --git a/crates/align_mir/src/lib.rs b/crates/align_mir/src/lib.rs index 4870c82a..46569c9c 100644 --- a/crates/align_mir/src/lib.rs +++ b/crates/align_mir/src/lib.rs @@ -101,6 +101,7 @@ pub struct ImportedFn { pub ret: Ty, pub return_borrow: hir::ReturnBorrowSummary, pub return_region: hir::ReturnRegionSummary, + pub return_cleanup: hir::ReturnCleanupAbi, } #[derive(Clone, Debug)] @@ -143,11 +144,16 @@ pub struct Function { /// Source-level parameter modes. This is signature identity even while L2a lowers only the /// existing `ByValue` and `Out` physical ABI. pub param_modes: Vec, + /// Local proxy cleanup-bit slots for whole-Move `BorrowMut` parameters, parallel to `params`. + /// Codegen loads each proxy from the caller's hidden cleanup pointer and writes it back on + /// every return; other parameter modes and Copy `BorrowMut` entries are `None`. + pub borrow_mut_cleanup_slots: Vec>, pub ret: Ty, /// Span-free return provenance carried through MIR for interface and ABI identity. L2a emits /// only `None`; L2b computes roots. pub return_borrow: hir::ReturnBorrowSummary, pub return_region: hir::ReturnRegionSummary, + pub return_cleanup: hir::ReturnCleanupAbi, /// Type of every slot, indexed by [`Slot`]. pub slots: Vec, /// Declared over-alignment of every slot (bytes, a validated power of two), indexed by @@ -182,7 +188,16 @@ fn par_map_function_work_units(f: &Function) -> u16 { units = units.saturating_add(4); } for stmt in &block.stmts { - if matches!(stmt, Stmt::Let(_, Rvalue::Call(..) | Rvalue::CallIndirect { .. })) { + if matches!( + stmt, + Stmt::Let( + _, + Rvalue::Call(..) + | Rvalue::CallWithCleanup(_) + | Rvalue::CallIndirect { .. } + | Rvalue::CallIndirectWithCleanup(_) + ) + ) { // A call is opaque at this stage; give it more weight than a local arithmetic // instruction without trying to recursively model a separately compiled callee. units = units.saturating_add(4); @@ -261,6 +276,8 @@ impl Function { Operand::Const(Const::Unit) => Ty::Unit, Operand::Value(v) => self.value_tys[*v as usize], Operand::Arg(i) => self.slots[self.params[*i as usize] as usize], + Operand::BorrowedPlace(place) => place.ty, + Operand::BorrowedCleanupArg(_) => Ty::Bool, } } } @@ -402,6 +419,7 @@ pub struct FnSignatureFacts { pub param_modes: Vec, pub return_borrow: hir::ReturnBorrowSummary, pub return_region: hir::ReturnRegionSummary, + pub return_cleanup: hir::ReturnCleanupAbi, } #[derive(Clone, Debug)] @@ -424,8 +442,11 @@ pub enum Rvalue { /// numeric operand/result type; lowers to the matching LLVM intrinsic (signedness/float from `ty`). MathOp { fn_: align_sema::MathFn, ty: Ty, operands: Vec }, Call(DirectCall, Vec), + /// An Align-ABI call whose recursively Move result is returned together with its runtime + /// cleanup bit. Codegen defines both `result` and `cleanup` from the one physical call. + CallWithCleanup(Box), /// The address of a top-level function as a value (`Ty::Fn`) — a function pointer. - FnAddr { target: ProgramCall, signature: FnSignatureFacts }, + FnAddr { target: ProgramCall, signature: Box }, /// A capturing closure value: the lifted function `lifted` (which takes the captures as /// trailing parameters) plus the captured values. Codegen copies the captures into a /// frame-local environment and builds `{ thunk_ptr, env_ptr }`, where the thunk unpacks the @@ -434,7 +455,7 @@ pub enum Rvalue { lifted: ProgramCall, captures: Vec, capture_tys: Vec, - signature: FnSignatureFacts, + signature: Box, }, /// An indirect call through a function-value `callee` (a `Ty::Fn` pointer). `param_tys`/`ret_ty` /// give codegen the LLVM function type for the indirect `call` (taken from the checked args / @@ -444,8 +465,10 @@ pub enum Rvalue { args: Vec, param_tys: Vec, ret_ty: Ty, - signature: FnSignatureFacts, + signature: Box, }, + /// The dynamic-cleanup counterpart of [`Rvalue::CallIndirect`]. + CallIndirectWithCleanup(Box), /// Load a (possibly nested) field from the struct in `slot`, addressed by the index `path` /// (length ≥ 1) — a GEP `[0, *path]` then a load. Field(Slot, Vec), @@ -1366,6 +1389,38 @@ pub enum Operand { Value(ValueId), /// The i-th incoming function argument. Arg(u32), + /// A stable caller-owned place passed by shared or exclusive borrow. The root slot remains + /// owned by the caller; `path` selects a nested struct field without loading or moving it. + BorrowedPlace(Box), + /// Cleanup bit carried beside an incoming whole-Move `BorrowMut` parameter. + BorrowedCleanupArg(u32), +} + +#[derive(Clone, Debug)] +pub struct BorrowedPlace { + pub slot: Slot, + pub path: Vec, + pub ty: Ty, + /// Caller-side cleanup-bit slot for an exclusive borrow of a whole Move place. `None` for a + /// shared borrow, a Copy pointee, or a Copy field of a Move aggregate. + pub cleanup: Option, +} + +#[derive(Clone, Debug)] +pub struct IndirectCallWithCleanup { + pub callee: Operand, + pub args: Vec, + pub param_tys: Vec, + pub ret_ty: Ty, + pub signature: FnSignatureFacts, + pub cleanup: ValueId, +} + +#[derive(Clone, Debug)] +pub struct DirectCallWithCleanup { + pub target: ProgramCall, + pub args: Vec, + pub cleanup: ValueId, } /// The boxed operands of a [`Rvalue::CryptoArgon2`] — two byte views (`password` / `salt`) and the @@ -1409,6 +1464,8 @@ pub enum Term { Goto(BlockId), Branch(Operand, BlockId, BlockId), Return(Option), + /// Return a recursively Move value and its path-selected ownership bit atomically. + ReturnWithCleanup(Box<(Operand, Operand)>), Unreachable, } @@ -1632,6 +1689,44 @@ fn lower_program_unchecked( // Function signature facts are immutable during MIR lowering. Materialize the shared table once // so lowering F functions does not deep-clone all T entries F times. let fn_types: Rc<[hir::FnTy]> = program.fn_types.clone().into(); + let named_return_cleanup = Rc::new( + program + .fns + .iter() + .map(|function| (function.name.clone(), function.return_cleanup)) + .chain( + program + .imported_fns + .iter() + .map(|function| (function.name.clone(), function.return_cleanup)), + ) + .chain( + program + .externs + .iter() + .map(|function| (function.name.clone(), function.return_cleanup)), + ) + .collect::>(), + ); + let named_param_modes = Rc::new( + program + .fns + .iter() + .map(|function| (function.name.clone(), function.param_modes.clone())) + .chain( + program + .imported_fns + .iter() + .map(|function| (function.name.clone(), function.param_modes.clone())), + ) + .chain( + program + .externs + .iter() + .map(|function| (function.name.clone(), function.param_modes.clone())), + ) + .collect::>(), + ); let mut fns: Vec = program .fns .iter() @@ -1643,6 +1738,8 @@ fn lower_program_unchecked( &program.enums, &program.tagged_types, &fn_types, + &named_return_cleanup, + &named_param_modes, lines.as_ref(), ); // Separate-compilation visibility (per-unit lowering only); whole-program lowering keeps @@ -1674,6 +1771,7 @@ fn lower_program_unchecked( ret: extern_.ret, return_borrow: extern_.return_borrow.clone(), return_region: extern_.return_region.clone(), + return_cleanup: extern_.return_cleanup, }) .collect(), // Cross-unit `pub` callee declares are a per-unit-only concern; the whole-program path has @@ -1689,6 +1787,7 @@ fn lower_program_unchecked( ret: import.ret, return_borrow: import.return_borrow.clone(), return_region: import.return_region.clone(), + return_cleanup: import.return_cleanup, }) .collect() } else { @@ -1706,6 +1805,7 @@ fn lower_program_unchecked( ret: definition.ret, return_borrow: definition.return_borrow.clone(), return_region: definition.return_region.clone(), + return_cleanup: definition.return_cleanup, }) .collect(), tuples: program.tuples.clone(), @@ -1755,6 +1855,10 @@ pub fn function_embedded_types(f: &Function) -> Vec { types.extend(param_tys.iter().copied()); types.push(*ret_ty); } + Rvalue::CallIndirectWithCleanup(call) => { + types.extend(call.param_tys.iter().copied()); + types.push(call.ret_ty); + } Rvalue::SpawnTask { capture_tys, r, .. } => { @@ -2147,6 +2251,10 @@ fn remap_function_embedded_types( remap_vec(param_tys); remap_ty(ret_ty, remap); } + Rvalue::CallIndirectWithCleanup(call) => { + remap_vec(&mut call.param_tys); + remap_ty(&mut call.ret_ty, remap); + } Rvalue::SpawnTask { capture_tys, r, .. } => { remap_vec(capture_tys); remap_ty(r, remap); @@ -2317,7 +2425,7 @@ fn simplify_known_drop_flags(f: &mut Function) { propagate(*then_bb); propagate(*else_bb); } - (Term::Return(_) | Term::Unreachable, _) => {} + (Term::Return(_) | Term::ReturnWithCleanup(_) | Term::Unreachable, _) => {} } } @@ -2351,7 +2459,7 @@ fn simplify_known_drop_flags(f: &mut Function) { pending.push(then_bb); pending.push(else_bb); } - Term::Return(_) | Term::Unreachable => {} + Term::Return(_) | Term::ReturnWithCleanup(_) | Term::Unreachable => {} } } if reachable.iter().all(|value| *value) { @@ -2376,7 +2484,7 @@ fn simplify_known_drop_flags(f: &mut Function) { *then_bb = remap[*then_bb as usize]; *else_bb = remap[*else_bb as usize]; } - Term::Return(_) | Term::Unreachable => {} + Term::Return(_) | Term::ReturnWithCleanup(_) | Term::Unreachable => {} } } f.entry = remap[f.entry as usize]; @@ -2400,7 +2508,7 @@ fn builder_key(op: &Operand, loads: &std::collections::HashMap) - match op { Operand::Value(v) => Some(loads.get(v).map(|s| BuilderKey::Slot(*s)).unwrap_or(BuilderKey::Value(*v))), Operand::Arg(i) => Some(BuilderKey::Arg(*i)), - Operand::Const(_) => None, + Operand::Const(_) | Operand::BorrowedPlace(_) | Operand::BorrowedCleanupArg(_) => None, } } @@ -2618,6 +2726,12 @@ struct BuilderCtx { /// Sema function-type facts used to make function-value and indirect-call signatures explicit /// in MIR. Kept behind the existing box to preserve recursive lowering stack headroom. fn_types: Rc<[hir::FnTy]>, + /// Producer-owned physical return facts for every named callable visible to this unit. + named_return_cleanup: Rc>, + /// Checked physical parameter modes for direct named calls. + named_param_modes: Rc>>, + /// Physical return ABI of the function currently being lowered. + return_cleanup: hir::ReturnCleanupAbi, /// Results produced by the active eager-expression worklist, keyed by stable HIR address. /// Recursive child requests consume these operands without re-entering `lower_expr`. eager_expr_results: std::collections::HashMap, @@ -2860,7 +2974,7 @@ impl Builder { mark(*then_bb); mark(*else_bb); } - Term::Return(_) | Term::Unreachable => {} + Term::Return(_) | Term::ReturnWithCleanup(_) | Term::Unreachable => {} } } self.blocks[current].term = Some(t); @@ -2879,6 +2993,9 @@ impl Builder { } } +// The source type tables and shared signature maps are distinct lowering invariants; keeping them +// explicit here makes the one Builder construction auditable and avoids a bag-of-context fields. +#[allow(clippy::too_many_arguments)] fn lower_fn( f: &hir::Fn, tuples: &[hir::TupleDef], @@ -2886,6 +3003,8 @@ fn lower_fn( enums: &[hir::EnumDef], tagged_types: &[hir::TaggedType], fn_types: &Rc<[hir::FnTy]>, + named_return_cleanup: &Rc>, + named_param_modes: &Rc>>, lines: Option<&Rc>, ) -> Function { let mut slots: Vec = f.locals.iter().map(|l| l.ty).collect(); @@ -2898,6 +3017,20 @@ fn lower_fn( drop_flags.push(None); drop_flags[local as usize] = Some(flag); } + let mut borrow_mut_cleanup_slots = vec![None; f.params.len()]; + for (index, (&local, &mode)) in f.params.iter().zip(&f.param_modes).enumerate() { + let ty = f.locals[local as usize].ty; + if mode == align_ast::ParamMode::BorrowMut + && needs_drop_flag(ty, structs, tuples, enums, tagged_types) + { + let flag = slots.len() as Slot; + slots.push(Ty::Bool); + slot_align.push(None); + drop_flags.push(None); + drop_flags[local as usize] = Some(flag); + borrow_mut_cleanup_slots[index] = Some(flag); + } + } let mut b = Builder { slots, slot_align, @@ -2926,6 +3059,9 @@ fn lower_fn( slot_borrow_owners: std::collections::HashMap::new(), dbg: lines.map(|l| Box::new(LineCtx { lines: Rc::clone(l), cur_span: None })), fn_types: Rc::clone(fn_types), + named_return_cleanup: Rc::clone(named_return_cleanup), + named_param_modes: Rc::clone(named_param_modes), + return_cleanup: f.return_cleanup, eager_expr_results: std::collections::HashMap::new(), eager_expr_active: false, }), @@ -2938,6 +3074,9 @@ fn lower_fn( let params: Vec = f.params.clone(); for (i, &slot) in params.iter().enumerate() { b.push(Stmt::Store(slot, Operand::Arg(i as u32))); + if let Some(flag) = borrow_mut_cleanup_slots[i] { + b.push(Stmt::Store(flag, Operand::BorrowedCleanupArg(i as u32))); + } if b.drop_locals.contains(&slot) { b.set_drop_flag(slot, b.drop_individual_locals.contains(&slot)); } @@ -2956,15 +3095,29 @@ fn lower_fn( // Fall-through end of the body: if the trailing value moves an owned local out (the // function returns it), clear that local's slot and flag — the caller now owns the value — // then conditionally drop the remaining owned locals. - if f.ret != Ty::Unit - && let Some(v) = &f.body.value { - null_moved_source(&mut b, v); - } + let cleanup = if f.return_cleanup == hir::ReturnCleanupAbi::DynamicBit { + f.body + .value + .as_ref() + .zip(tail.as_ref()) + .and_then(|(value, operand)| lowered_drop_flag(&mut b, value, operand)) + } else { + None + }; + if f.ret != Ty::Unit && let Some(v) = &f.body.value { + null_moved_source(&mut b, v); + } let tail = tail.filter(|_| f.ret != Ty::Unit); b.emit_exit_cleanup(); - match tail { - Some(op) => b.terminate(Term::Return(Some(op))), - None => b.terminate(Term::Return(None)), + match (tail, f.return_cleanup, cleanup) { + (Some(op), hir::ReturnCleanupAbi::DynamicBit, Some(cleanup)) => { + b.terminate(Term::ReturnWithCleanup(Box::new((op, cleanup)))) + } + (Some(_), hir::ReturnCleanupAbi::DynamicBit, None) => { + b.terminate(Term::Unreachable) + } + (Some(op), hir::ReturnCleanupAbi::None, _) => b.terminate(Term::Return(Some(op))), + (None, _, _) => b.terminate(Term::Return(None)), } } @@ -3007,9 +3160,11 @@ fn lower_fn( name: ProgramCall::from_validated(&f.name), params, param_modes: f.param_modes.clone(), + borrow_mut_cleanup_slots, ret: f.ret, return_borrow: f.return_borrow.clone(), return_region: f.return_region.clone(), + return_cleanup: f.return_cleanup, slots: b.slots, slot_align: b.slot_align, value_tys: b.value_tys, @@ -3784,13 +3939,30 @@ fn lower_stmt(b: &mut Builder, s: &hir::Stmt) { if !lowering_continues(b) { return; } + let cleanup = if b.ctx.return_cleanup == hir::ReturnCleanupAbi::DynamicBit { + value + .as_ref() + .zip(op.as_ref()) + .and_then(|(value, operand)| lowered_drop_flag(b, value, operand)) + } else { + None + }; // A returned owned array is moved out: null its slot so the exit cleanup below frees // null (the caller now owns the buffer), then free open arenas / drop owned locals. if let Some(e) = value { null_moved_source(b, e); } b.emit_exit_cleanup(); - b.terminate(Term::Return(op)); + match (op, b.ctx.return_cleanup, cleanup) { + (Some(value), hir::ReturnCleanupAbi::DynamicBit, Some(cleanup)) => { + b.terminate(Term::ReturnWithCleanup(Box::new((value, cleanup)))) + } + (Some(_), hir::ReturnCleanupAbi::DynamicBit, None) => { + b.terminate(Term::Unreachable) + } + (value, hir::ReturnCleanupAbi::None, _) => b.terminate(Term::Return(value)), + (None, hir::ReturnCleanupAbi::DynamicBit, _) => b.terminate(Term::Unreachable), + } // The current block is now terminated; `lower_block` stops here, so no dead // block is created and callers can see the divergence via `is_terminated`. } @@ -6817,6 +6989,31 @@ fn null_consumed_struct_sources(b: &mut Builder, value: &hir::Expr) { } } +fn lower_borrowed_place( + b: &Builder, + e: &hir::Expr, + mode: align_ast::ParamMode, +) -> Operand { + let (slot, path) = match &e.kind { + hir::ExprKind::Local(local) => (*local, Vec::new()), + hir::ExprKind::Field { root, path } => (*root, path.clone()), + _ => unreachable!("sema admitted a non-place borrowed argument"), + }; + let needs_cleanup = mode == align_ast::ParamMode::BorrowMut + && path.is_empty() + && needs_drop_flag(e.ty, &b.structs, &b.tuples, &b.enums, &b.tagged_types); + let cleanup = needs_cleanup.then(|| { + b.drop_flags[slot as usize] + .expect("whole-Move BorrowMut place has a caller-visible cleanup slot") + }); + Operand::BorrowedPlace(Box::new(BorrowedPlace { + slot, + path, + ty: e.ty, + cleanup, + })) +} + /// Lower an indirect call out-of-line so temporary-owner propagation does not enlarge the deeply /// recursive `lower_expr` stack frame (the `expr_depth` contract). #[inline(never)] @@ -6828,14 +7025,33 @@ fn lower_call_fn_value(b: &mut Builder, e: &hir::Expr) -> Operand { if !lowering_continues(b) { return Operand::Const(Const::Unit); } + let Some(signature) = fn_signature_facts(b, callee.ty) else { + b.terminate(Term::Unreachable); + return Operand::Const(Const::Unit); + }; // The function type for the indirect call comes from the (sema-checked) arg types and the // call's result type — no signature table is threaded into MIR. let mut param_tys = Vec::with_capacity(args.len()); let mut ops = Vec::with_capacity(args.len()); let mut arg_owners = Vec::with_capacity(args.len()); - for arg in args { + for (index, arg) in args.iter().enumerate() { param_tys.push(arg.ty); - let (op, owner) = lower_consumed_call_arg(b, arg); + let borrowed = matches!( + signature.param_modes.get(index), + Some(align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut) + ); + let (op, owner) = if borrowed { + ( + lower_borrowed_place( + b, + arg, + signature.param_modes[index], + ), + Vec::new(), + ) + } else { + lower_consumed_call_arg(b, arg) + }; ops.push(op); arg_owners.push(owner); // A nested `return`, `?`, or diverging expression already terminated the block. Do not @@ -6844,17 +7060,18 @@ fn lower_call_fn_value(b: &mut Builder, e: &hir::Expr) -> Operand { return Operand::Const(Const::Unit); } } - let Some(signature) = fn_signature_facts(b, callee.ty) else { - b.terminate(Term::Unreachable); - return Operand::Const(Const::Unit); - }; // A by-value owned argument is MOVED into the callee, exactly as in a direct call // (`lower_direct_call`) — null the source so the caller's exit `Drop` doesn't free the buffer the // callee now owns. Without this a bound owned local passed indirectly is double-freed (an inline // temporary was safe only because it has no source local to null). No-op for a Copy / borrowed // argument. An indirect call has no borrow-only intrinsics, so every argument transfers. - for arg in args { - null_consumed_struct_sources(b, arg); + for (index, arg) in args.iter().enumerate() { + if !matches!( + signature.param_modes.get(index), + Some(align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut) + ) { + null_consumed_struct_sources(b, arg); + } } // Every argument succeeded, so the outer call takes ownership. The hidden owners only protect // temporaries while later arguments are being evaluated. @@ -6893,16 +7110,33 @@ fn emit_indirect_call( signature: FnSignatureFacts, ) -> Operand { let v = b.fresh_value(ret_ty); - b.push(Stmt::Let( - v, - Rvalue::CallIndirect { - callee, - args, - param_tys, - ret_ty, - signature, - }, - )); + if signature.return_cleanup == hir::ReturnCleanupAbi::DynamicBit { + let cleanup = b.fresh_value(Ty::Bool); + b.push(Stmt::Let( + v, + Rvalue::CallIndirectWithCleanup(Box::new(IndirectCallWithCleanup { + callee, + args, + param_tys, + ret_ty, + signature, + cleanup, + })), + )); + b.attach_value_drop_flag(v, Operand::Value(cleanup)); + b.attach_value_temp_drop_flag(v, Operand::Value(cleanup)); + } else { + b.push(Stmt::Let( + v, + Rvalue::CallIndirect { + callee, + args, + param_tys, + ret_ty, + signature: Box::new(signature), + }, + )); + } // Align Unit is a value, but its function ABI is LLVM `void`. Keep the call statement for its // effects while giving every enclosing value context the canonical MIR Unit operand. if ret_ty == Ty::Unit { Operand::Const(Const::Unit) } else { Operand::Value(v) } @@ -6922,7 +7156,7 @@ fn finish_fn_value(b: &mut Builder, name: &str, ty: Ty) -> Operand { value, Rvalue::FnAddr { target: ProgramCall::from_validated(name), - signature, + signature: Box::new(signature), }, )); Operand::Value(value) @@ -6947,7 +7181,7 @@ fn finish_closure( lifted: ProgramCall::from_validated(lifted), captures, capture_tys, - signature, + signature: Box::new(signature), }, )); Operand::Value(value) @@ -6962,6 +7196,7 @@ fn fn_signature_facts(b: &Builder, ty: Ty) -> Option { param_modes: signature.params.iter().map(|(mode, _)| *mode).collect(), return_borrow: signature.return_borrow.clone(), return_region: signature.return_region.clone(), + return_cleanup: signature.return_cleanup, }) } @@ -6971,7 +7206,28 @@ fn fn_signature_facts(b: &Builder, ty: Ty) -> Option { /// Pipeline callables use this helper as well as ordinary call expressions so the rule cannot drift. fn emit_named_call(b: &mut Builder, func: ProgramCall, args: Vec, ret_ty: Ty) -> Operand { let v = b.fresh_value(ret_ty); - b.push(Stmt::Let(v, Rvalue::Call(DirectCall::Program(func), args))); + match b.ctx.named_return_cleanup.get(func.as_str()).copied() { + Some(hir::ReturnCleanupAbi::DynamicBit) => { + let cleanup = b.fresh_value(Ty::Bool); + b.push(Stmt::Let( + v, + Rvalue::CallWithCleanup(Box::new(DirectCallWithCleanup { + target: func, + args, + cleanup, + })), + )); + b.attach_value_drop_flag(v, Operand::Value(cleanup)); + b.attach_value_temp_drop_flag(v, Operand::Value(cleanup)); + } + Some(hir::ReturnCleanupAbi::None) => { + b.push(Stmt::Let(v, Rvalue::Call(DirectCall::Program(func), args))); + } + None => { + b.terminate(Term::Unreachable); + return Operand::Const(Const::Unit); + } + } if ret_ty == Ty::Unit { Operand::Const(Const::Unit) } else { Operand::Value(v) } } @@ -7120,10 +7376,24 @@ fn lower_direct_call(b: &mut Builder, e: &hir::Expr) -> Operand { unreachable!("lower_direct_call on a non-call expression"); }; let borrows_args = matches!(func.as_str(), "print" | "hash64" | "hash128"); + let param_modes = b.ctx.named_param_modes.get(func).cloned(); let mut ops = Vec::with_capacity(args.len()); let mut arg_owners = Vec::with_capacity(args.len()); - for arg in args { - let (op, owners) = if borrows_args { + for (index, arg) in args.iter().enumerate() { + let borrowed_place = matches!( + param_modes.as_ref().and_then(|modes| modes.get(index)), + Some(align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut) + ); + let (op, owners) = if borrowed_place { + ( + lower_borrowed_place( + b, + arg, + param_modes.as_ref().expect("program call modes")[index], + ), + Vec::new(), + ) + } else if borrows_args { (lower_borrowed_owned(b, arg), Vec::new()) } else { lower_consumed_call_arg(b, arg) @@ -7144,22 +7414,29 @@ fn lower_direct_call(b: &mut Builder, e: &hir::Expr) -> Operand { // A by-value owned-array argument is moved into the callee. Borrow-only intrinsics retain the // source, matching their sema contract. if !borrows_args { - for arg in args { - null_consumed_struct_sources(b, arg); + for (index, arg) in args.iter().enumerate() { + if !matches!( + param_modes.as_ref().and_then(|modes| modes.get(index)), + Some(align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut) + ) { + null_consumed_struct_sources(b, arg); + } } for owner in arg_owners.into_iter().flatten() { b.set_drop_flag(owner, false); } } - let v = b.fresh_value(e.ty); - b.push(Stmt::Let( - v, - Rvalue::Call(direct_call_target(func, args), ops.clone()), - )); - let result = if e.ty == Ty::Unit { - Operand::Const(Const::Unit) - } else { - Operand::Value(v) + let result = match direct_call_target(func, args) { + DirectCall::Program(target) => emit_named_call(b, target, ops.clone(), e.ty), + target @ DirectCall::Runtime(_) => { + let v = b.fresh_value(e.ty); + b.push(Stmt::Let(v, Rvalue::Call(target, ops.clone()))); + if e.ty == Ty::Unit { + Operand::Const(Const::Unit) + } else { + Operand::Value(v) + } + } }; if let Operand::Value(v) = &result { inherit_borrow_owners(b, *v, &ops); @@ -13404,7 +13681,15 @@ fn lower_try(b: &mut Builder, inner: &hir::Expr, ok_ty: Ty) -> Operand { null_moved_source(b, inner); // `?` exits the function: free open arenas and drop owned locals first. b.emit_exit_cleanup(); - b.terminate(Term::Return(Some(Operand::Value(propagated)))); + match (b.ctx.return_cleanup, inner_flag.clone()) { + (hir::ReturnCleanupAbi::DynamicBit, Some(cleanup)) => b.terminate( + Term::ReturnWithCleanup(Box::new((Operand::Value(propagated), cleanup))), + ), + (hir::ReturnCleanupAbi::DynamicBit, None) => b.terminate(Term::Unreachable), + (hir::ReturnCleanupAbi::None, _) => { + b.terminate(Term::Return(Some(Operand::Value(propagated)))) + } + } // Ok: continue with the unwrapped value. If the operand was a bound local holding an owned // payload (e.g. `r: Result`), the payload is now moved into `v`, so null the source @@ -13988,15 +14273,11 @@ fn lower_map_err(b: &mut Builder, result: &hir::Expr, f: &hir::Expr, out_ty: Ty) e2_ty, mapper_signature, ); + let mapped_flag = b + .value_drop_flag(&conv) + .unwrap_or(Operand::Const(Const::Bool(false))); let errr = b.fresh_value(out_ty); b.push(Stmt::Let(errr, Rvalue::ResultErr(conv))); - let mapped_flag = Operand::Const(Const::Bool(needs_drop_flag( - e2_ty, - &b.structs, - &b.tuples, - &b.enums, - &b.tagged_types, - ))); if let Some(flag_slot) = result_flag { b.push(Stmt::Store(flag_slot, mapped_flag.clone())); } @@ -14308,6 +14589,9 @@ mod tests { slot_borrow_owners: Default::default(), dbg: None, fn_types: Rc::from(Vec::::new()), + named_return_cleanup: Rc::new(std::collections::HashMap::new()), + named_param_modes: Rc::new(std::collections::HashMap::new()), + return_cleanup: hir::ReturnCleanupAbi::None, eager_expr_results: std::collections::HashMap::new(), eager_expr_active: false, }), @@ -15090,7 +15374,10 @@ fn main() -> i32 = 0 function .blocks .iter() - .any(|block| matches!(block.term, Term::Return(Some(_)))), + .any(|block| matches!( + block.term, + Term::Return(Some(_)) | Term::ReturnWithCleanup(_) + )), "{name} must retain a real continuation to its function return: {function:#?}" ); } @@ -15129,8 +15416,10 @@ fn main() -> i32 = 0 name: ProgramCall::from_validated("main"), params: vec![], param_modes: vec![], + borrow_mut_cleanup_slots: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, ret: i32_ty, slots: vec![], slot_align: vec![], @@ -15145,11 +15434,12 @@ fn main() -> i32 = 0 lifted: ProgramCall::from_validated("unused"), captures: vec![], capture_tys: vec![Ty::Tagged(1)], - signature: FnSignatureFacts { + signature: Box::new(FnSignatureFacts { param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, - }, + return_cleanup: hir::ReturnCleanupAbi::None, + }), }, )], stmt_lines: vec![(0, 0)], @@ -15237,7 +15527,12 @@ fn main() -> i32 = 0 function .blocks .iter() - .any(|block| matches!(block.term, Term::Return(Some(_)))), + .any(|block| { + matches!( + block.term, + Term::Return(Some(_)) | Term::ReturnWithCleanup(_) + ) + }), "{name} must return a typed operand:\n{}", print::function_to_string(function) ); diff --git a/crates/align_mir/src/print.rs b/crates/align_mir/src/print.rs index b16f2280..30870356 100644 --- a/crates/align_mir/src/print.rs +++ b/crates/align_mir/src/print.rs @@ -51,12 +51,13 @@ fn fn_to_string(out: &mut String, f: &Function) { .collect(); let _ = writeln!( out, - "fn {}({}) -> {} borrow={:?} region={:?} {{", + "fn {}({}) -> {} borrow={:?} region={:?} cleanup={:?} {{", f.name, params.join(", "), ty_name(f.ret), f.return_borrow, - f.return_region + f.return_region, + f.return_cleanup ); for b in &f.blocks { block_to_string(out, b); @@ -175,6 +176,15 @@ fn block_to_string(out: &mut String, b: &Block) { Term::Return(None) => { let _ = writeln!(out, " return"); } + Term::ReturnWithCleanup(returned) => { + let (value, cleanup) = returned.as_ref(); + let _ = writeln!( + out, + " return_with_cleanup {}, {}", + operand_str(value), + operand_str(cleanup) + ); + } Term::Unreachable => { let _ = writeln!(out, " unreachable"); } @@ -226,6 +236,11 @@ fn rvalue_str(rv: &Rvalue) -> String { } } } + Rvalue::CallWithCleanup(call) => { + let crate::DirectCallWithCleanup { target, args, cleanup } = call.as_ref(); + let a: Vec = args.iter().map(operand_str).collect(); + format!("call_with_cleanup program {target}({}) -> %{cleanup}", a.join(", ")) + } Rvalue::FnAddr { target, signature } => { format!("fn_addr {target} signature={signature:?}") } @@ -251,6 +266,21 @@ fn rvalue_str(rv: &Rvalue) -> String { a.join(", ") ) } + Rvalue::CallIndirectWithCleanup(call) => { + let crate::IndirectCallWithCleanup { + callee, + args, + signature, + cleanup, + .. + } = call.as_ref(); + let a: Vec = args.iter().map(operand_str).collect(); + format!( + "call_indirect_with_cleanup {}({}) signature={signature:?} -> %{cleanup}", + operand_str(callee), + a.join(", ") + ) + } Rvalue::Field(slot, path) => format!("_{slot}.{}", path.iter().map(|i| i.to_string()).collect::>().join(".")), Rvalue::Select { cond, a, b } => format!("select({}, {}, {})", operand_str(cond), operand_str(a), operand_str(b)), Rvalue::SoaColumn { base, struct_id, field } => format!("soa_col(_{base}: struct#{struct_id}, .{field})"), @@ -819,6 +849,15 @@ fn operand_str(op: &Operand) -> String { Operand::Const(Const::Unit) => "()".to_string(), Operand::Value(v) => format!("%{v}"), Operand::Arg(i) => format!("arg{i}"), + Operand::BorrowedPlace(place) => { + let suffix = place + .path + .iter() + .map(|field| format!(".{field}")) + .collect::(); + format!("borrow slot{}{}", place.slot, suffix) + } + Operand::BorrowedCleanupArg(index) => format!("arg{index}.cleanup"), } } diff --git a/crates/align_mir/src/source_shape.rs b/crates/align_mir/src/source_shape.rs index cd31f80e..ba3fa224 100644 --- a/crates/align_mir/src/source_shape.rs +++ b/crates/align_mir/src/source_shape.rs @@ -25,6 +25,7 @@ pub(super) enum SourceShapeNode<'a> { ret: &'a Ty, return_borrow: &'a hir::ReturnBorrowSummary, return_region: &'a hir::ReturnRegionSummary, + return_cleanup: hir::ReturnCleanupAbi, }, } @@ -86,6 +87,7 @@ impl SourceShapeView for hir::Program { ret: &definition.ret, return_borrow: &definition.return_borrow, return_region: &definition.return_region, + return_cleanup: definition.return_cleanup, }) } } @@ -290,14 +292,19 @@ impl SourceShapeCo ret: left_ret, return_borrow: left_borrow, return_region: left_region, + return_cleanup: left_cleanup, }, SourceShapeNode::Function { params: right_params, ret: right_ret, return_borrow: right_borrow, return_region: right_region, + return_cleanup: right_cleanup, }, ) => { + if left_cleanup != right_cleanup { + return false; + } if left_params.len() != right_params.len() || left_borrow != right_borrow || left_region != right_region @@ -562,6 +569,7 @@ fn shape_cost(node: &SourceShapeNode<'_>) -> (usize, usize) { ret, return_borrow, return_region, + return_cleanup: _, } => { let mut cost = ty_cost(**ret); cost.1 += 4 + borrow_summary_work(return_borrow) + region_summary_work(return_region); diff --git a/crates/align_mir/src/validate_hir.rs b/crates/align_mir/src/validate_hir.rs index 164e0abd..58023c24 100644 --- a/crates/align_mir/src/validate_hir.rs +++ b/crates/align_mir/src/validate_hir.rs @@ -6,6 +6,15 @@ use align_span::Span; use super::canonical_graph::Node; use super::source_shape::source_shape_equal; +fn source_shapes_match( + program: &hir::Program, + left: Node, + right: Node, + known_shapes: &mut HashSet<(Node, Node)>, +) -> bool { + source_shape_equal(program, left, right, known_shapes) +} + /// Validate the program-global HIR type domain before MIR construction. pub(crate) fn global_type_metadata_is_valid(program: &hir::Program) -> bool { Validator::new(program).validate() @@ -197,6 +206,7 @@ impl<'a> DeclarationValidator<'a> { .all(|&ty| self.placement.ffi_parameter_ok(ty)) || !self.placement.ffi_return_ok(function.ret) || !summary_is_none(&function.return_borrow, &function.return_region) + || !self.return_cleanup_valid(function.ret, function.return_cleanup) { return false; } @@ -217,6 +227,7 @@ impl<'a> DeclarationValidator<'a> { &function.return_borrow, &function.return_region, ) + || !self.return_cleanup_valid(function.ret, function.return_cleanup) { return false; } @@ -238,7 +249,7 @@ impl<'a> DeclarationValidator<'a> { }; 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) + !mode_is_valid(self.program, *mode, align_sema::scalar_to_ty(*scalar), true) || !self.placement.scalar_ok( *scalar, ScalarPlacement::FnParameter { allow_param }, @@ -253,7 +264,13 @@ impl<'a> DeclarationValidator<'a> { .iter() .map(|(_, scalar)| align_sema::scalar_to_ty(*scalar)) .collect::>(), + &function + .params + .iter() + .map(|(mode, _)| *mode) + .collect::>(), ) + || !self.return_cleanup_valid(function.ret, function.return_cleanup) { return false; } @@ -282,17 +299,22 @@ impl<'a> DeclarationValidator<'a> { fn origin_valid(&self, function: &hir::Fn) -> bool { match function.origin { - hir::FnOrigin::Source { .. } | hir::FnOrigin::Monomorph => true, + hir::FnOrigin::Source { .. } | hir::FnOrigin::Monomorph => match &function.return_borrow { + hir::ReturnBorrowSummary::None => true, + hir::ReturnBorrowSummary::Roots { captures, .. } => captures.is_empty(), + }, 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, - ) + && match &function.return_borrow { + hir::ReturnBorrowSummary::None => true, + hir::ReturnBorrowSummary::Roots { captures, .. } => captures + .iter() + .all(|capture| *capture < capture_count), + } } } } @@ -305,6 +327,7 @@ impl<'a> DeclarationValidator<'a> { for (&local_id, &mode) in function.params.iter().zip(&function.param_modes) { if !seen.insert(local_id) || !mode_is_valid( + self.program, mode, function .locals @@ -319,7 +342,9 @@ impl<'a> DeclarationValidator<'a> { let Some(local) = function.locals.get(local_id as usize) else { return false; }; - if local.id != local_id { + if local.id != local_id + || (mode == align_ast::ParamMode::BorrowMut && !local.is_mut) + { return false; } } @@ -384,7 +409,24 @@ impl<'a> DeclarationValidator<'a> { &function.return_borrow, &function.return_region, ¶meter_types, + &function.param_modes, ) + && self.return_cleanup_valid(function.ret, function.return_cleanup) + } + + fn return_cleanup_valid(&self, ret: Ty, cleanup: hir::ReturnCleanupAbi) -> bool { + let expected = if align_sema::needs_drop_flag( + ret, + &self.program.structs, + &self.program.tuples, + &self.program.enums, + &self.program.tagged_types, + ) { + hir::ReturnCleanupAbi::DynamicBit + } else { + hir::ReturnCleanupAbi::None + }; + cleanup == expected } fn drop_sets_valid(&self, function: &hir::Fn) -> bool { @@ -458,11 +500,11 @@ impl<'a> DeclarationValidator<'a> { .iter() .zip(params) .all(|(&mode, &ty)| { - mode_is_valid(mode, ty, true) + mode_is_valid(self.program, 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) + && summary_valid(self.program, borrow, region, params, modes) } } @@ -478,11 +520,25 @@ fn valid_span(span: Span) -> bool { span.lo <= span.hi } -fn mode_is_valid(mode: align_ast::ParamMode, ty: Ty, allow_out: bool) -> bool { +fn mode_is_valid( + program: &hir::Program, + 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, + align_ast::ParamMode::Borrow => { + align_sema::needs_drop_flag( + ty, + &program.structs, + &program.tuples, + &program.enums, + &program.tagged_types, + ) + } + align_ast::ParamMode::BorrowMut => true, } } @@ -499,6 +555,7 @@ fn summary_valid( borrow: &hir::ReturnBorrowSummary, region: &hir::ReturnRegionSummary, params: &[Ty], + modes: &[align_ast::ParamMode], ) -> bool { match (borrow, region) { (hir::ReturnBorrowSummary::None, hir::ReturnRegionSummary::None) => true, @@ -512,16 +569,16 @@ fn summary_valid( captures: region_captures, }, ) => { - !borrow_params.is_empty() + (!borrow_params.is_empty() || !borrow_captures.is_empty()) && borrow_params == region_params - && borrow_captures.is_empty() - && region_captures.is_empty() + && borrow_captures == region_captures && borrow_params.windows(2).all(|pair| pair[0] < pair[1]) + && borrow_captures.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( + params.get(id as usize).is_some_and(|&ty| { + modes.get(id as usize).is_some_and(|mode| { + matches!(mode, align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut) + || align_sema::ty_may_borrow( ty, &program.structs, &program.tuples, @@ -529,6 +586,7 @@ fn summary_valid( &program.tagged_types, ) }) + }) }) } _ => false, @@ -656,7 +714,7 @@ impl<'a> NominalLinkValidator<'a> { return true; }; existing_kind == kind - && source_shape_equal( + && source_shapes_match( self.program, nominal_node(kind, existing_id), nominal_node(kind, id), @@ -2549,6 +2607,7 @@ impl<'a> BodyValidator<'a> { let mut work = vec![Pending::Ty(actual, expected)]; let mut seen_tys = HashSet::new(); let mut seen_scalars = HashSet::new(); + let mut known_shapes = HashSet::new(); while let Some(item) = work.pop() { match item { Pending::Ty(actual, expected) => { @@ -2600,6 +2659,92 @@ impl<'a> BodyValidator<'a> { }; work.push(Pending::Ty(actual, expected)); } + (Ty::Struct(actual), Ty::Struct(expected)) => { + if !source_shapes_match( + self.program, + Node::Struct(actual), + Node::Struct(expected), + &mut known_shapes, + ) { + return false; + } + } + (Ty::Enum(actual), Ty::Enum(expected)) => { + if !source_shapes_match( + self.program, + Node::Enum(actual), + Node::Enum(expected), + &mut known_shapes, + ) { + return false; + } + } + (Ty::Tuple(actual), Ty::Tuple(expected)) => { + if !source_shapes_match( + self.program, + Node::Tuple(actual), + Node::Tuple(expected), + &mut known_shapes, + ) { + return false; + } + } + ( + Ty::StructArray(actual, actual_len), + Ty::StructArray(expected, expected_len), + ) => { + if actual_len != expected_len + || !source_shapes_match( + self.program, + Node::Struct(actual), + Node::Struct(expected), + &mut known_shapes, + ) + { + return false; + } + } + ( + Ty::DynStructArray(actual, actual_layout), + Ty::DynStructArray(expected, expected_layout), + ) => { + if actual_layout != expected_layout + || !source_shapes_match( + self.program, + Node::Struct(actual), + Node::Struct(expected), + &mut known_shapes, + ) + { + return false; + } + } + (Ty::Soa(actual), Ty::Soa(expected)) + | (Ty::JsonScanner(actual), Ty::JsonScanner(expected)) => { + if !source_shapes_match( + self.program, + Node::Struct(actual), + Node::Struct(expected), + &mut known_shapes, + ) { + return false; + } + } + ( + Ty::DictEncoded(actual, actual_field), + Ty::DictEncoded(expected, expected_field), + ) => { + if actual_field != expected_field + || !source_shapes_match( + self.program, + Node::Struct(actual), + Node::Struct(expected), + &mut known_shapes, + ) + { + return false; + } + } (Ty::Option(actual), Ty::Option(expected)) | (Ty::Box(actual), Ty::Box(expected)) | (Ty::Slice(actual), Ty::Slice(expected)) @@ -2628,6 +2773,31 @@ impl<'a> BodyValidator<'a> { continue; } match (actual, expected) { + (Scalar::Struct(actual), Scalar::Struct(expected)) + | ( + Scalar::DynStructArray(actual), + Scalar::DynStructArray(expected), + ) + | (Scalar::Soa(actual), Scalar::Soa(expected)) => { + if !source_shapes_match( + self.program, + Node::Struct(actual), + Node::Struct(expected), + &mut known_shapes, + ) { + return false; + } + } + (Scalar::Enum(actual), Scalar::Enum(expected)) => { + if !source_shapes_match( + self.program, + Node::Enum(actual), + Node::Enum(expected), + &mut known_shapes, + ) { + return false; + } + } (Scalar::Fn(actual_id), Scalar::Fn(expected_id)) => { work.push(Pending::Ty(Ty::Fn(actual_id), Ty::Fn(expected_id))); } @@ -4873,11 +5043,7 @@ impl<'a> BodyValidator<'a> { let callee_flow = self.expr_flow(callee)?; let Ty::Fn(fid) = callee_flow.ty else { return None }; let function = self.program.fn_types.get(fid as usize)?; - if function.params.len() != args.len() - || function.params.iter().any(|(mode, _)| { - matches!(mode, align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut) - }) - { + if function.params.len() != args.len() { return None; } let arg_flows = self.expr_flows(args)?; @@ -4891,6 +5057,11 @@ impl<'a> BodyValidator<'a> { { return None; } + if matches!(mode, align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut) + && !self.borrow_arg_is_valid(context, &args[index], *mode) + { + return None; + } } let mut all = vec![callee_flow]; all.extend(arg_flows); @@ -5110,11 +5281,16 @@ impl<'a> BodyValidator<'a> { if !self.body_ty_matches(actual.ty, *expected) { return None; } - if matches!(mode, align_ast::ParamMode::Out | align_ast::ParamMode::BorrowMut) + if *mode == align_ast::ParamMode::Out && !self.out_arg_is_writable(context, args, index) { return None; } + if matches!(mode, align_ast::ParamMode::Borrow | align_ast::ParamMode::BorrowMut) + && !self.borrow_arg_is_valid(context, &args[index], *mode) + { + return None; + } } let all = arg_flows; let (falls, breaks) = strict_flow(&all); @@ -8573,14 +8749,56 @@ impl<'a> BodyValidator<'a> { args: &[hir::Expr], index: usize, ) -> bool { - let Some(hir::ExprKind::Local(id)) = args.get(index).map(|arg| &arg.kind) else { + let Some(mut argument) = args.get(index) else { return false; }; + let id = loop { + match &argument.kind { + hir::ExprKind::Local(id) => break *id, + hir::ExprKind::ArrayToSlice(inner) => argument = inner, + hir::ExprKind::SliceRange { recv, .. } => argument = recv, + _ => return false, + } + }; self.program .fns .get(context.function) - .and_then(|function| function.locals.get(*id as usize)) - .is_some_and(|local| local.id == *id && local.is_mut) + .and_then(|function| function.locals.get(id as usize)) + .is_some_and(|local| local.id == id && local.is_mut) + } + + fn borrow_arg_is_valid( + &self, + context: &BodyContext, + argument: &hir::Expr, + mode: align_ast::ParamMode, + ) -> bool { + let (root, field) = match &argument.kind { + hir::ExprKind::Local(local) => (*local, false), + hir::ExprKind::Field { root, .. } => (*root, true), + _ => return false, + }; + let Some(local) = self + .program + .fns + .get(context.function) + .and_then(|function| function.locals.get(root as usize)) + .filter(|local| local.id == root) + else { + return false; + }; + let move_pointee = align_sema::needs_drop_flag( + argument.ty, + &self.program.structs, + &self.program.tuples, + &self.program.enums, + &self.program.tagged_types, + ); + match mode { + align_ast::ParamMode::Borrow => move_pointee, + align_ast::ParamMode::BorrowMut => local.is_mut && !(field && move_pointee), + _ => false, + } } fn raw_scalar_ok(&self, scalar: Scalar) -> bool { diff --git a/crates/align_mir/src/validate_hir_tests.rs b/crates/align_mir/src/validate_hir_tests.rs index a8d57b10..0bf1fcdf 100644 --- a/crates/align_mir/src/validate_hir_tests.rs +++ b/crates/align_mir/src/validate_hir_tests.rs @@ -29,6 +29,7 @@ fn declaration_header_program() -> hir::Program { ret: int(64), return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }); program.imported_fns.push(ImportedFn { name: "dep$read".to_string(), @@ -44,6 +45,7 @@ fn declaration_header_program() -> hir::Program { params: vec![0], captures: Vec::new(), }, + return_cleanup: hir::ReturnCleanupAbi::None, effect: FnEffect::Pure, }); let span = align_span::Span::new(0, 0, 0); @@ -64,6 +66,7 @@ fn declaration_header_program() -> hir::Program { params: vec![0], captures: Vec::new(), }, + return_cleanup: hir::ReturnCleanupAbi::None, locals: vec![ hir::Local { id: 0, @@ -151,7 +154,10 @@ fn checked_interface_program( external_effects.insert("dep$identity".to_string(), effect); let mut external_provenance = align_sema::ExternalReturnProvenance::new(); if let Some(provenance) = provenance { - external_provenance.insert("dep$identity".to_string(), provenance); + external_provenance.insert( + "dep$identity".to_string(), + (provenance.0, provenance.1, hir::ReturnCleanupAbi::None), + ); } let program = if external_provenance.is_empty() { align_sema::check_program_with_effects(&modules, &external_effects, &mut diagnostics) @@ -215,6 +221,7 @@ fn fn_type_header_program() -> hir::Program { params: vec![0, 1], captures: Vec::new(), }, + return_cleanup: hir::ReturnCleanupAbi::None, effect: Cell::new(FnEffect::Pure), }; program @@ -268,6 +275,7 @@ fn main_header_program(params: Vec, param_modes: Vec, ret, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: local_params, body: hir::Block { stmts: Vec::new(), @@ -433,9 +441,34 @@ fn malformed_hir_declaration_header_metadata_fails_closed() { 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 mut lifted_summary = base.clone(); + lifted_summary.fns[0].origin = hir::FnOrigin::Lifted { capture_count: 1 }; + lifted_summary.fns[0].locals[0].is_param = false; + lifted_summary.fns[0].return_borrow = ReturnBorrowSummary::Roots { + params: Vec::new(), + captures: vec![0], + }; + lifted_summary.fns[0].return_region = ReturnRegionSummary::Roots { + params: Vec::new(), + captures: vec![0], + }; + assert!( + validate_hir::declaration_header_metadata_is_valid(&lifted_summary), + "an in-range lifted capture summary is valid declaration metadata" + ); + assert!( + align_sema::checked_hir_body_facts_are_valid(&lifted_summary), + "the lifted capture summary must agree with producer replay" + ); + assert_one_header_mutation("lifted-origin-summary-range", &lifted_summary, |program| { + program.fns[0].return_borrow = ReturnBorrowSummary::Roots { + params: Vec::new(), + captures: vec![1], + }; + program.fns[0].return_region = ReturnRegionSummary::Roots { + params: Vec::new(), + captures: vec![1], + }; }); let fn_base = fn_type_header_program(); @@ -473,16 +506,35 @@ fn malformed_hir_declaration_header_metadata_fails_closed() { captures: Vec::new(), }; }); - assert_one_header_mutation("fn-type-summary-captures", &fn_base, |program| { - program.fn_types[0].return_borrow = ReturnBorrowSummary::Roots { + let mut stale_fn_capture = fn_base.clone(); + stale_fn_capture.fn_types[0].return_borrow = ReturnBorrowSummary::Roots { params: vec![0, 1], captures: vec![0], - }; - program.fn_types[0].return_region = ReturnRegionSummary::Roots { + }; + stale_fn_capture.fn_types[0].return_region = ReturnRegionSummary::Roots { params: vec![0, 1], captures: vec![0], - }; - }); + }; + assert!( + validate_hir::declaration_header_metadata_is_valid(&stale_fn_capture), + "a canonical function-type capture summary is structurally valid" + ); + assert_replay_rejects_without_mutating( + stale_fn_capture.clone(), + "a function-type capture summary without a concrete producer target must fail replay", + ); + let source_map = SourceMap::new(); + for lowered in [ + lower_program(&stale_fn_capture), + lower_program_located(&stale_fn_capture, &source_map), + lower_program_per_unit(&stale_fn_capture), + lower_program_per_unit_located(&stale_fn_capture, &source_map), + ] { + assert!( + is_empty(&lowered), + "fn-type-summary-captures: stale producer fact published MIR" + ); + } let summary_base = summary_header_program(); assert!(validate_hir::declaration_header_metadata_is_valid(&summary_base)); @@ -1950,6 +2002,7 @@ fn deep_hir_header_type_dag_is_stack_bounded() { params: vec![0], captures: Vec::new(), }, + return_cleanup: hir::ReturnCleanupAbi::None, effect: FnEffect::Unknown, }); assert!(validate_hir::declaration_header_metadata_is_valid(&program)); @@ -1964,6 +2017,7 @@ fn deep_hir_header_type_dag_is_stack_bounded() { return_provenance_known: false, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, effect: FnEffect::Impure, }); assert_header_rejected("deep-header-later-sibling", &malformed); @@ -1993,6 +2047,7 @@ fn fn_type(ret: Ty) -> FnTy { ret, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, effect: Cell::new(FnEffect::Unknown), } } @@ -2003,6 +2058,7 @@ fn body_fn_type(params: Vec<(align_ast::ParamMode, Scalar)>, ret: Ty) -> FnTy { ret, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, effect: Cell::new(FnEffect::Pure), } } @@ -2178,6 +2234,7 @@ fn imported_fn(name: &str, params: Vec, ret: Ty) -> ImportedFn { return_provenance_known: false, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, effect: FnEffect::Pure, } } @@ -2244,6 +2301,7 @@ fn with_return(ty: Ty) -> hir::Program { return_provenance_known: false, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, effect: FnEffect::Pure, }); program @@ -2848,6 +2906,7 @@ fn with_unary_body_depth(depth: usize) -> hir::Program { ret: int(64), return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -2932,6 +2991,7 @@ fn with_mixed_eager_body_depth(depth: usize) -> hir::Program { ret, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -2958,6 +3018,7 @@ fn with_str_trim_body_depth(depth: usize) -> hir::Program { ret: Ty::Str, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -3057,6 +3118,7 @@ fn with_path_string_body_depth(depth: usize) -> hir::Program { ret: Ty::String, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -3098,6 +3160,7 @@ fn with_reader_buffered_body_depth(depth: usize) -> hir::Program { ret: Ty::Reader, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -3177,6 +3240,7 @@ fn with_bytes_str_cycle_body_depth(depth: usize) -> hir::Program { ret: result_ty, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -3271,6 +3335,7 @@ fn with_regex_string_body_depth(depth: usize) -> hir::Program { ret: Ty::String, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: vec![hir::Local { id: 0, name: "regex".to_string(), @@ -3340,6 +3405,7 @@ fn with_template_body_depth(depth: usize) -> hir::Program { ret: Ty::String, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -3379,6 +3445,7 @@ fn with_file_body_depth(depth: usize) -> hir::Program { ret: result_ty, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -3424,6 +3491,7 @@ fn with_array_builder_body_depth(depth: usize) -> hir::Program { ret: Ty::Unit, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: vec![hir::Local { id: 0, name: "builder".to_string(), @@ -3477,6 +3545,7 @@ fn with_process_command_body_depth(depth: usize) -> hir::Program { ret: Ty::Unit, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: vec![hir::Local { id: 0, name: "argv".to_string(), @@ -3529,6 +3598,7 @@ fn with_http_body_depth(depth: usize) -> hir::Program { ret: Ty::Unit, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: vec![hir::Stmt::Expr(expr)], @@ -3594,6 +3664,7 @@ fn with_block_stmt_body_depth(depth: usize) -> hir::Program { ret: Ty::Unit, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -3664,6 +3735,7 @@ fn with_match_arm_body_depth(depth: usize) -> hir::Program { ret: expr.ty, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -3749,6 +3821,7 @@ fn with_if_branch_body_depth(depth: usize) -> hir::Program { ret, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -3840,6 +3913,7 @@ fn with_binary_match_body_depth(depth: usize) -> hir::Program { ret, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -3905,6 +3979,7 @@ fn with_conditional_operand_body_depth(depth: usize) -> hir::Program { ret: Ty::Bool, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -3972,6 +4047,7 @@ fn with_scoped_control_body_depth(depth: usize) -> hir::Program { ret, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -4033,6 +4109,7 @@ fn with_loop_body_depth(depth: usize) -> hir::Program { ret: int(64), return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: hir::Block { stmts: Vec::new(), @@ -4103,6 +4180,7 @@ fn with_stage_body_depth(depth: usize) -> hir::Program { ret: int(64), return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: vec![hir::Local { id: 0, name: "xs".to_string(), @@ -4134,6 +4212,38 @@ struct DepthFixture { owner: MirOwner, } +fn normalize_test_return_cleanup(program: &mut hir::Program) { + let structs = &program.structs; + let tuples = &program.tuples; + let enums = &program.enums; + let tagged_types = &program.tagged_types; + let classify = |ret| { + if align_sema::needs_drop_flag( + ret, + structs, + tuples, + enums, + tagged_types, + ) { + hir::ReturnCleanupAbi::DynamicBit + } else { + hir::ReturnCleanupAbi::None + } + }; + for function in &mut program.fns { + function.return_cleanup = classify(function.ret); + } + for function in &mut program.externs { + function.return_cleanup = classify(function.ret); + } + for function in &mut program.imported_fns { + function.return_cleanup = classify(function.ret); + } + for function in &mut program.fn_types { + function.return_cleanup = classify(function.ret); + } +} + fn depth_fixtures() -> Vec { vec![ DepthFixture { @@ -4250,7 +4360,8 @@ fn checked_hir_depth_closure_matrix() { align_sema::MAX_CHECKED_HIR_DEPTH - 1, align_sema::MAX_CHECKED_HIR_DEPTH, ] { - let program = (fixture.make)(depth); + let mut program = (fixture.make)(depth); + normalize_test_return_cleanup(&mut program); assert!( align_sema::checked_hir_body_depth_is_valid(&program), "{}: valid checked-HIR depth {depth} was rejected", @@ -4263,7 +4374,8 @@ fn checked_hir_depth_closure_matrix() { let depth = align_sema::MAX_CHECKED_HIR_DEPTH + 1; let source_map = SourceMap::new(); for fixture in depth_fixtures() { - let program = (fixture.make)(depth); + let mut program = (fixture.make)(depth); + normalize_test_return_cleanup(&mut program); assert!( !align_sema::checked_hir_body_depth_is_valid(&program), "{}: over-bound checked-HIR depth {depth} was accepted", @@ -4322,6 +4434,7 @@ fn deep_type_consumer_closure_matrix() { }) .collect(); program.fn_types[0].ret = Ty::Tagged(0); + program.fn_types[0].return_cleanup = hir::ReturnCleanupAbi::DynamicBit; assert_accepted("deep nominal/tagged type consumer", &program); program.structs[DEPTH - 1].fields.push(FieldDef { @@ -4567,6 +4680,7 @@ fn malformed_hir_type_placement_fails_closed() { ret: Ty::Unit, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }); assert_placement_rejected("bool extern parameter", &extern_bool); @@ -4578,6 +4692,7 @@ fn malformed_hir_type_placement_fails_closed() { ret: Ty::Str, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }); assert_placement_rejected("view extern return", &extern_view_return); } @@ -4987,10 +5102,12 @@ fn valid_hir_type_placement_preflight_is_mir_identity() { ), ]; program.fn_types[0].ret = Ty::Result(Scalar::String, Scalar::Enum(0)); + program.fn_types[0].return_cleanup = hir::ReturnCleanupAbi::DynamicBit; program .imported_fns .push(imported_fn("dep$placement", vec![Ty::File], Ty::Unit)); program.imported_fns[0].ret = Ty::Result(Scalar::File, Scalar::Enum(0)); + program.imported_fns[0].return_cleanup = hir::ReturnCleanupAbi::DynamicBit; assert_accepted("body-independent placement matrix", &program); let mut externs = program.clone(); @@ -5019,6 +5136,7 @@ fn valid_hir_type_placement_preflight_is_mir_identity() { ret: Ty::Struct(c_struct), return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }); assert_accepted("extern placement matrix", &externs); } @@ -5100,6 +5218,24 @@ fn body_test_expr(kind: hir::ExprKind, ty: Ty) -> hir::Expr { } } +fn body_test_return_cleanup(ret: Ty) -> hir::ReturnCleanupAbi { + let dynamic = match ret { + Ty::String + | Ty::DynArray(_) + | Ty::DynStructArray(..) + | Ty::DynSliceArray(_) + | Ty::DynResponseArray => true, + Ty::Option(value) => value.is_move(), + Ty::Result(ok, err) => ok.is_move() || err.is_move(), + other => align_sema::is_move_handle(other), + }; + if dynamic { + hir::ReturnCleanupAbi::DynamicBit + } else { + hir::ReturnCleanupAbi::None + } +} + fn body_test_named_function( name: &str, body: hir::Block, @@ -5117,6 +5253,7 @@ fn body_test_named_function( ret, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: body_test_return_cleanup(ret), locals, body, span: align_span::Span::new(0, 0, 0), @@ -10709,6 +10846,7 @@ fn hir_body_validator_generated_callables() { ret: integer, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }); program.fns.push(body_unit_case("generated_source", body_test_expr(hir::ExprKind::Unit, Ty::Unit))); let mut lifted = body_test_parameter_function( @@ -10881,6 +11019,7 @@ fn hir_body_validator_generated_callables() { ret: integer, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }); program.fns.push(body_test_parameter_function( "generated_pipeline_extern", @@ -10985,6 +11124,7 @@ fn hir_body_validator_native_control_flow() { ret: Ty::Unit, return_borrow: ReturnBorrowSummary::None, return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }); program.fns.push(body_tail_case( "native_control_exit", @@ -11850,6 +11990,7 @@ fn hir_body_type_mangle_golden_vectors() { params: vec![0], captures: Vec::new(), }, + return_cleanup: hir::ReturnCleanupAbi::None, effect: Cell::new(FnEffect::Pure), }; assert_eq!( diff --git a/crates/align_parser/src/lib.rs b/crates/align_parser/src/lib.rs index ae885508..ab69f57c 100644 --- a/crates/align_parser/src/lib.rs +++ b/crates/align_parser/src/lib.rs @@ -768,8 +768,29 @@ impl<'a> Parser<'a> { self.expect(&TokKind::LParen, "'('"); let mut params = Vec::new(); while !self.at(&TokKind::RParen) && !self.at(&TokKind::Eof) { - let mode = if self.eat_ident_keyword("out") { + // Parameter modes are weak keywords. Treat them as modes only when the complete + // mode + name + `:` shape is present, so `out: region` and `borrow: T` remain valid + // by-value parameter declarations. + let has_out_mode = matches!(self.peek(), TokKind::Ident(name) if name == "out") + && matches!(self.peek_at(1), TokKind::Ident(_)) + && matches!(self.peek_at(2), TokKind::Colon); + let has_borrow_mode = matches!(self.peek(), TokKind::Ident(name) if name == "borrow") + && matches!(self.peek_at(1), TokKind::Ident(_)) + && matches!(self.peek_at(2), TokKind::Colon); + let has_borrow_mut_mode = matches!(self.peek(), TokKind::Ident(name) if name == "borrow") + && matches!(self.peek_at(1), TokKind::Mut) + && matches!(self.peek_at(2), TokKind::Ident(_)) + && matches!(self.peek_at(3), TokKind::Colon); + let mode = if has_out_mode { + self.bump(); ParamMode::Out + } else if has_borrow_mut_mode { + self.bump(); + self.bump(); + ParamMode::BorrowMut + } else if has_borrow_mode { + self.bump(); + ParamMode::Borrow } else { ParamMode::ByValue }; @@ -1816,9 +1837,22 @@ impl<'a> Parser<'a> { // after it; otherwise `fn(out) -> T` continues to name the by-value type `out`. let has_out_mode = matches!(self.peek(), TokKind::Ident(name) if name == "out") && matches!(self.peek_at(1), TokKind::Fn | TokKind::LParen | TokKind::Ident(_)); + let borrow_type_follows = matches!(self.peek_at(1), TokKind::Fn | TokKind::LParen | TokKind::Ident(_)); + let has_borrow_mode = matches!(self.peek(), TokKind::Ident(name) if name == "borrow") + && borrow_type_follows; + let has_borrow_mut_mode = matches!(self.peek(), TokKind::Ident(name) if name == "borrow") + && matches!(self.peek_at(1), TokKind::Mut) + && matches!(self.peek_at(2), TokKind::Fn | TokKind::LParen | TokKind::Ident(_)); let mode = if has_out_mode { self.bump(); ParamMode::Out + } else if has_borrow_mut_mode { + self.bump(); + self.bump(); + ParamMode::BorrowMut + } else if has_borrow_mode { + self.bump(); + ParamMode::Borrow } else { ParamMode::ByValue }; @@ -1889,15 +1923,6 @@ impl<'a> Parser<'a> { } } - /// Consume a weak keyword (one that appears as an `Ident`), like `out`. - fn eat_ident_keyword(&mut self, kw: &str) -> bool { - if let TokKind::Ident(name) = self.peek() - && name == kw { - self.bump(); - return true; - } - false - } } #[cfg(test)] @@ -2023,6 +2048,32 @@ mod tests { )); } + #[test] + fn borrowed_parameter_modes_are_contextual() { + let (file, errors) = parse( + "borrow { value: i64 }\nfn modes(borrow owner: borrow, borrow mut copy: borrow, borrow: borrow, out: borrow) -> i64 = 0\nfn indirect(f: fn(borrow borrow, borrow mut borrow) -> i64) -> i64 = 0\n", + ); + assert!(!errors); + let Item::Fn(modes) = &file.items[1] else { panic!("expected modes") }; + assert_eq!( + modes.params.iter().map(|parameter| parameter.mode).collect::>(), + [ + ParamMode::Borrow, + ParamMode::BorrowMut, + ParamMode::ByValue, + ParamMode::ByValue, + ] + ); + assert_eq!(modes.params[2].name.name, "borrow"); + assert_eq!(modes.params[3].name.name, "out"); + let Item::Fn(indirect) = &file.items[2] else { panic!("expected indirect") }; + let Type::Fn { params, .. } = &indirect.params[0].ty else { + panic!("expected function type") + }; + assert_eq!(params[0].mode, ParamMode::Borrow); + assert_eq!(params[1].mode, ParamMode::BorrowMut); + } + #[test] fn template_splits_holes_and_keeps_bad_braces_literal() { use crate::RawPart; diff --git a/crates/align_sema/src/hir.rs b/crates/align_sema/src/hir.rs index 36ee4ea3..084ba4c0 100644 --- a/crates/align_sema/src/hir.rs +++ b/crates/align_sema/src/hir.rs @@ -13,6 +13,14 @@ use align_span::Span; /// Identifier of a local variable (and its memory slot) within a function body. pub type LocalId = u32; +/// Physical ownership result carried by an Align call. Copy returns have no extra ABI value; +/// recursively Move returns carry the path-selected cleanup bit beside the returned value. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ReturnCleanupAbi { + None, + DynamicBit, +} + /// The overflow handling of an explicit-overflow integer op ([`ExprKind::IntArith`]). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ArithMode { @@ -64,6 +72,7 @@ pub struct ExternFn { pub ret: crate::Ty, pub return_borrow: ReturnBorrowSummary, pub return_region: ReturnRegionSummary, + pub return_cleanup: ReturnCleanupAbi, } #[derive(Clone, Debug)] @@ -126,6 +135,7 @@ pub struct ImportedFn { pub return_provenance_known: bool, pub return_borrow: ReturnBorrowSummary, pub return_region: ReturnRegionSummary, + pub return_cleanup: ReturnCleanupAbi, /// 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, @@ -146,6 +156,8 @@ pub struct FnTy { /// Inputs/captures whose allocation region may own the returned value. L2a records `None`; /// L2b computes roots. pub return_region: ReturnRegionSummary, + /// Whether calls return only the value or the value plus a path-selected cleanup bit. + pub return_cleanup: ReturnCleanupAbi, /// Inferred observable effect of invoking a value of this type. This is internal type /// information: source annotations remain `fn(T) -> R`, while the checker refines the bit from /// each value's origin and conservatively joins mutable assignments. `Unknown` is fail-closed @@ -281,6 +293,7 @@ pub struct Fn { pub ret: Ty, pub return_borrow: ReturnBorrowSummary, pub return_region: ReturnRegionSummary, + pub return_cleanup: ReturnCleanupAbi, /// All locals (params + `let` bindings), indexed by [`LocalId`]. Each is a slot. pub locals: Vec, pub body: Block, diff --git a/crates/align_sema/src/hir_depth.rs b/crates/align_sema/src/hir_depth.rs index 9157e35f..072b49ad 100644 --- a/crates/align_sema/src/hir_depth.rs +++ b/crates/align_sema/src/hir_depth.rs @@ -1344,6 +1344,7 @@ mod tests { ret: int_ty(), return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body: Block { stmts: Vec::new(), @@ -1402,6 +1403,7 @@ mod tests { ret: result_ty, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::DynamicBit, locals: vec![hir::Local { id: 0, name: "value".to_string(), @@ -1470,6 +1472,7 @@ mod tests { ret: Ty::String, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::DynamicBit, locals: vec![hir::Local { id: 0, name: "value".to_string(), @@ -1540,6 +1543,11 @@ mod tests { ret, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: if ret == Ty::String { + hir::ReturnCleanupAbi::DynamicBit + } else { + hir::ReturnCleanupAbi::None + }, locals: vec![hir::Local { id: 0, name: "value".to_string(), @@ -1963,6 +1971,11 @@ mod tests { ret, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: if ret == Ty::String { + hir::ReturnCleanupAbi::DynamicBit + } else { + hir::ReturnCleanupAbi::None + }, locals: vec![hir::Local { id: 0, name: "value".to_string(), @@ -2131,15 +2144,23 @@ mod tests { assert!(checked_hir_body_depth_is_valid(&move_program)); let mut diagnostics = crate::Diagnostics::new(); let named_return_borrow = std::collections::HashMap::new(); + let named_param_modes = std::collections::HashMap::new(); + let callable_targets = + vec![crate::CallableTargetSet::new(); move_program.fn_types.len()]; + let callable_target_ids = std::collections::HashMap::new(); crate::MoveCheck { f: &move_program.fns[0], diags: &mut diagnostics, named_return_borrow: &named_return_borrow, + named_param_modes: &named_param_modes, summary_dependencies: None, tuples: &move_program.tuples, structs: &move_program.structs, enums: &move_program.enums, tagged_types: &move_program.tagged_types, + fn_types: &move_program.fn_types, + callable_targets: &callable_targets, + callable_target_ids: &callable_target_ids, loop_breaks: Vec::new(), borrows: crate::BorrowState::default(), next_pipeline_snapshot: 0, @@ -2173,6 +2194,7 @@ mod tests { f: function, diags: &mut diagnostics, named_return_region: &named_return_region, + fn_types: &program.fn_types, tuples: &program.tuples, structs: &program.structs, enums: &program.enums, diff --git a/crates/align_sema/src/lib.rs b/crates/align_sema/src/lib.rs index 07a1da1c..af573ff9 100644 --- a/crates/align_sema/src/lib.rs +++ b/crates/align_sema/src/lib.rs @@ -1615,7 +1615,13 @@ fn borrow_leaf_paths_for_type( let ty = expand_tagged_ty(ty, tagged_types); let aggregate = matches!( ty, - Ty::Struct(_) | Ty::Tuple(_) | Ty::Enum(_) | Ty::Option(_) | Ty::Result(..) + Ty::Struct(_) + | Ty::Tuple(_) + | Ty::Array(..) + | Ty::StructArray(..) + | Ty::Enum(_) + | Ty::Option(_) + | Ty::Result(..) ); if aggregate && !visiting.insert(ty) { // Malformed cycles retain a conservative whole-value leaf at the cycle edge. @@ -1652,6 +1658,16 @@ fn borrow_leaf_paths_for_type( )); } } + Ty::Array(element, len) => { + children.extend((0..len).map(|index| { + (BorrowProjection::ArrayElement(index), scalar_to_ty(element)) + })); + } + Ty::StructArray(id, len) => { + children.extend((0..len).map(|index| { + (BorrowProjection::ArrayElement(index), Ty::Struct(id)) + })); + } Ty::Enum(id) => { if let Some(definition) = enums.get(id as usize) { for (variant_index, variant) in @@ -2147,6 +2163,7 @@ struct FnSig { json_scan_return_spelling: Option, return_borrow: hir::ReturnBorrowSummary, return_region: hir::ReturnRegionSummary, + return_cleanup: hir::ReturnCleanupAbi, /// Generic type-parameter names (`fn f` → `["T", "U"]`); empty for a non-generic fn. /// The `params`/`ret` types may contain `Ty::Param(i)` indexing into this list. type_params: Vec, @@ -3317,7 +3334,14 @@ pub fn check_program(modules: &[Module], diags: &mut Diagnostics) -> Program { /// The imported return-provenance facts of non-generic public functions, keyed by canonical /// (mangled) name. The interface codec validates every root before constructing this map. pub type ExternalReturnProvenance = - std::collections::HashMap; + std::collections::HashMap< + String, + ( + hir::ReturnBorrowSummary, + hir::ReturnRegionSummary, + hir::ReturnCleanupAbi, + ), + >; /// M15 S1b compatibility entry point. L2b callers that reconstruct interface-only dependencies use /// [`check_program_with_interface_facts`] so imported return provenance is preserved as well. @@ -4352,6 +4376,7 @@ pub fn check_program_with_interface_facts( json_scan_return_spelling: f.ret.as_ref().and_then(json_scan_row_source_spelling), return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, type_params: tparams, bounds, is_extern: false, @@ -4362,10 +4387,11 @@ pub fn check_program_with_interface_facts( // Synthesized interface source cannot spell compiler-owned provenance facts. Restore those // facts after signature collection. The driver supplies the complete transitive fact map, so // entries outside the modules visible to this check are intentionally ignored. - for (name, (return_borrow, return_region)) in external_return_provenance { + for (name, (return_borrow, return_region, return_cleanup)) in external_return_provenance { if let Some(sig) = sigs.get_mut(name) { sig.return_borrow = return_borrow.clone(); sig.return_region = return_region.clone(); + sig.return_cleanup = *return_cleanup; } } @@ -4490,6 +4516,7 @@ pub fn check_program_with_interface_facts( json_scan_return_spelling: None, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, type_params: Vec::new(), bounds: Vec::new(), is_extern: true, @@ -4506,6 +4533,7 @@ pub fn check_program_with_interface_facts( ret, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, }); } } @@ -4553,6 +4581,19 @@ 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 expected_cleanup = return_cleanup_abi( + sig.ret, + &structs, + &tuples, + &enums, + &tagged_types, + ); + if sig.return_cleanup != expected_cleanup { + diags.error( + "imported return cleanup ABI disagrees with its return type".to_owned(), + f.span, + ); + } let return_provenance_known = external_return_provenance.contains_key(&mangled); let effect = external_effects @@ -4567,6 +4608,7 @@ pub fn check_program_with_interface_facts( return_provenance_known, return_borrow: sig.return_borrow.clone(), return_region: sig.return_region.clone(), + return_cleanup: sig.return_cleanup, effect, }); } @@ -4773,6 +4815,8 @@ pub fn check_program_with_interface_facts( fn_types, imported_fns, }; + assign_return_cleanup_abi(&mut program); + prepare_local_fn_types(&mut program); infer_return_provenance(&mut program); run_body_analysis_passes( &mut program, @@ -4783,6 +4827,50 @@ pub fn check_program_with_interface_facts( program } +fn return_cleanup_abi( + ty: Ty, + structs: &[hir::StructDef], + tuples: &[hir::TupleDef], + enums: &[hir::EnumDef], + tagged_types: &[hir::TaggedType], +) -> hir::ReturnCleanupAbi { + if needs_drop_flag(ty, structs, tuples, enums, tagged_types) { + hir::ReturnCleanupAbi::DynamicBit + } else { + hir::ReturnCleanupAbi::None + } +} + +fn assign_return_cleanup_abi(program: &mut Program) { + let Program { + fns, + externs, + imported_fns, + structs, + tuples, + enums, + tagged_types, + fn_types, + .. + } = program; + for function in fns { + function.return_cleanup = + return_cleanup_abi(function.ret, structs, tuples, enums, tagged_types); + } + for function in externs { + function.return_cleanup = + return_cleanup_abi(function.ret, structs, tuples, enums, tagged_types); + } + for function in imported_fns { + function.return_cleanup = + return_cleanup_abi(function.ret, structs, tuples, enums, tagged_types); + } + for function in fn_types { + function.return_cleanup = + return_cleanup_abi(function.ret, structs, tuples, enums, tagged_types); + } +} + fn run_body_analysis_passes( program: &mut Program, external_effects: &std::collections::HashMap, @@ -4813,6 +4901,18 @@ fn run_body_analysis_passes( .map(|function| (function.name.clone(), function.return_region.clone())), ) .collect(); + let named_param_modes: std::collections::HashMap> = program + .fns + .iter() + .map(|function| (function.name.clone(), function.param_modes.clone())) + .chain( + program + .imported_fns + .iter() + .map(|function| (function.name.clone(), function.param_modes.clone())), + ) + .collect(); + let callable = infer_fn_value_return_provenance(program, &named_return_borrow); // Pass 3 (partial): move / use-after-move checking + arena escape checking // (`03-types.md` §6–§7), then derive the per-function drop set (MMv2 slice 4). // Destructure so the flow analyses can read `tuples` (a tuple may be region-tracked when it @@ -4823,22 +4923,28 @@ fn run_body_analysis_passes( structs, enums, tagged_types, + fn_types, .. } = program; let tuples: &[hir::TupleDef] = tuples; let structs: &[StructDef] = structs; let enums: &[hir::EnumDef] = enums; let tagged_types: &[hir::TaggedType] = tagged_types; + let fn_types: &[hir::FnTy] = fn_types; for f in fns.iter_mut() { MoveCheck { f, diags, named_return_borrow: &named_return_borrow, + named_param_modes: &named_param_modes, summary_dependencies: None, tuples, structs, enums, tagged_types, + fn_types, + callable_targets: &callable.targets_by_type, + callable_target_ids: &callable.target_ids, loop_breaks: Vec::new(), borrows: BorrowState::default(), next_pipeline_snapshot: 0, @@ -4863,6 +4969,7 @@ fn run_body_analysis_passes( f, diags, named_return_region: &named_return_region, + fn_types, tuples, structs, enums, @@ -4888,12 +4995,23 @@ fn run_body_analysis_passes( // write iff that path installed an individually owned value, and cleared for arena values, // moves, and uninitialised paths. This lets one slot safely hold Arena and Static values on // different paths without either freeing arena memory or leaking heap memory. + let borrowed_params = f + .params + .iter() + .copied() + .zip(f.param_modes.iter().copied()) + .filter_map(|(local, mode)| { + matches!(mode, ast::ParamMode::Borrow | ast::ParamMode::BorrowMut) + .then_some(local) + }) + .collect::>(); let drops: Vec = f .locals .iter() .filter(|l| { - is_owned_droppable(l.ty, structs, enums, tagged_types) - || ty_tuple_is_move(l.ty, tuples) + !borrowed_params.contains(&l.id) + && (is_owned_droppable(l.ty, structs, enums, tagged_types) + || ty_tuple_is_move(l.ty, tuples)) }) .map(|l| l.id) .collect(); @@ -4996,7 +5114,9 @@ fn reset_body_analysis_facts(program: &mut Program) { } } } - for function in &program.fn_types { + for function in &mut program.fn_types { + function.return_borrow = hir::ReturnBorrowSummary::None; + function.return_region = hir::ReturnRegionSummary::None; function.effect.set(FnEffect::Unknown); } } @@ -5014,6 +5134,7 @@ fn body_analysis_facts_equal(expected: &hir::Program, actual: &Program) -> bool || expected.ret != actual.ret || expected.return_borrow != actual.return_borrow || expected.return_region != actual.return_region + || expected.return_cleanup != actual.return_cleanup }) { return false; @@ -5039,6 +5160,7 @@ fn body_analysis_facts_equal(expected: &hir::Program, actual: &Program) -> bool }) || expected.return_borrow != actual.return_borrow || expected.return_region != actual.return_region + || expected.return_cleanup != actual.return_cleanup || expected.drop_locals != actual.drop_locals || expected.drop_individual_locals != actual.drop_individual_locals || expected.drop_individual_exprs != actual.drop_individual_exprs @@ -5073,11 +5195,13 @@ fn assignment_facts(function: &hir::Fn) -> Vec<(hir::LocalId, bool, bool)> { .collect() } -fn summary_from_roots(roots: &BorrowRoots) -> hir::ReturnBorrowSummary { +fn summary_from_roots(roots: &BorrowRoots, explicit_params: u32) -> hir::ReturnBorrowSummary { let mut params = Vec::new(); + let mut captures = Vec::new(); for root in roots { match root { - BorrowRoot::Param(index) => params.push(*index), + BorrowRoot::Param(index) if *index < explicit_params => params.push(*index), + BorrowRoot::Param(index) => captures.push(index - explicit_params), BorrowRoot::Local(_) | BorrowRoot::IterTemp(_) | BorrowRoot::EndedLocal(_, _) @@ -5085,16 +5209,399 @@ fn summary_from_roots(roots: &BorrowRoots) -> hir::ReturnBorrowSummary { | BorrowRoot::EndedParam(_, _) => {} } } - if params.is_empty() { + if params.is_empty() && captures.is_empty() { hir::ReturnBorrowSummary::None } else { hir::ReturnBorrowSummary::Roots { params, - captures: Vec::new(), + captures, } } } +fn join_return_summary( + left: &hir::ReturnBorrowSummary, + right: &hir::ReturnBorrowSummary, +) -> hir::ReturnBorrowSummary { + let mut params = std::collections::BTreeSet::new(); + let mut captures = std::collections::BTreeSet::new(); + for summary in [left, right] { + if let hir::ReturnBorrowSummary::Roots { + params: incoming_params, + captures: incoming_captures, + } = summary + { + params.extend(incoming_params.iter().copied()); + captures.extend(incoming_captures.iter().copied()); + } + } + if params.is_empty() && captures.is_empty() { + hir::ReturnBorrowSummary::None + } else { + hir::ReturnBorrowSummary::Roots { + params: params.into_iter().collect(), + captures: captures.into_iter().collect(), + } + } +} + +fn fn_type_id_for_expr(expression: &Expr, locals: &[Local]) -> Option { + let ty = match &expression.kind { + ExprKind::Local(local) => locals.get(*local as usize).map(|local| local.ty)?, + _ => expression.ty, + }; + let Ty::Fn(id) = ty else { return None }; + Some(id) +} + +fn struct_path_type(root: Ty, path: &[u32], structs: &[StructDef]) -> Option { + let mut ty = root; + for &field in path { + let Ty::Struct(id) = ty else { return None }; + ty = structs + .get(id as usize)? + .fields + .get(field as usize)? + .ty; + } + Some(ty) +} + +type CallableTargetSet = std::collections::BTreeMap; + +#[derive(Clone, PartialEq, Eq)] +struct CallableProvenance { + target_ids: std::collections::HashMap, + targets_by_type: Vec, +} + +fn fn_type_targets( + expression: &Expr, + locals: &[Local], + targets: &[CallableTargetSet], +) -> CallableTargetSet { + fn_type_id_for_expr(expression, locals) + .and_then(|id| targets.get(id as usize)) + .cloned() + .unwrap_or_default() +} + +fn join_fn_type_targets( + targets: &mut [CallableTargetSet], + id: u32, + incoming: &CallableTargetSet, +) -> bool { + let Some(current) = targets.get_mut(id as usize) else { + return false; + }; + let before = current.clone(); + for (&target, summary) in incoming { + current + .entry(target) + .and_modify(|existing| *existing = join_return_summary(existing, summary)) + .or_insert_with(|| summary.clone()); + } + *current != before +} + +/// Refine concrete function-value origins and mutable locals to a finite may-union of target +/// relative parameter/capture summaries. The same summary record travels with a moved local; a +/// closure's capture indices remain relative to its own environment. +fn infer_fn_value_return_provenance( + program: &mut Program, + named: &std::collections::HashMap, +) -> CallableProvenance { + let mut names = program + .fns + .iter() + .map(|function| function.name.clone()) + .chain(program.imported_fns.iter().map(|function| function.name.clone())) + .collect::>(); + names.sort(); + names.dedup(); + let target_ids = names + .into_iter() + .enumerate() + .filter_map(|(index, name)| u32::try_from(index).ok().map(|index| (name, index))) + .collect::>(); + let mut targets = vec![CallableTargetSet::new(); program.fn_types.len()]; + let max_passes = program + .fn_types + .len() + .saturating_add(program.fns.iter().map(|function| function.locals.len()).sum()) + .saturating_add(1); + for _ in 0..max_passes { + let mut changed = false; + for function in &program.fns { + let events = hir_depth::body_events(&function.body); + for expression in events.iter().filter_map(|event| match event { + hir_depth::BodyEvent::ExprExit { expression, .. } => Some(*expression), + _ => None, + }) { + let Some(destination) = fn_type_id_for_expr(expression, &function.locals) else { + continue; + }; + let incoming = match &expression.kind { + ExprKind::FnValue(target) | ExprKind::Closure { lifted: target, .. } => { + let mut targets = CallableTargetSet::new(); + if let Some(&id) = target_ids.get(target) { + targets.insert( + id, + named + .get(target) + .cloned() + .unwrap_or(hir::ReturnBorrowSummary::None), + ); + } + targets + } + ExprKind::Local(local) => function + .locals + .get(*local as usize) + .and_then(|local| match local.ty { + Ty::Fn(id) => targets.get(id as usize), + _ => None, + }) + .cloned() + .unwrap_or_default(), + _ => hir_depth::direct_expr_children(expression).into_iter().fold( + CallableTargetSet::new(), + |mut joined, child| { + let child = fn_type_targets(child, &function.locals, &targets); + for (target, summary) in child { + joined + .entry(target) + .and_modify(|existing| { + *existing = join_return_summary(existing, &summary) + }) + .or_insert(summary); + } + joined + }, + ), + }; + changed |= join_fn_type_targets(&mut targets, destination, &incoming); + } + for expression in events.iter().filter_map(|event| match event { + hir_depth::BodyEvent::ExprExit { expression, .. } => Some(*expression), + _ => None, + }) { + match &expression.kind { + ExprKind::StructLit { struct_id, fields } => { + if let Some(definition) = program.structs.get(*struct_id as usize) { + for (field, value) in definition.fields.iter().zip(fields) { + if let Ty::Fn(destination) = field.ty { + let incoming = + fn_type_targets(value, &function.locals, &targets); + changed |= join_fn_type_targets( + &mut targets, + destination, + &incoming, + ); + } + } + } + } + ExprKind::Tuple { tuple_id, elems } => { + if let Some(definition) = program.tuples.get(*tuple_id as usize) { + for (element, value) in definition.elems.iter().zip(elems) { + if let Scalar::Fn(destination) = element { + let incoming = + fn_type_targets(value, &function.locals, &targets); + changed |= join_fn_type_targets( + &mut targets, + *destination, + &incoming, + ); + } + } + } + } + ExprKind::ArrayLit { elems, elem: Ty::Fn(destination), .. } => { + for value in elems { + let incoming = fn_type_targets(value, &function.locals, &targets); + changed |= join_fn_type_targets( + &mut targets, + *destination, + &incoming, + ); + } + } + ExprKind::OptionSome(value) => { + if let Ty::Option(Scalar::Fn(destination)) = expression.ty { + let incoming = fn_type_targets(value, &function.locals, &targets); + changed |= join_fn_type_targets( + &mut targets, + destination, + &incoming, + ); + } + } + ExprKind::ResultOk(value) => { + if let Ty::Result(Scalar::Fn(destination), _) = expression.ty { + let incoming = fn_type_targets(value, &function.locals, &targets); + changed |= join_fn_type_targets( + &mut targets, + destination, + &incoming, + ); + } + } + ExprKind::ResultErr(value) => { + if let Ty::Result(_, Scalar::Fn(destination)) = expression.ty { + let incoming = fn_type_targets(value, &function.locals, &targets); + changed |= join_fn_type_targets( + &mut targets, + destination, + &incoming, + ); + } + } + ExprKind::EnumValue { + enum_id, + variant, + payload, + } => { + if let Some(case) = program + .enums + .get(*enum_id as usize) + .and_then(|definition| definition.variants.get(*variant as usize)) + { + for (element, value) in case.payload.iter().zip(payload) { + if let Scalar::Fn(destination) = element { + let incoming = + fn_type_targets(value, &function.locals, &targets); + changed |= join_fn_type_targets( + &mut targets, + *destination, + &incoming, + ); + } + } + } + } + _ => {} + } + } + for event in events { + let (local, value) = match event { + hir_depth::BodyEvent::StmtEnter(Stmt::Let { local, init, .. }) + | hir_depth::BodyEvent::StmtEnter(Stmt::Assign { local, value: init, .. }) => { + (*local, init) + } + hir_depth::BodyEvent::StmtEnter(Stmt::AssignField { + root, + path, + value, + }) => { + let Some(root_ty) = + function.locals.get(*root as usize).map(|local| local.ty) + else { + continue; + }; + if let Some(Ty::Fn(destination)) = + struct_path_type(root_ty, path, &program.structs) + { + let incoming = fn_type_targets(value, &function.locals, &targets); + changed |= join_fn_type_targets( + &mut targets, + destination, + &incoming, + ); + } + continue; + } + hir_depth::BodyEvent::StmtEnter(Stmt::AssignElemField { + struct_id, + path, + value, + .. + }) => { + if let Some(Ty::Fn(destination)) = + struct_path_type(Ty::Struct(*struct_id), path, &program.structs) + { + let incoming = fn_type_targets(value, &function.locals, &targets); + changed |= join_fn_type_targets( + &mut targets, + destination, + &incoming, + ); + } + continue; + } + hir_depth::BodyEvent::StmtEnter(Stmt::AssignIndex { base, value, .. }) => { + let destination = function + .locals + .get(*base as usize) + .and_then(|local| match local.ty { + Ty::Array(Scalar::Fn(id), _) => Some(id), + _ => None, + }); + if let Some(destination) = destination { + let incoming = fn_type_targets(value, &function.locals, &targets); + changed |= join_fn_type_targets( + &mut targets, + destination, + &incoming, + ); + } + continue; + } + hir_depth::BodyEvent::StmtEnter(Stmt::LetTuple { locals, init, .. }) => { + let Ty::Tuple(tuple) = init.ty else { continue }; + let Some(definition) = program.tuples.get(tuple as usize) else { + continue; + }; + for (index, local) in locals.iter().enumerate() { + let Some(local) = local else { continue }; + let Some(Ty::Fn(destination)) = function + .locals + .get(*local as usize) + .map(|local| local.ty) + else { + continue; + }; + let Some(Scalar::Fn(source)) = definition.elems.get(index) else { + continue; + }; + let incoming = targets.get(*source as usize).cloned().unwrap_or_default(); + changed |= join_fn_type_targets( + &mut targets, + destination, + &incoming, + ); + } + continue; + } + _ => continue, + }; + let Some(Ty::Fn(destination)) = + function.locals.get(local as usize).map(|local| local.ty) + else { + continue; + }; + let incoming = fn_type_targets(value, &function.locals, &targets); + changed |= join_fn_type_targets(&mut targets, destination, &incoming); + } + } + if !changed { + break; + } + } + for (function, target_set) in program.fn_types.iter_mut().zip(&targets) { + let summary = target_set.values().fold( + hir::ReturnBorrowSummary::None, + |joined, summary| join_return_summary(&joined, summary), + ); + function.return_region = borrow_to_region_summary(&summary); + function.return_borrow = summary; + } + CallableProvenance { + target_ids, + targets_by_type: targets, + } +} + fn borrow_to_region_summary(summary: &hir::ReturnBorrowSummary) -> hir::ReturnRegionSummary { match summary { hir::ReturnBorrowSummary::None => hir::ReturnRegionSummary::None, @@ -5105,11 +5612,11 @@ fn borrow_to_region_summary(summary: &hir::ReturnBorrowSummary) -> hir::ReturnRe } } -/// Infer the L2b-a1 named-function parameter roots before the ordinary move/escape diagnostic pass. +/// Infer named-function parameter roots before the ordinary move/escape diagnostic pass. /// Calls form a finite may-lattice over sorted root sets, so recursive functions converge -/// monotonically. Aggregate projections deliberately remain flattened until L2b-a2; capture roots -/// and function-value targets belong to L2b-b. The existing exhaustive borrow-provenance walker -/// remains the single expression classifier. +/// monotonically. The callable-provenance pass then refines target-relative parameter and capture +/// roots. The existing exhaustive borrow-provenance walker remains the single expression +/// classifier. fn infer_return_provenance(program: &mut Program) { let mut named: std::collections::HashMap = program .fns @@ -5134,15 +5641,22 @@ fn infer_return_provenance(program: &mut Program) { let mut queued = vec![true; function_count]; let mut worklist: std::collections::VecDeque = (0..function_count).collect(); - while let Some(index) = worklist.pop_front() { - queued[index] = false; - let function = &program.fns[index]; - // 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 matches!(function.origin, hir::FnOrigin::Lifted { .. }) { - continue; - } + let named_param_modes = program + .fns + .iter() + .map(|function| (function.name.clone(), function.param_modes.clone())) + .chain( + program + .imported_fns + .iter() + .map(|function| (function.name.clone(), function.param_modes.clone())), + ) + .collect::>(); + let mut callable = infer_fn_value_return_provenance(program, &named); + loop { + while let Some(index) = worklist.pop_front() { + queued[index] = false; + let function = &program.fns[index]; let mut dependencies = std::collections::HashSet::new(); let collect_dependencies = @@ -5152,11 +5666,15 @@ fn infer_return_provenance(program: &mut Program) { f: function, diags: &mut sink, named_return_borrow: &named, + named_param_modes: &named_param_modes, summary_dependencies: collect_dependencies, tuples: &program.tuples, structs: &program.structs, enums: &program.enums, tagged_types: &program.tagged_types, + fn_types: &program.fn_types, + callable_targets: &callable.targets_by_type, + callable_target_ids: &callable.target_ids, loop_breaks: Vec::new(), borrows: BorrowState::default(), next_pipeline_snapshot: 0, @@ -5185,19 +5703,46 @@ fn infer_return_provenance(program: &mut Program) { dependencies_recorded[index] = true; } - let summary = summary_from_roots(&roots); - if named.get(&function.name) == Some(&summary) { - continue; - } - named.insert(function.name.clone(), summary); - if let Some(callers) = reverse_callers.get(&function.name) { - for &caller in callers { - if !queued[caller] { - queued[caller] = true; - worklist.push_back(caller); + let capture_count = match function.origin { + hir::FnOrigin::Lifted { capture_count } => capture_count, + hir::FnOrigin::Source { .. } | hir::FnOrigin::Monomorph => 0, + }; + let explicit_params = (function.params.len() as u32).saturating_sub(capture_count); + let summary = summary_from_roots(&roots, explicit_params); + if named.get(&function.name) == Some(&summary) { + continue; + } + named.insert(function.name.clone(), summary); + if let Some(callers) = reverse_callers.get(&function.name) { + for &caller in callers { + if !queued[caller] { + queued[caller] = true; + worklist.push_back(caller); + } } } } + + let next_callable = infer_fn_value_return_provenance(program, &named); + if next_callable == callable { + break; + } + callable = next_callable; + for (index, function) in program.fns.iter().enumerate() { + let has_indirect_call = hir_depth::body_events(&function.body).into_iter().any(|event| { + matches!( + event, + hir_depth::BodyEvent::ExprEnter(Expr { + kind: ExprKind::CallFnValue { .. } | ExprKind::ResultMapErr { .. }, + .. + }) + ) + }); + if has_indirect_call && !queued[index] { + queued[index] = true; + worklist.push_back(index); + } + } } for function in &mut program.fns { @@ -5427,32 +5972,25 @@ fn effects_by_name(program: &Program, sets: &EffectSets) -> std::collections::Ha .collect() } -/// Infer the effect stored in every concrete function-value type. Named targets begin at the Pure -/// bottom of the effect lattice; value origins then refine their own `FnTy`, local bindings join all -/// assigned targets, and the call graph is recomputed until stable. Unknown function parameters and -/// fail-closed external targets seed the non-Pure cases. Starting at the bottom also solves a cycle -/// of otherwise-Pure function values as Pure instead of permanently pinning it Unknown. -fn infer_fn_type_effects( - program: &mut Program, - external_effects: &std::collections::HashMap, -) -> EffectSets { - // A local needs its own effect cell: two unrelated values with the same ABI must not poison one - // another merely because source-level `fn(T) -> R` annotations omit the inferred effect. +/// Give each concrete non-parameter function local an independent inference record. Return +/// provenance and effects share this identity so joining either fact can never contaminate an +/// unrelated value with the same written `fn(T) -> R` signature. +fn prepare_local_fn_types(program: &mut Program) { let Program { fns, fn_types, .. } = program; - for f in fns { - for local in &mut f.locals { + for function in fns { + for local in &mut function.locals { if local.is_param { - continue; // a function-typed parameter has no statically known target + continue; } let Ty::Fn(fid) = local.ty else { continue }; - let Some(ft) = fn_types.get(fid as usize) else { + let Some(source) = fn_types.get(fid as usize) else { continue; }; let (params, ret, return_borrow, return_region) = ( - ft.params.clone(), - ft.ret, - ft.return_borrow.clone(), - ft.return_region.clone(), + source.params.clone(), + source.ret, + source.return_borrow.clone(), + source.return_region.clone(), ); let fresh = fresh_fn_type(fn_types, params, ret, FnEffect::Unknown); fn_types[fresh as usize].return_borrow = return_borrow; @@ -5460,7 +5998,17 @@ fn infer_fn_type_effects( local.ty = Ty::Fn(fresh); } } +} +/// Infer the effect stored in every concrete function-value type. Named targets begin at the Pure +/// bottom of the effect lattice; value origins then refine their own `FnTy`, local bindings join all +/// assigned targets, and the call graph is recomputed until stable. Unknown function parameters and +/// fail-closed external targets seed the non-Pure cases. Starting at the bottom also solves a cycle +/// of otherwise-Pure function values as Pure instead of permanently pinning it Unknown. +fn infer_fn_type_effects( + program: &mut Program, + external_effects: &std::collections::HashMap, +) -> EffectSets { solve_fn_type_effects( program, external_effects, @@ -7880,10 +8428,10 @@ impl EffectScan<'_> { } } } - // A bound/moved function value no longer identifies the named parameter boundary - // that must receive a callback-bearing actual. Until L2b-b carries joined target - // roots through function values, such an invocation is legal sequentially but - // cannot prove the enclosing function Pure for `par_map`. + // A bound/moved function value with no single static target does not identify the + // named effect boundary that receives a callback-bearing actual. Such an + // invocation is legal sequentially but cannot prove the enclosing function Pure + // for `par_map`. if target.is_none() { self.unresolved_dispatches.push(( e.span, @@ -8729,6 +9277,9 @@ enum Region { Arena(u32), } +type CallableRegionFact = + std::collections::BTreeMap, Region>; + impl Region { /// Ordinal in the lattice; smaller = longer-lived. fn ord(self) -> u32 { @@ -8774,6 +9325,11 @@ impl Region { #[derive(Clone, Default, PartialEq, Eq)] struct EscapeState { region: std::collections::HashMap, + /// Region contributed to an indirect result by the selected callable's captured values. This + /// is distinct from `region`: a capturing closure value is frame-local because its environment + /// buffer lives in this frame, while a view returned from that environment may point into a + /// caller-owned `Static` parameter and remain returnable. + callable_capture_region: std::collections::HashMap, local_backed_slice: std::collections::HashSet, /// True only when every reaching path holds individually owned storage. individual: std::collections::HashMap, @@ -8792,6 +9348,15 @@ impl EscapeState { .and_modify(|current| *current = current.shorter(region)) .or_insert(region); } + for (&local, fact) in &other.callable_capture_region { + let current = joined.callable_capture_region.entry(local).or_default(); + for (path, ®ion) in fact { + current + .entry(path.clone()) + .and_modify(|current| *current = current.shorter(region)) + .or_insert(region); + } + } joined .local_backed_slice .extend(other.local_backed_slice.iter().copied()); @@ -8973,6 +9538,9 @@ struct EscapeCheck<'a> { diags: &'a mut Diagnostics, /// Settled same-program/imported return-region summaries for direct calls. named_return_region: &'a std::collections::HashMap, + /// Settled function-value signatures. Parameter roots select call arguments; capture roots are + /// resolved through `EscapeState::callable_capture_region`. + fn_types: &'a [hir::FnTy], /// Tuple defs (to decide whether a `Ty::Tuple` is region-tracked — true iff an element is). tuples: &'a [hir::TupleDef], /// Struct defs (to decide whether a `soa` has a `str` column — see `struct_has_str`). @@ -9022,6 +9590,30 @@ struct EscapeCheck<'a> { } impl<'a> EscapeCheck<'a> { + fn borrowed_param_place(&self, expression: &Expr) -> bool { + let root = match &expression.kind { + ExprKind::Local(local) => *local, + ExprKind::Field { root, .. } => *root, + _ => return false, + }; + self.f + .params + .iter() + .position(|¶meter| parameter == root) + .and_then(|position| self.f.param_modes.get(position)) + .is_some_and(|mode| { + matches!(mode, ast::ParamMode::Borrow | ast::ParamMode::BorrowMut) + }) + } + + fn borrowed_storage_cap(&self, expression: &Expr) -> Region { + if self.borrowed_param_place(expression) { + Region::Static + } else { + Region::Frame + } + } + fn check(&mut self) { for ¶m in &self.f.params { if self.f.locals.get(param as usize).is_some_and(|local| { @@ -10075,6 +10667,388 @@ impl<'a> EscapeCheck<'a> { struct_contains_str(id, self.structs, self.enums) } + fn callable_type_id(&self, expression: &Expr) -> Option { + let ty = match expression.kind { + ExprKind::Local(local) => self + .f + .locals + .get(local as usize) + .map(|local| local.ty) + .unwrap_or(expression.ty), + _ => expression.ty, + }; + match ty { + Ty::Fn(id) => Some(id), + _ => None, + } + } + + fn join_callable_region_fact( + mut left: CallableRegionFact, + right: CallableRegionFact, + ) -> CallableRegionFact { + for (path, region) in right { + left.entry(path) + .and_modify(|current| *current = current.shorter(region)) + .or_insert(region); + } + left + } + + fn prefix_callable_region_fact( + fact: CallableRegionFact, + projection: BorrowProjection, + ) -> CallableRegionFact { + fact.into_iter() + .map(|(mut path, region)| { + path.insert(0, projection); + (path, region) + }) + .collect() + } + + fn project_callable_region_fact( + fact: &CallableRegionFact, + path: &[BorrowProjection], + ) -> CallableRegionFact { + fact.iter() + .filter_map(|(candidate, ®ion)| { + candidate + .strip_prefix(path) + .map(|suffix| (suffix.to_vec(), region)) + }) + .collect() + } + + fn project_callable_array_elements(fact: &CallableRegionFact) -> CallableRegionFact { + let mut selected = CallableRegionFact::new(); + for (path, ®ion) in fact { + let Some((BorrowProjection::ArrayElement(_), suffix)) = path.split_first() else { + continue; + }; + selected + .entry(suffix.to_vec()) + .and_modify(|current| *current = current.shorter(region)) + .or_insert(region); + } + selected + } + + fn callable_region_fact(&self, expression: &Expr, depth: u32) -> CallableRegionFact { + let leaf = |region| [(Vec::new(), region)].into_iter().collect(); + match &expression.kind { + ExprKind::FnValue(_) => leaf(Region::Static), + ExprKind::Closure { lifted, captures } => { + let selected = match self.named_return_region.get(lifted) { + Some(hir::ReturnRegionSummary::None) => return leaf(Region::Static), + Some(hir::ReturnRegionSummary::Roots { captures, .. }) => { + Some(captures.as_slice()) + } + None => None, + }; + let region = captures + .iter() + .enumerate() + .filter(|(index, _)| { + selected.is_none_or(|selected| selected.contains(&(*index as u32))) + }) + .fold(Region::Static, |region, (_, capture)| { + region.shorter(self.region_of(capture, depth)) + }); + leaf(region) + } + ExprKind::Local(local) => self + .state + .callable_capture_region + .get(local) + .cloned() + .unwrap_or_else(|| { + // A function-valued parameter borrows caller storage, like a `str` parameter. + matches!(expression.ty, Ty::Fn(_)) + .then(|| leaf(Region::Static)) + .unwrap_or_default() + }), + ExprKind::Field { root, path } => { + let path = path + .iter() + .copied() + .map(BorrowProjection::StructField) + .collect::>(); + self.state + .callable_capture_region + .get(root) + .map_or_else(CallableRegionFact::new, |fact| { + Self::project_callable_region_fact(fact, &path) + }) + } + ExprKind::TupleIndex { recv, index } => Self::project_callable_region_fact( + &self.callable_region_fact(recv, depth), + &[BorrowProjection::TupleElement(*index)], + ), + ExprKind::Index { recv, index } => { + let fact = self.callable_region_fact(recv, depth); + match &index.kind { + ExprKind::Int(value) if *value >= 0 => Self::project_callable_region_fact( + &fact, + &[BorrowProjection::ArrayElement(*value as u32)], + ), + _ => Self::project_callable_array_elements(&fact), + } + } + ExprKind::IndexField { base, index, path } => { + let mut projections = vec![BorrowProjection::ArrayElement(*index)]; + projections.extend(path.iter().copied().map(BorrowProjection::StructField)); + self.state + .callable_capture_region + .get(base) + .map_or_else(CallableRegionFact::new, |fact| { + Self::project_callable_region_fact(fact, &projections) + }) + } + ExprKind::ElemField { recv, index, path, .. } => { + let fact = self.callable_region_fact(recv, depth); + let fact = match &index.kind { + ExprKind::Int(value) if *value >= 0 => Self::project_callable_region_fact( + &fact, + &[BorrowProjection::ArrayElement(*value as u32)], + ), + _ => Self::project_callable_array_elements(&fact), + }; + let path = path + .iter() + .copied() + .map(BorrowProjection::StructField) + .collect::>(); + Self::project_callable_region_fact(&fact, &path) + } + ExprKind::StructLit { fields, .. } => fields.iter().enumerate().fold( + CallableRegionFact::new(), + |fact, (index, field)| { + Self::join_callable_region_fact( + fact, + Self::prefix_callable_region_fact( + self.callable_region_fact(field, depth), + BorrowProjection::StructField(index as u32), + ), + ) + }, + ), + ExprKind::Tuple { elems, .. } => elems.iter().enumerate().fold( + CallableRegionFact::new(), + |fact, (index, element)| { + Self::join_callable_region_fact( + fact, + Self::prefix_callable_region_fact( + self.callable_region_fact(element, depth), + BorrowProjection::TupleElement(index as u32), + ), + ) + }, + ), + ExprKind::ArrayLit { elems, .. } => elems.iter().enumerate().fold( + CallableRegionFact::new(), + |fact, (index, element)| { + Self::join_callable_region_fact( + fact, + Self::prefix_callable_region_fact( + self.callable_region_fact(element, depth), + BorrowProjection::ArrayElement(index as u32), + ), + ) + }, + ), + ExprKind::EnumValue { variant, payload, .. } => payload.iter().enumerate().fold( + CallableRegionFact::new(), + |fact, (index, element)| { + Self::join_callable_region_fact( + fact, + Self::prefix_callable_region_fact( + self.callable_region_fact(element, depth), + BorrowProjection::EnumPayload { + variant: *variant, + index: index as u32, + }, + ), + ) + }, + ), + ExprKind::OptionSome(value) => Self::prefix_callable_region_fact( + self.callable_region_fact(value, depth), + BorrowProjection::OptionSome, + ), + ExprKind::OptionNone => CallableRegionFact::new(), + ExprKind::ResultOk(value) => Self::prefix_callable_region_fact( + self.callable_region_fact(value, depth), + BorrowProjection::ResultOk, + ), + ExprKind::ResultErr(value) => Self::prefix_callable_region_fact( + self.callable_region_fact(value, depth), + BorrowProjection::ResultErr, + ), + ExprKind::Try(result) => Self::project_callable_region_fact( + &self.callable_region_fact(result, depth), + &[BorrowProjection::ResultOk], + ), + ExprKind::ElseUnwrap { opt, fallback } => { + let projection = match expand_tagged_ty(opt.ty, self.tagged_types) { + Ty::Option(_) => Some(BorrowProjection::OptionSome), + Ty::Result(..) => Some(BorrowProjection::ResultOk), + _ => None, + }; + let success = projection.map_or_else(CallableRegionFact::new, |projection| { + Self::project_callable_region_fact( + &self.callable_region_fact(opt, depth), + &[projection], + ) + }); + Self::join_callable_region_fact( + success, + self.callable_region_fact(fallback, depth), + ) + } + ExprKind::Block(block) + | ExprKind::Unsafe(block) + | ExprKind::Arena(block) + | ExprKind::TaskGroup(block) => block.value.as_deref().map_or_else( + CallableRegionFact::new, + |value| self.callable_region_fact(value, depth), + ), + ExprKind::If { then, els, .. } => [then, els] + .into_iter() + .filter_map(|block| block.value.as_deref()) + .fold(CallableRegionFact::new(), |fact, value| { + Self::join_callable_region_fact( + fact, + self.callable_region_fact(value, depth), + ) + }), + ExprKind::Match { arms, .. } => arms.iter().fold( + CallableRegionFact::new(), + |fact, arm| { + Self::join_callable_region_fact( + fact, + self.callable_region_fact(&arm.body, depth), + ) + }, + ), + _ => CallableRegionFact::new(), + } + } + + /// Region of only the captured values that a callable may return. Do not use the closure + /// value's own region here: its environment header is frame-local, but invoking it copies the + /// selected returned view out of that header and does not return the header itself. + fn callable_capture_return_region(&self, expression: &Expr, depth: u32) -> Region { + self.callable_region_fact(expression, depth) + .get(&Vec::new()) + .copied() + .unwrap_or(Region::Static) + } + + fn indirect_return_region(&self, callee: &Expr, args: &[Expr], depth: u32) -> Region { + let Some(function) = self + .callable_type_id(callee) + .and_then(|id| self.fn_types.get(id as usize)) + else { + return args.iter().fold( + self.callable_capture_return_region(callee, depth), + |region, argument| region.shorter(self.region_of(argument, depth)), + ); + }; + match &function.return_region { + hir::ReturnRegionSummary::None => Region::Static, + hir::ReturnRegionSummary::Roots { params, captures } => { + let initial = if captures.is_empty() { + Region::Static + } else { + self.callable_capture_return_region(callee, depth) + }; + params.iter().fold(initial, |region, &index| { + args.get(index as usize).map_or(region, |argument| { + region.shorter(self.region_of(argument, depth)) + }) + }) + } + } + } + + fn replace_callable_region_path( + &mut self, + local: LocalId, + path: &[BorrowProjection], + value: &Expr, + depth: u32, + ) { + let incoming = self.callable_region_fact(value, depth); + let current = self + .state + .callable_capture_region + .entry(local) + .or_default(); + current.retain(|candidate, _| !candidate.starts_with(path)); + for (suffix, region) in incoming { + let mut destination = path.to_vec(); + destination.extend(suffix); + current.insert(destination, region); + } + if current.is_empty() { + self.state.callable_capture_region.remove(&local); + } + } + + fn replace_callable_array_path( + &mut self, + local: LocalId, + index: &Expr, + fields: &[u32], + value: &Expr, + depth: u32, + ) { + let length = self + .f + .locals + .get(local as usize) + .and_then(|local| match local.ty { + Ty::Array(_, length) | Ty::StructArray(_, length) => Some(length), + _ => None, + }); + let exact = match (&index.kind, length) { + (ExprKind::Int(value), Some(length)) + if *value >= 0 && (*value as u128) < length as u128 => + { + Some(*value as u32) + } + _ => None, + }; + let Some(index) = exact else { + // A dynamic write may replace any element. Keep every old target and join the incoming + // target at every fixed slot so no runtime-selected capture region is lost. + let Some(length) = length else { return }; + let incoming = self.callable_region_fact(value, depth); + let current = self + .state + .callable_capture_region + .entry(local) + .or_default(); + for index in 0..length { + let mut prefix = vec![BorrowProjection::ArrayElement(index)]; + prefix.extend(fields.iter().copied().map(BorrowProjection::StructField)); + for (suffix, ®ion) in &incoming { + let mut destination = prefix.clone(); + destination.extend(suffix); + current + .entry(destination) + .and_modify(|current| *current = current.shorter(region)) + .or_insert(region); + } + } + return; + }; + let mut path = vec![BorrowProjection::ArrayElement(index)]; + path.extend(fields.iter().copied().map(BorrowProjection::StructField)); + self.replace_callable_region_path(local, &path, value, depth); + } + /// The [`Region`] a region-bearing (`box`/`str`) value is bound to. `Static` = no region /// (a leaked/static str, a box param — none exist — etc.). Recurses through value forms so /// it can't slip out via an `if`/block value. @@ -10447,12 +11421,14 @@ impl<'a> EscapeCheck<'a> { | ExprKind::OptionSome(inner) | ExprKind::ResultOk(inner) | ExprKind::ResultErr(inner) => work.push(Work::Eval(inner, depth)), - // `map_err` passes the `Ok` payload through unchanged, while its mapped error may - // borrow the mapper closure's environment. The result can outlive neither source. - ExprKind::ResultMapErr { result, f } => push_fold( - &mut work, - Region::Static, - vec![(result, depth, None), (f, depth, None)], + // `map_err` passes the Ok payload through. Its mapped Err follows the mapper's settled + // argument/capture summary; the closure environment buffer itself is not returned. + ExprKind::ResultMapErr { result, f } => values.push( + self.region_of(result, depth).shorter(self.indirect_return_region( + f, + std::slice::from_ref(result.as_ref()), + depth, + )), ), // `opt else fb` yields one of two values, so it lives only as long as the shorter. ExprKind::ElseUnwrap { opt, fallback } => push_fold( @@ -10470,7 +11446,7 @@ impl<'a> EscapeCheck<'a> { // outlive that arena — taking the shorter keeps it sound for free. ExprKind::StrBorrow(inner) => push_fold( &mut work, - Region::Frame, + self.borrowed_storage_cap(inner), vec![(inner, depth, None)], ), // `str.bytes()` is a zero-copy re-view of exactly the same `{ptr,len}` storage, so it @@ -10491,7 +11467,7 @@ impl<'a> EscapeCheck<'a> { // frame exit), so — like `StrBorrow` — it is `Frame`-regioned and cannot escape the frame. ExprKind::BufferBytes { buffer } => push_fold( &mut work, - Region::Frame, + self.borrowed_storage_cap(buffer), vec![(buffer, depth, None)], ), // `bytes.as_str()` is a zero-copy `str` view of the SAME storage `bytes` viewed, so it @@ -10511,7 +11487,7 @@ impl<'a> EscapeCheck<'a> { // of a dropped `parsed` escape (the #297-class bug); `.clone()` copies out. ExprKind::CliGetStr { parsed, .. } => push_fold( &mut work, - Region::Frame, + self.borrowed_storage_cap(parsed), vec![(parsed, depth, None)], ), // `resp.header(name)` returns `Option` and `resp.body()` a `slice`, both **views** @@ -10522,7 +11498,7 @@ impl<'a> EscapeCheck<'a> { ExprKind::HttpRespHeader { resp, .. } | ExprKind::HttpRespBody { resp } => { push_fold( &mut work, - Region::Frame, + self.borrowed_storage_cap(resp), vec![(resp, depth, None)], ); } @@ -10533,7 +11509,7 @@ impl<'a> EscapeCheck<'a> { ExprKind::RunOutputStdout { out } | ExprKind::RunOutputStderr { out } => { push_fold( &mut work, - Region::Frame, + self.borrowed_storage_cap(out), vec![(out, depth, None)], ); } @@ -10548,7 +11524,7 @@ impl<'a> EscapeCheck<'a> { | ExprKind::HttpCtxBody { ctx } | ExprKind::HttpCtxHeaders { ctx } => push_fold( &mut work, - Region::Frame, + self.borrowed_storage_cap(ctx), vec![(ctx, depth, None)], ), // `hs.get(name)` **inherits** its receiver's region instead of re-capping at `Frame` — the @@ -10569,7 +11545,7 @@ impl<'a> EscapeCheck<'a> { ExprKind::ConnReader { conn } | ExprKind::ConnWriter { conn } => { push_fold( &mut work, - Region::Frame, + self.borrowed_storage_cap(conn), vec![(conn, depth, None)], ); } @@ -10673,17 +11649,12 @@ impl<'a> EscapeCheck<'a> { .map(|arm| (&arm.body, depth, None)) .collect(), ), - // An indirect call's result may borrow one of its arguments (`g := id; g(s)`) or its - // closure environment (`f := fn { captured_view }; f()`). It therefore lives no longer - // than either the callee or the shortest-lived argument. Without the callee fold, a - // zero-argument closure can return an arena capture after that arena is freed. - ExprKind::CallFnValue { callee, args } => push_fold( - &mut work, - Region::Static, - std::iter::once((callee.as_ref(), depth, None)) - .chain(args.iter().map(|argument| (argument, depth, None))) - .collect(), - ), + // Resolve the settled function-value summary. The closure environment itself is + // frame-local, but only selected capture values (plus selected explicit arguments) + // back the returned view. + ExprKind::CallFnValue { callee, args } => { + values.push(self.indirect_return_region(callee, args, depth)); + } // `arr[const].field` reads a field of a struct-array element; a `str` field is a view // into the array's storage, so it inherits the array's region (like `ElemField`). ExprKind::IndexField { base, .. } => values.push( @@ -11677,6 +12648,12 @@ impl<'a> EscapeCheck<'a> { match s { Stmt::Let { local, init } => { self.decl_depth.insert(*local, depth); + let callable = self.callable_region_fact(init, depth); + if callable.is_empty() { + self.state.callable_capture_region.remove(local); + } else { + self.state.callable_capture_region.insert(*local, callable); + } if is_owned_droppable(init.ty, self.structs, self.enums, self.tagged_types) || ty_tuple_is_move(init.ty, self.tuples) { @@ -11770,9 +12747,38 @@ impl<'a> EscapeCheck<'a> { ); } } + match s { + Stmt::AssignIndex { base, index, value } => { + self.replace_callable_array_path(*base, index, &[], value, depth); + } + Stmt::AssignElemField { + base, + index, + path, + value, + .. + } => { + self.replace_callable_array_path(*base, index, path, value, depth); + } + Stmt::AssignElem { + base, + index, + value, + .. + } => { + self.replace_callable_array_path(*base, index, &[], value, depth); + } + _ => unreachable!("array assignment group"), + } } Stmt::AssignVecLane { .. } => {} Stmt::Assign { local, value, drop_new, .. } => { + let callable = self.callable_region_fact(value, depth); + if callable.is_empty() { + self.state.callable_capture_region.remove(local); + } else { + self.state.callable_capture_region.insert(*local, callable); + } // Record allocation provenance separately from escape Region: region-free Move // resources and owned call results are individual, while arena allocations are not. if is_owned_droppable(value.ty, self.structs, self.enums, self.tagged_types) @@ -11817,7 +12823,7 @@ impl<'a> EscapeCheck<'a> { } } } - Stmt::AssignField { root, value, .. } => { + Stmt::AssignField { root, path, value } => { if needs_drop_flag( value.ty, self.structs, @@ -11856,6 +12862,12 @@ impl<'a> EscapeCheck<'a> { ); } } + let path = path + .iter() + .copied() + .map(BorrowProjection::StructField) + .collect::>(); + self.replace_callable_region_path(*root, &path, value, depth); } Stmt::Return(Some(e)) => { // A returned value escapes to the caller (`Static`): only a `Static`-region @@ -11898,6 +12910,19 @@ impl<'a> EscapeCheck<'a> { } } } + let callable = self.callable_region_fact(init, depth); + for (index, local) in locals.iter().enumerate() { + let Some(local) = local else { continue }; + let selected = Self::project_callable_region_fact( + &callable, + &[BorrowProjection::TupleElement(index as u32)], + ); + if selected.is_empty() { + self.state.callable_capture_region.remove(local); + } else { + self.state.callable_capture_region.insert(*local, selected); + } + } } // `break e` carries `e` out of the loop, so `e` escapes the loop exactly as a returned // value escapes the function: it must be `Static`-region (a borrowed Frame/arena view @@ -12882,6 +13907,10 @@ struct MoveCheck<'a> { /// Settled same-program/imported return summaries for mapping call results back to their exact /// caller-side inputs. The map is recomputed to a fixpoint before the diagnostic pass. named_return_borrow: &'a std::collections::HashMap, + /// Checked parameter modes for direct callees. Borrowed arguments are read from caller + /// storage and therefore neither transfer ownership nor contribute their value facts as if + /// they were by-value copies. + named_param_modes: &'a std::collections::HashMap>, /// Direct named calls observed by the exhaustive expression walk. Present only while building /// the reverse worklist for named-return inference. summary_dependencies: Option<&'a mut std::collections::HashSet>, @@ -12895,6 +13924,12 @@ struct MoveCheck<'a> { enums: &'a [hir::EnumDef], /// Interned nested Option/Result payloads. tagged_types: &'a [hir::TaggedType], + /// Interned function signatures supply indirect-call parameter modes. + fn_types: &'a [hir::FnTy], + /// Target-relative summaries for each concrete function-value type. + callable_targets: &'a [CallableTargetSet], + /// Stable per-program target ordinals used in closure-environment projection facts. + callable_target_ids: &'a std::collections::HashMap, /// Stack of enclosing `loop`s (innermost last). Each entry collects the moved-set snapshot at /// every `break` bound to that loop; their union is the move state after the loop (code past a /// loop runs only after a `break`, so a local moved on *any* break path is possibly-moved). @@ -13040,6 +14075,9 @@ impl BorrowRoot { enum BorrowProjection { StructField(u32), TupleElement(u32), + ArrayElement(u32), + ClosureTarget(u32), + ClosureCapture(u32), EnumPayload { variant: u32, index: u32 }, OptionSome, ResultOk, @@ -13124,6 +14162,24 @@ impl BorrowFact { out } + fn project_array_elements(&self) -> Self { + let mut out = Self::from_direct(self.direct.clone()); + for (path, roots) in &self.projected { + let Some((BorrowProjection::ArrayElement(_), rest)) = path.split_first() else { + continue; + }; + if rest.is_empty() { + out.direct.extend(roots); + } else { + out.projected + .entry(rest.to_vec()) + .or_default() + .extend(roots); + } + } + out + } + fn replace_exact(&mut self, path: &[BorrowProjection], incoming: BorrowFact) { self.projected.retain(|candidate, _| { !candidate.starts_with(path) @@ -13142,6 +14198,20 @@ impl BorrowFact { } } + fn join_at(&mut self, path: &[BorrowProjection], incoming: &BorrowFact) { + if !incoming.direct.is_empty() { + self.projected + .entry(path.to_vec()) + .or_default() + .extend(&incoming.direct); + } + for (suffix, roots) in &incoming.projected { + let mut destination = path.to_vec(); + destination.extend(suffix); + self.projected.entry(destination).or_default().extend(roots); + } + } + fn mark_ended(&mut self, invalid: &EndedRoots) { let mark = |roots: &mut BorrowRoots| { *roots = std::mem::take(roots) @@ -13191,7 +14261,8 @@ struct MoveMatchPrepared { evaluated_borrows: BorrowState, consumed: Option, consumed_borrows: Option, - scrutinee_roots: BorrowRoots, + scrutinee_ty: Ty, + scrutinee_fact: BorrowFact, } #[derive(Default)] @@ -13300,6 +14371,10 @@ impl BorrowState { self.invalidate_matching(how, |r| r == BorrowRoot::Local(owner)); } + fn invalidate_roots(&mut self, roots: &BorrowRoots, how: BorrowEnd) { + self.invalidate_matching(how, |root| roots.contains(&root)); + } + fn join(a: &Self, b: &Self) -> Self { let mut out = a.clone(); for (&local, roots) in &b.sources { @@ -13400,7 +14475,15 @@ macro_rules! move_expr { impl<'a> MoveCheck<'a> { fn check(mut self) -> BorrowRoots { for (position, &local) in self.f.params.iter().enumerate() { - if !self.local_may_borrow(local) { + let mode = self + .f + .param_modes + .get(position) + .copied() + .unwrap_or(ast::ParamMode::ByValue); + if !self.local_may_borrow(local) + && !matches!(mode, ast::ParamMode::Borrow | ast::ParamMode::BorrowMut) + { continue; } let mut roots = BorrowRoots::new(); @@ -13465,9 +14548,102 @@ impl<'a> MoveCheck<'a> { }) } + fn borrowed_param_position(&self, id: LocalId) -> Option { + self.f + .params + .iter() + .position(|¶m| param == id) + .filter(|&position| { + matches!( + self.f.param_modes.get(position), + Some(ast::ParamMode::Borrow | ast::ParamMode::BorrowMut) + ) + }) + .map(|position| position as u32) + } + + fn borrowed_param_mode(&self, id: LocalId) -> Option { + self.borrowed_param_position(id) + .and_then(|position| self.f.param_modes.get(position as usize).copied()) + } + + fn refresh_borrow_mut_place(&mut self, argument: &Expr) { + let root = match argument.kind { + ExprKind::Local(local) => Some(local), + ExprKind::Field { root, .. } => Some(root), + _ => None, + }; + if let Some(root) = root { + self.borrows.invalid.remove(&root); + } + } + + fn check_call_borrow_aliases( + &mut self, + display: &str, + args: &[Expr], + modes: &[ast::ParamMode], + ) { + let place = |expression: &Expr| match &expression.kind { + ExprKind::Local(local) => Some((*local, Vec::new())), + ExprKind::Field { root, path } => Some((*root, path.clone())), + _ => None, + }; + let overlaps = |left: &(LocalId, Vec), right: &(LocalId, Vec)| { + left.0 == right.0 + && (left.1.starts_with(&right.1) || right.1.starts_with(&left.1)) + }; + for (index, mode) in modes.iter().copied().enumerate() { + if !matches!(mode, ast::ParamMode::Borrow | ast::ParamMode::BorrowMut) { + continue; + } + let Some(argument) = args.get(index) else { continue }; + let roots = self.storage_roots(argument); + let argument_place = place(argument); + for (peer_index, peer) in args.iter().enumerate() { + if peer_index == index { + continue; + } + let peer_mode = modes + .get(peer_index) + .copied() + .unwrap_or(ast::ParamMode::ByValue); + let conflicts = match (mode, peer_mode) { + // An exclusive borrow can replace its owner, so every overlapping peer is + // invalid even when that peer is a Copy view embedded in a plain aggregate. + (ast::ParamMode::BorrowMut, _) => true, + (ast::ParamMode::Borrow, ast::ParamMode::Out) => true, + (ast::ParamMode::Borrow, ast::ParamMode::BorrowMut) => true, + (ast::ParamMode::Borrow, ast::ParamMode::ByValue) => self.is_move_ty(peer.ty), + _ => false, + }; + if conflicts { + let peer_roots = self.storage_roots(peer); + let direct_overlap = argument_place + .as_ref() + .zip(place(peer).as_ref()) + .is_some_and(|(left, right)| overlaps(left, right)); + if direct_overlap || roots.iter().any(|root| peer_roots.contains(root)) { + self.diags.error( + format!( + "borrowed argument {} to '{display}' aliases argument {}, whose mode may invalidate the same owner", + index + 1, + peer_index + 1, + ), + argument.span, + ); + break; + } + } + } + } + } + fn local_storage_roots(&self, id: LocalId) -> BorrowRoots { let mut roots = self.borrows.sources.get(&id).cloned().unwrap_or_default(); - if self.local_owns_view_storage(id) { + if let Some(position) = self.borrowed_param_position(id) { + roots.insert(BorrowRoot::Param(position)); + } else if self.local_owns_view_storage(id) || self.local_may_borrow(id) { roots.insert(BorrowRoot::Local(id)); } roots @@ -13475,7 +14651,9 @@ impl<'a> MoveCheck<'a> { fn local_borrow_fact(&self, id: LocalId) -> BorrowFact { let mut fact = self.borrows.facts.get(&id).cloned().unwrap_or_default(); - if self.local_owns_view_storage(id) { + if let Some(position) = self.borrowed_param_position(id) { + fact.direct.insert(BorrowRoot::Param(position)); + } else if self.local_owns_view_storage(id) || self.local_may_borrow(id) { fact.direct.insert(BorrowRoot::Local(id)); } fact @@ -13494,6 +14672,12 @@ impl<'a> MoveCheck<'a> { .and_then(|def| def.elems.get(index as usize)) .copied() .map(scalar_to_ty), + (Ty::Array(element, len), BorrowProjection::ArrayElement(index)) if index < len => { + Some(scalar_to_ty(element)) + } + (Ty::StructArray(id, len), BorrowProjection::ArrayElement(index)) if index < len => { + Some(Ty::Struct(id)) + } ( Ty::Enum(id), BorrowProjection::EnumPayload { variant, index }, @@ -13524,7 +14708,16 @@ impl<'a> MoveCheck<'a> { } fn normalize_borrow_fact(&self, ty: Ty, fact: BorrowFact) -> BorrowFact { - if !matches!(expand_tagged_ty(ty, self.tagged_types), Ty::Struct(_) | Ty::Tuple(_) | Ty::Enum(_) | Ty::Option(_) | Ty::Result(..)) { + if !matches!( + expand_tagged_ty(ty, self.tagged_types), + Ty::Struct(_) + | Ty::Tuple(_) + | Ty::Array(..) + | Ty::StructArray(..) + | Ty::Enum(_) + | Ty::Option(_) + | Ty::Result(..) + ) { return fact; } let mut out = BorrowFact::default(); @@ -13727,8 +14920,10 @@ impl<'a> MoveCheck<'a> { fact.direct.extend(self.borrow_sources(capture)); } } - for capture in node_captures(&e.kind) { - fact.direct.extend(self.borrow_sources(capture)); + if !matches!(e.kind, ExprKind::Closure { .. }) { + for capture in node_captures(&e.kind) { + fact.direct.extend(self.borrow_sources(capture)); + } } fact.join(&self.borrow_fact_inner(e)) } @@ -13827,8 +15022,8 @@ impl<'a> MoveCheck<'a> { path: &[BorrowProjection], result_ty: Ty, ) -> BorrowFact { - let fact = self.normalize_borrow_fact(ty, fact); let fallback = fact.flatten(); + let fact = self.normalize_borrow_fact(ty, fact); let mut current_ty = ty; let mut selected = fact; for &projection in path { @@ -13846,6 +15041,134 @@ impl<'a> MoveCheck<'a> { selected } + fn fixed_array_shape(&self, ty: Ty) -> Option<(Ty, u32)> { + match expand_tagged_ty(ty, self.tagged_types) { + Ty::Array(element, len) => Some((scalar_to_ty(element), len)), + Ty::StructArray(id, len) => Some((Ty::Struct(id), len)), + _ => None, + } + } + + fn exact_fixed_index(&self, index: &Expr, len: u32) -> Option { + match index.kind { + ExprKind::Int(value) if value >= 0 && (value as u128) < len as u128 => { + Some(value as u32) + } + _ => None, + } + } + + fn project_fixed_array_fact(&self, recv: &Expr, index: &Expr, result_ty: Ty) -> BorrowFact { + let Some((element_ty, len)) = self.fixed_array_shape(recv.ty) else { + return BorrowFact::from_direct(self.borrow_sources(recv)); + }; + let raw = self.borrow_fact(recv); + let fallback = raw.flatten(); + let fact = self.normalize_borrow_fact(recv.ty, raw); + if expand_tagged_ty(element_ty, self.tagged_types) + != expand_tagged_ty(result_ty, self.tagged_types) + { + let mut roots = fallback; + roots.extend(self.storage_roots(recv)); + return BorrowFact::from_direct(roots); + } + self.exact_fixed_index(index, len).map_or_else( + || fact.project_array_elements(), + |index| fact.project_exact(BorrowProjection::ArrayElement(index)), + ) + } + + fn project_fixed_element_field_fact( + &self, + recv: &Expr, + index: &Expr, + path: &[u32], + result_ty: Ty, + ) -> BorrowFact { + let Some((element_ty, len)) = self.fixed_array_shape(recv.ty) else { + return BorrowFact::from_direct(self.borrow_sources(recv)); + }; + let raw = self.borrow_fact(recv); + let fallback = raw.flatten(); + let projections = path + .iter() + .copied() + .map(BorrowProjection::StructField) + .collect::>(); + let mut selected_ty = Some(element_ty); + for &projection in &projections { + selected_ty = selected_ty.and_then(|ty| self.projection_ty(ty, projection)); + } + if selected_ty.is_none_or(|ty| { + expand_tagged_ty(ty, self.tagged_types) + != expand_tagged_ty(result_ty, self.tagged_types) + }) { + let mut roots = fallback; + roots.extend(self.storage_roots(recv)); + return BorrowFact::from_direct(roots); + } + let mut fact = self.normalize_borrow_fact(recv.ty, raw); + fact = self.exact_fixed_index(index, len).map_or_else( + || fact.project_array_elements(), + |index| fact.project_exact(BorrowProjection::ArrayElement(index)), + ); + self.project_fact_or_flatten(element_ty, fact, &projections, result_ty) + } + + fn collection_element_ty(&self, ty: Ty) -> Option { + match expand_tagged_ty(ty, self.tagged_types) { + Ty::Array(element, _) | Ty::DynArray(element) | Ty::Slice(element) => { + Some(scalar_to_ty(element)) + } + Ty::StructArray(id, _) | Ty::DynStructArray(id, _) => Some(Ty::Struct(id)), + _ => None, + } + } + + fn pipeline_element_fact(&self, source: &Expr, stages: &[Stage]) -> (Ty, BorrowFact) { + let mut element_ty = self.collection_element_ty(source.ty).unwrap_or(source.ty); + let mut fact = if self.fixed_array_shape(source.ty).is_some() { + self.normalize_borrow_fact(source.ty, self.borrow_fact(source)) + .project_array_elements() + } else { + BorrowFact::from_direct(self.borrow_sources(source)) + }; + for stage in stages { + match &stage.kind { + StageKind::Project { field } => { + fact = self.project_fact_or_flatten( + element_ty, + fact, + &[BorrowProjection::StructField(*field)], + stage.out_ty, + ); + } + StageKind::WhereField { field } => { + if self.projection_ty(element_ty, BorrowProjection::StructField(*field)) + != Some(Ty::Bool) + { + fact = BorrowFact::from_direct(fact.flatten()); + } + } + StageKind::Where { .. } | StageKind::WhereStrContains { .. } => {} + StageKind::Map { .. } => { + fact = BorrowFact::from_direct(fact.flatten()); + } + } + element_ty = stage.out_ty; + } + (element_ty, fact) + } + + fn try_error_roots(&self, result: &Expr) -> BorrowRoots { + let fact = self.normalize_borrow_fact(result.ty, self.borrow_fact(result)); + if self.projection_ty(result.ty, BorrowProjection::ResultErr).is_none() { + fact.flatten() + } else { + fact.project_exact(BorrowProjection::ResultErr).flatten() + } + } + fn local_projected_fact( &self, local: LocalId, @@ -13873,6 +15196,94 @@ impl<'a> MoveCheck<'a> { } } + fn callable_return_fact(&self, callee: &Expr, args: &[Expr]) -> BorrowFact { + let signature = match callee.ty { + Ty::Fn(id) => self.fn_types.get(id as usize), + _ => None, + }; + let arguments = args + .iter() + .enumerate() + .map(|(index, argument)| { + let fact = if signature.is_some_and(|signature| { + matches!( + signature.params.get(index).map(|(mode, _)| mode), + Some(ast::ParamMode::Borrow | ast::ParamMode::BorrowMut) + ) + }) { + BorrowFact::from_direct(self.storage_roots(argument)) + } else { + self.borrow_fact(argument) + }; + (argument.ty, fact) + }) + .collect::>(); + self.callable_return_fact_from_facts(callee, &arguments) + } + + fn callable_return_fact_from_facts( + &self, + callee: &Expr, + arguments: &[(Ty, BorrowFact)], + ) -> BorrowFact { + let unresolved = || { + let mut fact = self.borrow_fact(callee); + for (ty, argument) in arguments { + if ty_may_borrow( + *ty, + self.structs, + self.tuples, + self.enums, + self.tagged_types, + ) { + fact = fact.join(argument); + } + } + fact + }; + let ty = match &callee.kind { + ExprKind::Local(local) => self + .f + .locals + .get(*local as usize) + .map(|local| local.ty) + .unwrap_or(callee.ty), + _ => callee.ty, + }; + let Ty::Fn(id) = ty else { + return BorrowFact::from_direct(unresolved().flatten()); + }; + let Some(targets) = self.callable_targets.get(id as usize) else { + return BorrowFact::from_direct(unresolved().flatten()); + }; + if targets.is_empty() { + // A function-typed parameter has no concrete target set. Keep the settled fail-closed + // fallback over every compatible explicit input and the callable environment itself. + return BorrowFact::from_direct(unresolved().flatten()); + } + let mut fact = BorrowFact::default(); + let environment = self.borrow_fact(callee); + for (&target, summary) in targets { + let hir::ReturnBorrowSummary::Roots { params, captures } = summary else { + continue; + }; + for index in params.iter().copied() { + if let Some((_, argument)) = arguments.get(index as usize) { + fact = fact.join(argument); + } + } + let selected_environment = + environment.project_exact(BorrowProjection::ClosureTarget(target)); + for capture in captures.iter().copied() { + fact = fact.join( + &selected_environment + .project_exact(BorrowProjection::ClosureCapture(capture)), + ); + } + } + fact + } + fn borrow_fact_inner(&self, e: &Expr) -> BorrowFact { match &e.kind { ExprKind::Local(id) => self.local_borrow_fact(*id), @@ -13890,6 +15301,78 @@ impl<'a> MoveCheck<'a> { &[BorrowProjection::TupleElement(*index)], e.ty, ), + ExprKind::ArrayLit { elems, elem, .. } => { + let valid = self.fixed_array_shape(e.ty).is_some_and(|(element_ty, len)| { + len as usize == elems.len() + && expand_tagged_ty(element_ty, self.tagged_types) + == expand_tagged_ty(*elem, self.tagged_types) + && elems.iter().all(|value| { + expand_tagged_ty(value.ty, self.tagged_types) + == expand_tagged_ty(element_ty, self.tagged_types) + }) + }); + if !valid { + return BorrowFact::from_direct(elems.iter().fold( + BorrowRoots::new(), + |mut roots, value| { + roots.extend(self.borrow_sources(value)); + roots + }, + )); + } + elems.iter().enumerate().fold( + BorrowFact::default(), + |fact, (index, value)| { + fact.join( + &self + .borrow_fact(value) + .prefixed(BorrowProjection::ArrayElement(index as u32)), + ) + }, + ) + } + ExprKind::Index { recv, index } if self.fixed_array_shape(recv.ty).is_some() => { + self.project_fixed_array_fact(recv, index, e.ty) + } + ExprKind::IndexField { base, index, path } => { + let Some(local_ty) = self.f.locals.get(*base as usize).map(|local| local.ty) else { + return BorrowFact::from_direct(self.local_storage_roots(*base)); + }; + let Some((element_ty, len)) = self.fixed_array_shape(local_ty) else { + return BorrowFact::from_direct(self.local_storage_roots(*base)); + }; + let raw = self.local_borrow_fact(*base); + let fallback = raw.flatten(); + let projections = path + .iter() + .copied() + .map(BorrowProjection::StructField) + .collect::>(); + let mut selected_ty = Some(element_ty); + for &projection in &projections { + selected_ty = selected_ty.and_then(|ty| self.projection_ty(ty, projection)); + } + if selected_ty.is_none_or(|ty| { + expand_tagged_ty(ty, self.tagged_types) + != expand_tagged_ty(e.ty, self.tagged_types) + }) { + let mut roots = fallback; + roots.extend(self.local_storage_roots(*base)); + return BorrowFact::from_direct(roots); + } + let mut fact = self.normalize_borrow_fact(local_ty, raw); + fact = if *index < len { + fact.project_exact(BorrowProjection::ArrayElement(*index)) + } else { + return BorrowFact::from_direct(fallback); + }; + self.project_fact_or_flatten(element_ty, fact, &projections, e.ty) + } + ExprKind::ElemField { recv, index, path, .. } + if self.fixed_array_shape(recv.ty).is_some() => + { + self.project_fixed_element_field_fact(recv, index, path, e.ty) + } ExprKind::StructLit { struct_id, fields } => { let valid = e.ty == Ty::Struct(*struct_id) && self.structs.get(*struct_id as usize).is_some_and(|definition| { @@ -13954,6 +15437,162 @@ impl<'a> MoveCheck<'a> { }, ) } + ExprKind::EnumValue { + enum_id, + variant, + payload, + } => { + let valid = expand_tagged_ty(e.ty, self.tagged_types) == Ty::Enum(*enum_id) + && self.enums.get(*enum_id as usize).is_some_and(|definition| { + definition + .variants + .get(*variant as usize) + .is_some_and(|case| { + case.payload.len() == payload.len() + && case.payload.iter().zip(payload).all(|(expected, value)| { + expand_tagged_ty(scalar_to_ty(*expected), self.tagged_types) + == expand_tagged_ty(value.ty, self.tagged_types) + }) + }) + }); + if !valid { + return BorrowFact::from_direct(payload.iter().fold( + BorrowRoots::new(), + |mut roots, value| { + roots.extend(self.borrow_sources(value)); + roots + }, + )); + } + payload.iter().enumerate().fold( + BorrowFact::default(), + |fact, (index, value)| { + fact.join(&self.borrow_fact(value).prefixed( + BorrowProjection::EnumPayload { + variant: *variant, + index: index as u32, + }, + )) + }, + ) + } + ExprKind::OptionSome(value) => self + .borrow_fact(value) + .prefixed(BorrowProjection::OptionSome), + ExprKind::OptionNone => BorrowFact::default(), + ExprKind::ResultOk(value) => self + .borrow_fact(value) + .prefixed(BorrowProjection::ResultOk), + ExprKind::ResultErr(value) => self + .borrow_fact(value) + .prefixed(BorrowProjection::ResultErr), + ExprKind::Try(result) => self.project_fact_or_flatten( + result.ty, + self.borrow_fact(result), + &[BorrowProjection::ResultOk], + e.ty, + ), + ExprKind::ResultMapErr { result, f } => { + let result_fact = self.normalize_borrow_fact(result.ty, self.borrow_fact(result)); + let (input_error_ty, output_error_ty) = match ( + expand_tagged_ty(result.ty, self.tagged_types), + expand_tagged_ty(e.ty, self.tagged_types), + ) { + (Ty::Result(_, input), Ty::Result(_, output)) => { + (scalar_to_ty(input), scalar_to_ty(output)) + } + _ => { + let mut roots = result_fact.flatten(); + roots.extend(self.borrow_sources(f)); + return BorrowFact::from_direct(roots); + } + }; + let ok = result_fact + .project_exact(BorrowProjection::ResultOk) + .prefixed(BorrowProjection::ResultOk); + let input_error = result_fact.project_exact(BorrowProjection::ResultErr); + let mapped_error = self.callable_return_fact_from_facts( + f, + &[(input_error_ty, input_error)], + ); + if self.projection_ty(e.ty, BorrowProjection::ResultErr) + != Some(output_error_ty) + { + let mut roots = ok.flatten(); + roots.extend(mapped_error.flatten()); + return BorrowFact::from_direct(roots); + } + ok.join(&mapped_error.prefixed(BorrowProjection::ResultErr)) + } + ExprKind::ElseUnwrap { opt, fallback } => { + let projection = match expand_tagged_ty(opt.ty, self.tagged_types) { + Ty::Option(_) => Some(BorrowProjection::OptionSome), + Ty::Result(..) => Some(BorrowProjection::ResultOk), + _ => None, + }; + let success = projection.map_or_else( + || BorrowFact::from_direct(self.borrow_sources(opt)), + |projection| { + self.project_fact_or_flatten( + opt.ty, + self.borrow_fact(opt), + &[projection], + e.ty, + ) + }, + ); + if self.non_fallthrough.contains(&fallback.span) { + success + } else { + success.join(&self.borrow_fact(fallback)) + } + } + ExprKind::Match { arms, .. } => arms + .iter() + .filter(|arm| !self.non_fallthrough.contains(&arm.body.span)) + .fold(BorrowFact::default(), |fact, arm| { + fact.join(&self.borrow_fact(&arm.body)) + }), + ExprKind::ArrayToArray { source, stages, .. } + | ExprKind::ArraySort { source, stages, .. } + | ExprKind::ArraySortBy { source, stages, .. } + | ExprKind::ArrayParMap { source, stages, .. } => { + let (_, element) = self.pipeline_element_fact(source, stages); + BorrowFact::from_direct(element.flatten()) + } + ExprKind::ArrayPartition { source, stages, .. } => { + let (_, element) = self.pipeline_element_fact(source, stages); + let roots = BorrowFact::from_direct(element.flatten()); + roots + .clone() + .prefixed(BorrowProjection::TupleElement(0)) + .join(&roots.prefixed(BorrowProjection::TupleElement(1))) + } + ExprKind::ArrayToSoa { source, .. } => { + let (_, element) = self.pipeline_element_fact(source, &[]); + BorrowFact::from_direct(element.flatten()) + } + ExprKind::ArrayChunks { source, .. } => { + BorrowFact::from_direct(self.storage_roots(source)) + } + ExprKind::CallFnValue { callee, args } => self.callable_return_fact(callee, args), + ExprKind::Closure { lifted, captures } => { + let fact = captures.iter().enumerate().fold( + BorrowFact::default(), + |fact, (index, capture)| { + fact.join( + &self + .borrow_fact(capture) + .prefixed(BorrowProjection::ClosureCapture(index as u32)), + ) + }, + ); + match self.callable_target_ids.get(lifted).copied() { + Some(target) => fact.prefixed(BorrowProjection::ClosureTarget(target)), + None => BorrowFact::from_direct(fact.flatten()), + } + } + ExprKind::FnValue(_) => BorrowFact::default(), ExprKind::Block(block) | ExprKind::Arena(block) | ExprKind::TaskGroup(block) @@ -13968,8 +15607,8 @@ impl<'a> MoveCheck<'a> { .get(&e.span) .cloned() .unwrap_or_default(), - // L2b-a2-t owns tagged construction, match bindings, `else`, `?`, and `map_err`. - // Every remaining expression retains the L2b-a1 flattened conservative fact. + // Remaining non-fixed collection and pipeline forms retain + // the conservative flattened fact until their dedicated C-B cells below. _ => BorrowFact::from_direct(self.borrow_sources_inner(e)), } } @@ -14015,6 +15654,7 @@ impl<'a> MoveCheck<'a> { fn map_summary_roots<'b>( &self, summary: &hir::ReturnBorrowSummary, + modes: Option<&[ast::ParamMode]>, args: impl std::ops::Fn(u32) -> Option<&'b Expr>, ) -> BorrowRoots { let hir::ReturnBorrowSummary::Roots { @@ -14027,7 +15667,16 @@ impl<'a> MoveCheck<'a> { let mut roots = BorrowRoots::new(); for &index in params { if let Some(value) = args(index) { - roots.extend(self.borrow_sources(value)); + if modes.is_some_and(|modes| { + matches!( + modes.get(index as usize), + Some(ast::ParamMode::Borrow | ast::ParamMode::BorrowMut) + ) + }) { + roots.extend(self.storage_roots(value)); + } else { + roots.extend(self.borrow_sources(value)); + } } } roots @@ -14164,6 +15813,7 @@ impl<'a> MoveCheck<'a> { |summary| { self.map_summary_roots( summary, + self.named_param_modes.get(func).map(Vec::as_slice), |index| args.get(index as usize), ) }, @@ -14408,6 +16058,57 @@ impl<'a> MoveCheck<'a> { self.borrows.update_fact(local, current); } + fn update_local_array_projection( + &mut self, + local: LocalId, + index: &Expr, + field_path: &[u32], + value: &Expr, + ) { + if !self.local_may_borrow(local) { + return; + } + let local_ty = self.f.locals[local as usize].ty; + let Some((element_ty, len)) = self.fixed_array_shape(local_ty) else { + self.join_local_borrow_fallback(local, value); + return; + }; + let mut current = self.normalize_borrow_fact( + local_ty, + self.borrows.facts.get(&local).cloned().unwrap_or_default(), + ); + let incoming = self.normalize_borrow_fact(value.ty, self.borrow_fact(value)); + let mut suffix = field_path + .iter() + .copied() + .map(BorrowProjection::StructField) + .collect::>(); + let mut destination_ty = Some(element_ty); + for &projection in &suffix { + destination_ty = destination_ty.and_then(|ty| self.projection_ty(ty, projection)); + } + if destination_ty.is_none_or(|ty| { + expand_tagged_ty(ty, self.tagged_types) + != expand_tagged_ty(value.ty, self.tagged_types) + }) { + current.direct.extend(incoming.flatten()); + self.borrows.update_fact(local, current); + return; + } + if let Some(exact) = self.exact_fixed_index(index, len) { + suffix.insert(0, BorrowProjection::ArrayElement(exact)); + current.replace_exact(&suffix, incoming); + } else { + for candidate in 0..len { + let mut path = Vec::with_capacity(suffix.len() + 1); + path.push(BorrowProjection::ArrayElement(candidate)); + path.extend(&suffix); + current.join_at(&path, &incoming); + } + } + self.borrows.update_fact(local, current); + } + fn join_local_borrow_fallback(&mut self, local: LocalId, value: &Expr) { if !self.local_may_borrow(local) { return; @@ -14899,14 +16600,32 @@ impl<'a> MoveCheck<'a> { } move_expr!(self, index, moved, false, false); move_expr!(self, value, moved, false, false); - self.join_local_borrow_fallback(*base, value); + self.update_local_array_projection(*base, index, &[], value); } // Struct element-field and whole-element stores may install an owned `string` or // Move struct. MIR drops the old destination and nulls a moved RHS source, so // MoveCheck must consume that RHS as well. Copy-only dynamic/SoA stores are // unaffected by a consuming context. - Stmt::AssignElemField { base, index, value, .. } - | Stmt::AssignElem { base, index, value, .. } => { + Stmt::AssignElemField { + base, + index, + path, + value, + .. + } => { + self.check_borrow_use(*base, index.span); + if whole_moved(moved, *base) { + let name = &self.f.locals[*base as usize].name; + self.diags.error(format!("use of moved value '{name}'"), index.span); + } + move_expr!(self, index, moved, false, false); + move_expr!(self, value, moved, true, true); + if self.is_move_ty(value.ty) { + self.invalidate_owner(*base); + } + self.update_local_array_projection(*base, index, path, value); + } + Stmt::AssignElem { base, index, value, .. } => { self.check_borrow_use(*base, index.span); if whole_moved(moved, *base) { let name = &self.f.locals[*base as usize].name; @@ -14917,7 +16636,7 @@ impl<'a> MoveCheck<'a> { if self.is_move_ty(value.ty) { self.invalidate_owner(*base); } - self.join_local_borrow_fallback(*base, value); + self.update_local_array_projection(*base, index, &[], value); } Stmt::AssignVecLane { value, .. } => { move_expr!(self, value, moved, false, false); @@ -15411,7 +17130,7 @@ impl<'a> MoveCheck<'a> { ) -> Option { enum Post<'e> { None, - Try(&'e Expr), + Try(BorrowRoots), LoopBreak(Option<&'e Expr>), LoopDiverge, Pipeline { @@ -15419,6 +17138,7 @@ impl<'a> MoveCheck<'a> { snapshots_source: bool, snapshot: Option>, }, + BorrowMutCall(&'e Expr), IfAfterCondition { then: &'e Block, els: &'e Block, @@ -15487,6 +17207,7 @@ impl<'a> MoveCheck<'a> { BlockPairAfterIndex { base: LocalId, value: &'e Expr, + field_path: &'e [u32], value_consuming: bool, invalidates_owner: bool, }, @@ -15494,6 +17215,7 @@ impl<'a> MoveCheck<'a> { base: LocalId, index: &'e Expr, value: &'e Expr, + field_path: &'e [u32], invalidates_owner: bool, index_complete: bool, }, @@ -15651,6 +17373,7 @@ impl<'a> MoveCheck<'a> { Post::BlockPairAfterIndex { base: *base, value, + field_path: &[], value_consuming: false, invalidates_owner: false, }, @@ -15665,6 +17388,7 @@ impl<'a> MoveCheck<'a> { base: *base, index, value, + field_path: &[], invalidates_owner: false, index_complete: false, }, @@ -15682,20 +17406,20 @@ impl<'a> MoveCheck<'a> { | Stmt::AssignElem { .. }] ) => { - let (base, index, value) = match &block.stmts[0] - { + let (base, index, value, field_path) = match &block.stmts[0] { Stmt::AssignElemField { base, index, + path, value, .. - } - | Stmt::AssignElem { + } => (*base, index, value, path.as_slice()), + Stmt::AssignElem { base, index, value, .. - } => (*base, index, value), + } => (*base, index, value, &[][..]), _ => unreachable!("single element assign guard"), }; self.check_borrow_use(base, index.span); @@ -15717,6 +17441,7 @@ impl<'a> MoveCheck<'a> { Post::BlockPairAfterIndex { base, value, + field_path, value_consuming: true, invalidates_owner: true, }, @@ -15731,6 +17456,7 @@ impl<'a> MoveCheck<'a> { base, index, value, + field_path, invalidates_owner: true, index_complete: false, }, @@ -16274,7 +18000,8 @@ impl<'a> MoveCheck<'a> { (child.as_ref(), false, true, true, Post::None) } ExprKind::Try(child) => { - (child.as_ref(), false, true, true, Post::Try(child)) + let error_roots = self.try_error_roots(child); + (child.as_ref(), false, true, true, Post::Try(error_roots)) } ExprKind::TaskGet(child) => { let consumes = is_owned_droppable( @@ -16294,8 +18021,20 @@ impl<'a> MoveCheck<'a> { { dependencies.insert(func.clone()); } - let consumes = func != "print"; - (&args[0], false, consumes, consumes, Post::None) + let mode = self + .named_param_modes + .get(func) + .and_then(|modes| modes.first()) + .copied() + .unwrap_or(ast::ParamMode::ByValue); + let consumes = func != "print" + && !matches!(mode, ast::ParamMode::Borrow | ast::ParamMode::BorrowMut); + let post = if mode == ast::ParamMode::BorrowMut { + Post::BorrowMutCall(&args[0]) + } else { + Post::None + }; + (&args[0], false, consumes, consumes, post) } ExprKind::CallFnValue { callee, args } if args.is_empty() => { (callee.as_ref(), false, false, false, Post::None) @@ -16618,7 +18357,7 @@ impl<'a> MoveCheck<'a> { } None } - Post::Try(result) => Some(result), + Post::Try(error_roots) => Some(error_roots), Post::LoopBreak(value) => { if falls_through { let fact = value.map_or_else( @@ -16657,6 +18396,14 @@ impl<'a> MoveCheck<'a> { } None } + Post::BorrowMutCall(argument) => { + if falls_through { + let roots = self.storage_roots(argument); + self.borrows.invalidate_roots(&roots, BorrowEnd::Consumed); + self.refresh_borrow_mut_place(argument); + } + None + } Post::MatchAfterScrutinee { scrutinee, arms, @@ -17032,6 +18779,7 @@ impl<'a> MoveCheck<'a> { Post::BlockPairAfterIndex { base, value, + field_path, value_consuming, invalidates_owner, } => { @@ -17048,14 +18796,32 @@ impl<'a> MoveCheck<'a> { { self.invalidate_owner(base); } - self.join_local_borrow_fallback(base, value); + self.update_local_array_projection( + base, + match &wrapper.kind { + ExprKind::Block(block) + | ExprKind::Arena(block) + | ExprKind::TaskGroup(block) + | ExprKind::Unsafe(block) => match &block.stmts[0] { + Stmt::AssignIndex { index, .. } + | Stmt::AssignElemField { index, .. } + | Stmt::AssignElem { index, .. } => index, + _ => unreachable!("array assignment post wrapper"), + }, + _ => unreachable!("array assignment post wrapper"), + }, + field_path, + value, + ); } } None } Post::BlockPairAfterValue { base, + index, value, + field_path, invalidates_owner, index_complete, .. @@ -17066,7 +18832,7 @@ impl<'a> MoveCheck<'a> { { self.invalidate_owner(base); } - self.join_local_borrow_fallback(base, value); + self.update_local_array_projection(base, index, field_path, value); } None } @@ -17118,7 +18884,7 @@ impl<'a> MoveCheck<'a> { if !falls_through { self.non_fallthrough.insert(wrapper.span); } else { - if let Some(result) = try_result + if let Some(error_roots) = try_result && ty_may_borrow( self.f.ret, self.structs, @@ -17127,7 +18893,7 @@ impl<'a> MoveCheck<'a> { self.tagged_types, ) { - self.return_roots.extend(self.borrow_sources(result)); + self.return_roots.extend(error_roots); } if !Self::defers_child_snapshot_validation(&wrapper.kind) { for snapshot in child_snapshots { @@ -17231,17 +18997,55 @@ impl<'a> MoveCheck<'a> { (None, None) }; self.borrows = evaluated_borrows.clone(); - let scrutinee_roots = self.borrow_sources(scrutinee); + let scrutinee_fact = self.borrow_fact(scrutinee); MoveMatchPrepared { incoming_borrows, evaluated, evaluated_borrows, consumed, consumed_borrows, - scrutinee_roots, + scrutinee_ty: scrutinee.ty, + scrutinee_fact, } } + fn match_binding_fact( + &self, + scrutinee_ty: Ty, + scrutinee_fact: &BorrowFact, + arm: &MatchArm, + binding_index: usize, + binding: LocalId, + ) -> BorrowFact { + if !self.local_may_borrow(binding) { + return BorrowFact::default(); + } + let Some(&variant) = arm + .variants + .as_slice() + .first() + .filter(|_| arm.variants.len() == 1) + else { + return BorrowFact::from_direct(scrutinee_fact.flatten()); + }; + let projection = match expand_tagged_ty(scrutinee_ty, self.tagged_types) { + Ty::Enum(_) => BorrowProjection::EnumPayload { + variant, + index: binding_index as u32, + }, + Ty::Option(_) if variant == 0 && binding_index == 0 => BorrowProjection::OptionSome, + Ty::Result(..) if variant == 0 && binding_index == 0 => BorrowProjection::ResultOk, + Ty::Result(..) if variant == 1 && binding_index == 0 => BorrowProjection::ResultErr, + _ => return BorrowFact::from_direct(scrutinee_fact.flatten()), + }; + self.project_fact_or_flatten( + scrutinee_ty, + scrutinee_fact.clone(), + &[projection], + self.f.locals[binding as usize].ty, + ) + } + fn begin_match_arm( &mut self, prepared: &MoveMatchPrepared, @@ -17270,14 +19074,15 @@ impl<'a> MoveCheck<'a> { } else { prepared.evaluated_borrows.clone() }; - for binding in &arm.bindings { - let roots = if self.local_may_borrow(*binding) { - prepared.scrutinee_roots.clone() - } else { - BorrowRoots::new() - }; - self.borrows - .assign(*binding, BorrowFact::from_direct(roots)); + for (binding_index, binding) in arm.bindings.iter().enumerate() { + let fact = self.match_binding_fact( + prepared.scrutinee_ty, + &prepared.scrutinee_fact, + arm, + binding_index, + *binding, + ); + self.borrows.assign(*binding, fact); clear_moved(moved, *binding); } } @@ -17499,6 +19304,14 @@ impl<'a> MoveCheck<'a> { } else { self.check_borrow_use(*id, e.span); if consuming && self.is_move(*id) { + if self.borrowed_param_mode(*id).is_some() { + let name = &self.f.locals[*id as usize].name; + self.diags.error( + format!("cannot move borrowed parameter '{name}'"), + e.span, + ); + return true; + } if !direct { let name = &self.f.locals[*id as usize].name; self.diags.error( @@ -17540,6 +19353,14 @@ impl<'a> MoveCheck<'a> { || is_move_handle(e.ty) || matches!(e.ty, Ty::Enum(id) if enum_is_move(id, self.structs, self.enums, self.tagged_types))) { + if self.borrowed_param_mode(*base).is_some() { + let name = &self.f.locals[*base as usize].name; + self.diags.error( + format!("cannot move a field out of borrowed parameter '{name}'"), + e.span, + ); + return true; + } // A partial move of a depth-1 owned `string`/`Option` field (`n := u.name`, // `f(u.name)` by value, `return u.name`) — or of a Move **handle** field // (`c.req.respond(rb)`, the pkg.web `Ctx` consuming its request handle), or @@ -17625,10 +19446,31 @@ impl<'a> MoveCheck<'a> { if let Some(dependencies) = self.summary_dependencies.as_deref_mut() { dependencies.insert(func.clone()); } - let consuming = func != "print"; - for a in args { + if let Some(modes) = self.named_param_modes.get(func).cloned() { + self.check_call_borrow_aliases(func, args, &modes); + } + for (index, a) in args.iter().enumerate() { + let consuming = func != "print" + && !matches!( + self.named_param_modes + .get(func) + .and_then(|modes| modes.get(index)), + Some(ast::ParamMode::Borrow | ast::ParamMode::BorrowMut) + ); move_expr!(self, a, moved, consuming, consuming); } + if let Some(mode_count) = self.named_param_modes.get(func).map(Vec::len) { + for index in 0..mode_count { + let mode = self.named_param_modes[func][index]; + if mode == ast::ParamMode::BorrowMut + && let Some(argument) = args.get(index) + { + let roots = self.storage_roots(argument); + self.borrows.invalidate_roots(&roots, BorrowEnd::Consumed); + self.refresh_borrow_mut_place(argument); + } + } + } } // A fn value is Copy (a pointer); an indirect call's callee + args are reads. ExprKind::FnValue(_) => {} @@ -17640,8 +19482,44 @@ impl<'a> MoveCheck<'a> { } ExprKind::CallFnValue { callee, args } => { move_expr!(self, callee, moved, false, false); - for a in args { - move_expr!(self, a, moved, true, true); + let modes = match callee.ty { + Ty::Fn(id) => self.fn_types.get(id as usize).map(|function| { + function.params.iter().map(|(mode, _)| *mode).collect::>() + }), + _ => None, + }; + if let Some(modes) = &modes { + self.check_call_borrow_aliases("function value", args, modes); + } + let mode_count = modes.as_ref().map_or(0, Vec::len); + drop(modes); + for (index, a) in args.iter().enumerate() { + let mode = match callee.ty { + Ty::Fn(id) => self + .fn_types + .get(id as usize) + .and_then(|function| function.params.get(index)) + .map(|(mode, _)| *mode), + _ => None, + }; + let consuming = !matches!( + mode, + Some(ast::ParamMode::Borrow | ast::ParamMode::BorrowMut) + ); + move_expr!(self, a, moved, consuming, consuming); + } + for index in 0..mode_count { + let mode = match callee.ty { + Ty::Fn(id) => self.fn_types[id as usize].params[index].0, + _ => ast::ParamMode::ByValue, + }; + if mode == ast::ParamMode::BorrowMut + && let Some(argument) = args.get(index) + { + let roots = self.storage_roots(argument); + self.borrows.invalidate_roots(&roots, BorrowEnd::Consumed); + self.refresh_borrow_mut_place(argument); + } } } ExprKind::StructLit { fields, .. } => { @@ -17654,10 +19532,9 @@ impl<'a> MoveCheck<'a> { | ExprKind::ResultErr(i) | ExprKind::HeapNew(i) => move_expr!(self, i, moved, true, true), ExprKind::Try(i) => { - // L2b-a1 preserves the conservative flattened Result provenance. L2b-a2 splits - // the implicit Err return edge from the continuing Ok projection. Do not attach - // that flattened union to a return type that cannot carry any borrow: the operand's - // Ok payload may borrow even though the propagated Err and enclosing return do not. + // Keep the same guard as the final return summary so a borrowing Ok payload cannot + // taint an enclosing Result whose returned paths carry no borrow. + let error_roots = self.try_error_roots(i); move_expr!(self, i, moved, true, true); if ty_may_borrow( self.f.ret, @@ -17666,7 +19543,7 @@ impl<'a> MoveCheck<'a> { self.enums, self.tagged_types, ) { - self.return_roots.extend(self.borrow_sources(i)); + self.return_roots.extend(error_roots); } } // `b.to_string()` consumes (moves) the builder; `b.write(...)` borrows it (and its @@ -19463,8 +21340,25 @@ impl<'a, 't> Checker<'a, 't> { p.ty.span(), ); } + if p.mode == ast::ParamMode::Borrow + && ty != Ty::Error + && !matches!(ty, Ty::Param(_)) + && !ty_is_move(ty, self.structs, self.tuples, self.enums, self.tagged_types) + { + self.diags.error( + format!( + "a borrowed parameter must be a Move type, got {}", + ty_name(ty) + ), + p.ty.span(), + ); + } self.check_shadow(&p.name.name, p.name.span, self.scope.len()); - let id = self.declare(&p.name.name, ty, p.mode.is_out()); + let id = self.declare( + &p.name.name, + ty, + p.mode.is_out() || p.mode == ast::ParamMode::BorrowMut, + ); if let Some(spelling) = self.json_scan_row_source_spelling(&p.ty) { self.json_scan_local_spellings.insert(id, spelling); } @@ -19508,6 +21402,7 @@ impl<'a, 't> Checker<'a, 't> { ret: self.finalize(ret), return_borrow: sig.return_borrow.clone(), return_region: sig.return_region.clone(), + return_cleanup: hir::ReturnCleanupAbi::None, locals, body, span: f.span, @@ -21917,6 +23812,75 @@ impl<'a, 't> Checker<'a, 't> { self.check_indirect_call(callee, args, expected, span) } + fn validate_borrow_argument( + &mut self, + argument: &Expr, + mode: ast::ParamMode, + display: &str, + ) { + if !matches!(mode, ast::ParamMode::Borrow | ast::ParamMode::BorrowMut) { + return; + } + let root = match &argument.kind { + ExprKind::Local(local) => Some(*local), + ExprKind::Field { root, .. } => Some(*root), + _ => None, + }; + let Some(root) = root else { + self.diags.error( + format!( + "the {mode:?} argument to '{display}' must be a stable named local or field, not a temporary value" + ), + argument.span, + ); + return; + }; + if mode == ast::ParamMode::Borrow + && !ty_is_move( + argument.ty, + self.structs, + self.tuples, + self.enums, + self.tagged_types, + ) + { + self.diags.error( + format!( + "the {mode:?} argument to '{display}' must have a Move type, got {}", + self.ty_display(argument.ty) + ), + argument.span, + ); + } + if mode == ast::ParamMode::BorrowMut + && matches!(argument.kind, ExprKind::Field { .. }) + && ty_is_move( + argument.ty, + self.structs, + self.tuples, + self.enums, + self.tagged_types, + ) + { + self.diags.error( + format!( + "cannot exclusively borrow a partial Move field for '{display}'; borrow the whole owner" + ), + argument.span, + ); + } + if mode == ast::ParamMode::BorrowMut + && !self.locals.get(root as usize).is_some_and(|local| local.is_mut) + { + self.diags.error( + format!( + "the exclusive borrowed argument to '{display}' must be rooted in mutable storage" + ), + argument.span, + ); + } + } + /// The shared body of an indirect call: `callee` is an already-checked expression of a /// **function-value** type (`Ty::Fn`) — a fn-value local (`f(args)`), or a struct's fn-typed /// field read (`route.handler(args)`, F1① of the pkg.web plan). Checks arity + argument types @@ -21941,14 +23905,6 @@ impl<'a, 't> Checker<'a, 't> { } let mut checked = Vec::with_capacity(args.len()); for (a, (mode, p)) in args.iter().zip(¶ms) { - if *mode != ast::ParamMode::ByValue { - self.diags.error( - "this function-value parameter mode is not callable until its borrow slice lands" - .to_string(), - span, - ); - return err; - } let pt = scalar_to_ty(*p); let e = self.check_expr(a, Some(pt)); if e.ty != Ty::Error && !self.source_ty_matches(e.ty, pt) { @@ -21957,6 +23913,7 @@ impl<'a, 't> Checker<'a, 't> { e.span, ); } + self.validate_borrow_argument(&e, *mode, "function value"); checked.push(e); } self.constrain(ret, expected, span); @@ -22098,6 +24055,7 @@ impl<'a, 't> Checker<'a, 't> { &name, &type_params, ¶m_tys, + ¶m_modes, ret, &json_scan_param_spellings, args, @@ -22194,6 +24152,11 @@ impl<'a, 't> Checker<'a, 't> { ); } } + for (index, mode) in param_modes.iter().copied().enumerate() { + if let Some(argument) = checked.get(index) { + self.validate_borrow_argument(argument, mode, &name); + } + } Expr { kind: ExprKind::Call { func: name, args: checked, type_args: Vec::new() }, ty: ret, span } } @@ -22213,6 +24176,7 @@ impl<'a, 't> Checker<'a, 't> { name: &str, type_params: &[String], param_tys: &[Ty], + param_modes: &[ast::ParamMode], ret: Ty, json_scan_param_spellings: &[Option], args: &[ast::Expr], @@ -22312,6 +24276,11 @@ impl<'a, 't> Checker<'a, 't> { } checked.push(ce); } + for (index, mode) in param_modes.iter().copied().enumerate() { + if let Some(argument) = checked.get(index) { + self.validate_borrow_argument(argument, mode, name); + } + } // A parameter that appears *nested* (inside `Option` / `Result<…>` / …) must resolve to a // concrete scalar now — a `Scalar` cannot hold an inference variable, so leaving it deferred // would leak a `Param` into the result type and downstream checking. Finalize those eagerly @@ -24352,6 +26321,7 @@ impl<'a, 't> Checker<'a, 't> { ret, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals, body: body_fin, span, @@ -34500,6 +36470,7 @@ fn intern_fn_type( ret, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, effect: std::cell::Cell::new(FnEffect::Unknown), }; if let Some(i) = fn_types.iter().position(|t| *t == ft) { @@ -34523,6 +36494,7 @@ fn fresh_fn_type( ret, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, effect: std::cell::Cell::new(effect), }); (fn_types.len() - 1) as u32 @@ -34612,11 +36584,14 @@ fn resolve_type( ); return Ty::Error; } - if matches!(p.mode, ast::ParamMode::Borrow | ast::ParamMode::BorrowMut) { + if p.mode == ast::ParamMode::Borrow + && !matches!(pty, Ty::Param(_)) + && !ty_is_move(pty, cx.structs, cx.tuples, cx.enums, cx.tagged_types) + { diags.error( format!( - "function-type parameter mode {:?} is not enabled in L2a", - p.mode + "a borrowed function-type parameter must be a Move type, got {}", + ty_name(pty) ), p.ty.span(), ); @@ -36006,16 +37981,23 @@ fn main() -> i32 = 0 .find(|function| function.name == "probe") .expect("probe function"); let named = std::collections::HashMap::new(); + let named_modes = std::collections::HashMap::new(); + let callable_targets = vec![CallableTargetSet::new(); program.fn_types.len()]; + let callable_target_ids = std::collections::HashMap::new(); let mut sink = Diagnostics::new(); let mut checker = MoveCheck { f: function, diags: &mut sink, named_return_borrow: &named, + named_param_modes: &named_modes, summary_dependencies: None, tuples: &program.tuples, structs: &program.structs, enums: &program.enums, tagged_types: &program.tagged_types, + fn_types: &program.fn_types, + callable_targets: &callable_targets, + callable_target_ids: &callable_target_ids, loop_breaks: Vec::new(), borrows: BorrowState::default(), next_pipeline_snapshot: 0, @@ -36115,6 +38097,11 @@ fn main() -> i32 = 0 let pair_local = function.params[0]; let replacement_local = function.params[1]; + let mut all_with_local_generations = all.clone(); + all_with_local_generations.extend([ + BorrowRoot::Local(pair_local), + BorrowRoot::Local(replacement_local), + ]); let pair_fact = checker.normalize_borrow_fact( pair_ty, BorrowFact::from_direct([BorrowRoot::Param(0)].into_iter().collect()), @@ -36167,8 +38154,8 @@ fn main() -> i32 = 0 Ty::Str, ) .flatten(), - all, - "a malformed product constructor must flatten every child root" + all_with_local_generations, + "a malformed product constructor must flatten every child parameter and local-generation root" ); } @@ -36198,8 +38185,8 @@ fn main() -> i32 = 0 Ty::Str, ) .flatten(), - all, - "a malformed product write must retain old and incoming roots" + all_with_local_generations, + "a malformed product write must retain old and incoming parameter and local-generation roots" ); } } @@ -39776,6 +41763,7 @@ fn exit_branch(flag: bool) -> i64 { ret: Ty::Int(IntTy { bits: 64, signed: true }), return_borrow: borrow, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, effect: std::cell::Cell::new(FnEffect::Pure), }; let by_value = function(ast::ParamMode::ByValue, hir::ReturnBorrowSummary::None); @@ -39974,6 +41962,7 @@ fn exit_branch(flag: bool) -> i64 { }, return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, effect: std::cell::Cell::new(FnEffect::Unknown), }) .collect::>(); diff --git a/crates/align_sema/src/replay_clone.rs b/crates/align_sema/src/replay_clone.rs index e6109ed4..7c374c71 100644 --- a/crates/align_sema/src/replay_clone.rs +++ b/crates/align_sema/src/replay_clone.rs @@ -1388,6 +1388,7 @@ fn clone_function(function: &hir::Fn) -> Option { ret: function.ret, return_borrow: function.return_borrow.clone(), return_region: function.return_region.clone(), + return_cleanup: function.return_cleanup, locals: function.locals.clone(), body, span: function.span, @@ -1474,6 +1475,7 @@ fn drop_functions(fns: Vec) { ret, return_borrow, return_region, + return_cleanup, locals, body, span, @@ -1489,6 +1491,7 @@ fn drop_functions(fns: Vec) { ret, return_borrow, return_region, + return_cleanup, locals, span, drop_locals, @@ -2319,6 +2322,7 @@ mod tests { ret: int_ty(), return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, locals: Vec::new(), body, span: Span::new(0, 0, 0), @@ -2426,6 +2430,7 @@ mod tests { ret: int_ty(), return_borrow: hir::ReturnBorrowSummary::None, return_region: hir::ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, effect: std::cell::Cell::new(FnEffect::Impure), }); From 78c9ad65855eb60fdd894723ec3b0a4d60597178 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Wed, 5 Aug 2026 23:59:50 +0900 Subject: [PATCH 2/2] fix(compiler): preserve borrow return ABI contracts --- crates/align_codegen_llvm/src/lib.rs | 113 +++++++++++++++++- .../align_driver/tests/move_return_cleanup.rs | 35 ++++++ crates/align_sema/src/lib.rs | 4 +- 3 files changed, 145 insertions(+), 7 deletions(-) diff --git a/crates/align_codegen_llvm/src/lib.rs b/crates/align_codegen_llvm/src/lib.rs index 7f1be0c4..5b150c60 100644 --- a/crates/align_codegen_llvm/src/lib.rs +++ b/crates/align_codegen_llvm/src/lib.rs @@ -1180,7 +1180,13 @@ fn build_module<'c>( }; let thunk = module.add_function(emitted_name, thunk_ty, None); mark_nounwind(ctx, thunk); - mark_borrow_param_contracts_at(ctx, thunk, &declaration.signature.modes, 1); + mark_borrow_param_contracts_at( + ctx, + thunk, + &declaration.signature.modes, + &declaration.signature.borrow, + 1, + ); mark_private_helper(thunk); let bb = ctx.append_basic_block(thunk, "entry"); let tb = ctx.create_builder(); @@ -1271,7 +1277,7 @@ fn build_module<'c>( }; let thunk = module.add_function(emitted_name, thunk_ty, None); mark_nounwind(ctx, thunk); - mark_borrow_param_contracts_at(ctx, thunk, explicit_modes, 1); + mark_borrow_param_contracts_at(ctx, thunk, explicit_modes, &explicit_signature.borrow, 1); mark_private_helper(thunk); let bb = ctx.append_basic_block(thunk, "entry"); let tb = ctx.create_builder(); @@ -4661,7 +4667,7 @@ fn declare_fn<'c>( }; let fv = module.add_function(symbol, fn_ty, None); mark_nounwind(ctx, fv); - mark_borrow_param_contracts(ctx, fv, &f.param_modes); + mark_borrow_param_contracts(ctx, fv, &f.param_modes, &f.return_borrow); // Every Align program function is module-private (internal) EXCEPT: // - the C entry: an `-> i32` `main` keeps the symbol name `main` and IS the C entry (`crt0` // resolves it by name), so it must stay external. A `Result`- or `Unit`-returning main body @@ -4737,7 +4743,7 @@ fn declare_imported_fn<'c>( }; let fv = module.add_function(&encoded_program_symbol(&imp.name), fn_ty, None); mark_nounwind(ctx, fv); - mark_borrow_param_contracts(ctx, fv, &imp.param_modes); + mark_borrow_param_contracts(ctx, fv, &imp.param_modes, &imp.return_borrow); fv } @@ -4745,14 +4751,16 @@ fn mark_borrow_param_contracts( ctx: &Context, function: FunctionValue<'_>, modes: &[align_ast::ParamMode], + return_borrow: &hir::ReturnBorrowSummary, ) { - mark_borrow_param_contracts_at(ctx, function, modes, 0); + mark_borrow_param_contracts_at(ctx, function, modes, return_borrow, 0); } fn mark_borrow_param_contracts_at( ctx: &Context, function: FunctionValue<'_>, modes: &[align_ast::ParamMode], + return_borrow: &hir::ReturnBorrowSummary, offset: u32, ) { for (index, mode) in modes.iter().copied().enumerate() { @@ -4761,7 +4769,14 @@ fn mark_borrow_param_contracts_at( } let location = inkwell::attributes::AttributeLoc::Param(index as u32 + offset); add_enum_attr(ctx, function, location, "nonnull"); - add_valued_enum_attr(ctx, function, location, "captures", CAPTURES_NONE); + let returned = matches!( + return_borrow, + hir::ReturnBorrowSummary::Roots { params, .. } + if params.binary_search(&(index as u32)).is_ok() + ); + if !returned { + add_valued_enum_attr(ctx, function, location, "captures", CAPTURES_NONE); + } add_enum_attr(ctx, function, location, "readonly"); } } @@ -13530,6 +13545,92 @@ mod tests { assert_lowering(error, "callable target invalid:7265706c616365"); } + #[test] + fn returned_borrow_roots_do_not_claim_captures_none() { + let source = "fn size(borrow value: string) -> i64 = value.len()\n\ + fn view(borrow value: string) -> slice = value.bytes()\n\ + fn main() -> i32 { value := \"align\".clone(); bytes := view(value); return (size(value) + bytes.len()) as i32 }\n"; + let captures = enum_kind_id("captures"); + let readonly = enum_kind_id("readonly"); + let has = |function: FunctionValue<'_>, index: u32, kind| { + function + .get_enum_attribute(inkwell::attributes::AttributeLoc::Param(index), kind) + .is_some() + }; + let assert_contracts = |module: &Module<'_>| { + let view = module + .get_function(&encoded_program_symbol(&program_call("view"))) + .expect("view declaration"); + let size = module + .get_function(&encoded_program_symbol(&program_call("size"))) + .expect("size definition"); + assert!(has(view, 0, readonly)); + assert!( + !has(view, 0, captures), + "a returned borrow root is captured by the return value" + ); + assert!(has(size, 0, readonly)); + assert!( + has(size, 0, captures), + "a non-returned shared borrow remains captures(none)" + ); + }; + + let direct = mir(source); + let ctx = Context::create(); + let module = ctx.create_module("returned_borrow_direct"); + let tm = create_target_machine(&BuildTarget::Baseline, OptimizationLevel::Default).unwrap(); + build_module(&ctx, &module, &direct, &tm, None, &[], false).unwrap(); + assert_contracts(&module); + let ptr = ctx.ptr_type(AddressSpace::default()); + let thunk = module.add_function( + "returned_borrow_thunk_probe", + ctx.void_type().fn_type(&[ptr.into(), ptr.into()], false), + None, + ); + mark_borrow_param_contracts_at( + &ctx, + thunk, + &[align_ast::ParamMode::Borrow], + &hir::ReturnBorrowSummary::Roots { + params: vec![0], + captures: vec![], + }, + 1, + ); + assert!(has(thunk, 1, readonly)); + assert!( + !has(thunk, 1, captures), + "a generated thunk must apply return roots before its environment offset" + ); + + let mut imported = mir(source); + let view_index = imported + .fns + .iter() + .position(|function| function.name.as_str() == "view") + .expect("view definition"); + let view = imported.fns.remove(view_index); + imported.imported_fns.push(align_mir::ImportedFn { + name: view.name, + params: view + .params + .iter() + .map(|slot| view.slots[*slot as usize]) + .collect(), + param_modes: view.param_modes, + ret: view.ret, + return_borrow: view.return_borrow, + return_region: view.return_region, + return_cleanup: view.return_cleanup, + }); + let ctx = Context::create(); + let module = ctx.create_module("returned_borrow_imported"); + let tm = create_target_machine(&BuildTarget::Baseline, OptimizationLevel::Default).unwrap(); + build_module(&ctx, &module, &imported, &tm, None, &[], false).unwrap(); + assert_contracts(&module); + } + #[test] fn runtime_abi_source_compatible_externs_receive_each_attribute_class() { let program = mir( diff --git a/crates/align_driver/tests/move_return_cleanup.rs b/crates/align_driver/tests/move_return_cleanup.rs index b1b83813..3d6a7f33 100644 --- a/crates/align_driver/tests/move_return_cleanup.rs +++ b/crates/align_driver/tests/move_return_cleanup.rs @@ -44,6 +44,21 @@ fn main() -> i32 = option_len(owned(1)) + copy() "#; +const LOCAL_FUNCTION_VALUE_SOURCE: &str = r#" +fn fallible(ok: bool) -> Result = + if ok { Ok("ok".clone()) } else { Err("error".clone()) } + +fn result_len(value: Result) -> i32 = match value { + Ok(text) => text.len() as i32 + Err(text) => text.len() as i32 +} + +fn main() -> i32 { + handler := fallible + return result_len(handler(false)) +} +"#; + fn mir_text(source: &str) -> String { let mut source_map = SourceMap::new(); let checked = check(&mut source_map, "move-return-cleanup.align", source); @@ -79,6 +94,26 @@ fn move_return_cleanup_executes_none_some_try_and_map_err_paths() { assert_eq!(build_and_run("move-return-cleanup", SOURCE).status.code(), Some(25)); } +#[test] +fn local_named_function_value_preserves_move_return_cleanup_abi() { + let mir = mir_text(LOCAL_FUNCTION_VALUE_SOURCE); + assert!( + mir.contains("call_indirect_with_cleanup"), + "a local named function value must retain the target's DynamicBit return ABI:\n{mir}" + ); + if backend_available() { + assert_eq!( + build_and_run( + "move-return-local-function-value", + LOCAL_FUNCTION_VALUE_SOURCE + ) + .status + .code(), + Some(5), + ); + } +} + #[test] fn imported_move_return_cleanup_matches_whole_program_and_per_unit_abi() { if !backend_available() { diff --git a/crates/align_sema/src/lib.rs b/crates/align_sema/src/lib.rs index af573ff9..6b2c1093 100644 --- a/crates/align_sema/src/lib.rs +++ b/crates/align_sema/src/lib.rs @@ -5986,15 +5986,17 @@ fn prepare_local_fn_types(program: &mut Program) { let Some(source) = fn_types.get(fid as usize) else { continue; }; - let (params, ret, return_borrow, return_region) = ( + let (params, ret, return_borrow, return_region, return_cleanup) = ( source.params.clone(), source.ret, source.return_borrow.clone(), source.return_region.clone(), + source.return_cleanup, ); let fresh = fresh_fn_type(fn_types, params, ret, FnEffect::Unknown); fn_types[fresh as usize].return_borrow = return_borrow; fn_types[fresh as usize].return_region = return_region; + fn_types[fresh as usize].return_cleanup = return_cleanup; local.ty = Ty::Fn(fresh); } }