diff --git a/HANDOFF.md b/HANDOFF.md index 5255d99b..cad30314 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -258,10 +258,9 @@ admits one direct existing-`Scalar` Move payload per tagged arm; L1b-b (#669) ad payload partial construction and uniform ownership; L1b-c (#670) adds tagged-in-tagged type representation plus the exact `Result, DbError>` acceptance. Dynamic path-selected return cleanup bits remain L2; L1b accepts only values proven free-standing by the -current ABI rules. L2a is complete in #672. L2b is split into L2b-a1 named/direct/imported -parameter-root inference with conservative aggregate and indirect unions, L2b-a2 exact aggregate -and control projection, and L2b-b closure/capture/function-value provenance. PR #673 carries -L2b-a1. Its hand-written diff exceeds 1,000 lines because producer inference, consumer import +current ABI rules. L2a is complete in #672. L2b-a1 named/direct/imported parameter-root inference +with conservative aggregate and indirect unions is complete in #673. Its hand-written diff exceeds +1,000 lines because producer inference, consumer import validation, whole/per-unit serialization parity, source-order reachability, and their owner tests form one compatibility boundary: splitting them would either publish unvalidated provenance or consume provenance whose producer semantics are not yet closed. The closure matrix therefore @@ -288,10 +287,27 @@ borrow-preserving `if`/`match` sources retain selected owner roots until action; success payload and a value-producing loop instead transfer and null their old container/source. Terminating return and outer-break paths remove the analysis snapshot from current and saved loop states. +The next work is L2b-a2, now split by its closure matrix into three mergeable verticals: +L2b-a2-s adds the finite projection fact and exact struct/tuple construction, selection, partial +replacement, destructuring, and block/`if`/loop behavior; L2b-a2-a then adds fixed-array and +element selection/replacement plus pipeline `Project`/`WhereField`; L2b-a2-t completes +user-sum/`Option`/`Result`, `match`, `else`, `?`, and `map_err`. The original two-way split was +narrowed before PR preflight when the aggregate implementation and owner tests exceeded the +repository's roughly 1,000 changed-hand-written-line review bound. The public interface remains the +L2a parameter-index summary, so a single aggregate actual deliberately remains conservative. +Unknown extern and indirect targets retain the all-compatible-input fallback. +The final L2b-a2-s vertical is approximately 1,900 lines because its adversarial review required +malformed constructor/read/write fail-closed validation, common eager-child source-order snapshots, +snapshot-generation invalidation, checked-expression identity, action-boundary validation, and +discriminating deferred-array/liveness owners in the same PR; separating any of them would publish +an under-approximating or dangling fact. +Its final local provenance benchmark reports 3.147 ms/check, 22,848 interface bytes, and +1.844 ms/import on Apple Silicon. Do not begin a SQLite/PostgreSQL driver or add database-named compiler -variants before L1a–L7 are complete. L2 is implemented as seven closed slices so no incomplete -borrow surface is exposed: L2a parameter-mode and provenance-summary representation, L2b-a1/a2/b +variants before L1a–L7 are complete. L2 is implemented as seven conceptual milestones in nine +closed PRs so no incomplete borrow surface is exposed: L2a parameter-mode and provenance-summary +representation, L2b-a1/a2-s/a2-a/a2-t/b return-provenance slices, L2c cleanup-ABI record plus dynamic Move-return bit, L2d shared borrow, then L2e mutable borrow/out and all-peer diff --git a/crates/align_driver/tests/return_provenance.rs b/crates/align_driver/tests/return_provenance.rs index 4fc4d772..36e1b67c 100644 --- a/crates/align_driver/tests/return_provenance.rs +++ b/crates/align_driver/tests/return_provenance.rs @@ -105,6 +105,626 @@ pub fn choose(first: str, second: str, take_first: bool) -> str { ); } +#[test] +fn product_projection_summaries_select_exact_parameter_roots() { + let files = &[ + ( + "views.align", + "\ +module views +pub Inner { left: str, right: str } +pub Outer { inner: Inner, ignored: str } + +pub fn nested_field(left: str, right: str, ignored: str) -> str { + value := Outer { + inner: Inner { left: left, right: right } + ignored: ignored + } + return value.inner.left +} + +pub fn replaced_field(left: str, right: str) -> str { + mut value := Inner { left: left, right: right } + value.left = right + return value.left +} + +pub fn replaced_sibling(left: str, right: str) -> str { + mut value := Inner { left: left, right: right } + value.left = \"fixed\" + return value.right +} + +pub fn replaced_nested_field(left: str, right: str) -> str { + mut value := Outer { + inner: Inner { left: left, right: \"fixed\" } + ignored: \"fixed\" + } + value.inner.left = right + return value.inner.left +} + +pub fn whole_local_replacement(left: str, right: str) -> str { + mut value := Inner { left: left, right: \"fixed\" } + value = Inner { left: right, right: \"fixed\" } + return value.left +} + +pub fn field_self_assignment(left: str, right: str) -> str { + mut value := Inner { left: left, right: right } + value.left = value.left + return value.left +} + +pub fn tuple_index(left: str, right: str) -> str { + value := (left, right) + return value.0 +} + +pub fn struct_child_snapshot(first: str, second: str) -> str { + mut current := first + value := Inner { + left: current + right: { + saved := current + current = second + saved + } + } + return value.left +} + +pub fn tuple_child_snapshot(first: str, second: str) -> str { + mut current := first + value := ( + current, + { + saved := current + current = second + saved + }, + ) + return value.0 +} + +pub fn take_first(first: str, ignored: str) -> str = first + +pub fn call_child_snapshot(first: str, second: str) -> str { + mut current := first + return take_first( + current, + { + current = second + current + }, + ) +} + +pub fn tuple_binding(left: str, right: str) -> str { + (selected, _) := (left, right) + return selected +} + +pub fn control_tuple(left: str, right: str, choose: bool) -> str { + value := if choose { (left, \"fixed\") } else { (right, \"fixed\") } + return value.0 +} + +pub fn branch_reassignment( + left: str, + right: str, + then_value: str, + else_value: str, + choose: bool, +) -> str { + mut value := Inner { left: left, right: right } + return if choose { + value.left = then_value + value.left + } else { + value.right = else_value + value.right + } +} + +pub fn branch_loop(left: str, right: str, choose: bool) -> str { + value := loop { + break if choose { + Outer { + inner: Inner { left: left, right: \"fixed\" } + ignored: \"fixed\" + } + } else { + Outer { + inner: Inner { left: right, right: \"fixed\" } + ignored: \"fixed\" + } + } + } + return value.inner.left +} + +pub fn loop_reassignment(left: str, right: str, choose: bool) -> str { + mut value := Inner { left: left, right: \"fixed\" } + return loop { + if choose { + value.left = right + break value.left + } + break value.left + } +} + +pub fn deferred_array_write(first: str, second: str) -> str { + mut values := [\"fixed\", \"fixed\"] + values[0] = first + values[1] = second + return values[0] +} + +pub fn deferred_array_child_snapshot(first: str, second: str) -> str { + mut current := first + values := [ + current, + { + current = second + current + }, + ] + return values[0] +} + +pub fn deferred_element_field_write(first: str, second: str) -> str { + mut values := [Inner { left: \"fixed\", right: \"fixed\" }] + values[0].left = first + values[0].right = second + return values[0].left +} + +pub fn deferred_pipeline_projection(first: str, second: str) -> str { + values := [Inner { left: first, right: second }].left.to_array() + return values[0] +} + +", + ), + ("main.align", "import views\nfn main() -> i32 = 0\n"), + ]; + let differential = + diff_check_multi("l2b-a2-product-projections", files, "main.align"); + assert_eq!( + differential.whole_errors, differential.per_unit_errors, + "verdict mismatch:\nwhole:\n{}\nper-unit:\n{}", + differential.whole_diags, differential.per_unit_diags + ); + assert!( + !differential.per_unit.diags.has_errors(), + "product projection fixture must check:\nwhole:\n{}\nper-unit:\n{}", + differential.whole_diags, + differential.per_unit_diags + ); + let summary = differential + .per_unit + .summaries + .iter() + .find(|summary| summary.unit == "views") + .expect("views summary"); + let find = |name: &str| { + summary + .fns + .iter() + .find(|function| function.name == name) + .unwrap_or_else(|| panic!("{name} signature")) + }; + for name in [ + "nested_field", + "tuple_index", + "struct_child_snapshot", + "tuple_child_snapshot", + "call_child_snapshot", + "tuple_binding", + "field_self_assignment", + ] { + assert_eq!( + find(name).return_borrow, + roots(&[0], &[]), + "{name} must retain only the selected first parameter" + ); + } + for name in [ + "replaced_field", + "replaced_sibling", + "replaced_nested_field", + "whole_local_replacement", + ] { + assert_eq!( + find(name).return_borrow, + roots(&[1], &[]), + "{name} must retain only the installed/selected second parameter" + ); + } + for name in ["control_tuple", "branch_loop", "loop_reassignment"] { + assert_eq!( + find(name).return_borrow, + roots(&[0, 1], &[]), + "{name} must retain every runtime-selectable parameter" + ); + } + assert_eq!( + find("branch_reassignment").return_borrow, + roots(&[2, 3], &[]), + "branch result facts must be captured before branch-local states join" + ); + for name in [ + "deferred_array_write", + "deferred_array_child_snapshot", + "deferred_element_field_write", + "deferred_pipeline_projection", + ] { + assert_eq!( + find(name).return_borrow, + roots(&[0, 1], &[]), + "{name} must retain both roots until array/pipeline projection lands" + ); + } +} + +#[test] +fn source_order_snapshots_drive_imported_owner_liveness() { + let producer = ( + "views.align", + "\ +module views +pub fn take_first(first: str, ignored: str) -> str = first +pub fn call_child_snapshot(first: str, second: str) -> str { + mut current := first + return take_first( + current, + { + current = second + current + }, + ) +} +pub fn array_child_snapshot(first: str, second: str) -> str { + mut current := first + values := [ + current, + { + current = second + current + }, + ] + return values[0] +} +", + ); + let unselected_named = assert_same_verdict( + "l2b-a2-source-order-named-unselected-owner", + &[ + producer, + ( + "main.align", + "\ +import views +fn consume(value: string) -> i64 = value.len() +fn main() -> i32 { + first := \"first\".clone() + second := \"second\".clone() + result := views.call_child_snapshot(first, second) + consume(second) + return result.len() as i32 +} +", + ), + ], + "main.align", + ); + assert!( + !unselected_named.diags.has_errors(), + "a later named-call argument must not replace the selected earlier argument's owner" + ); + + let selected_named = assert_same_verdict( + "l2b-a2-source-order-named-selected-owner", + &[ + producer, + ( + "main.align", + "\ +import views +fn consume(value: string) -> i64 = value.len() +fn main() -> i32 { + first := \"first\".clone() + second := \"second\".clone() + result := views.call_child_snapshot(first, second) + consume(first) + return result.len() as i32 +} +", + ), + ], + "main.align", + ); + assert!( + selected_named.diags.has_errors(), + "a named-call result must retain the completion-time owner of its selected argument" + ); + + let selected_array = assert_same_verdict( + "l2b-a2-source-order-array-selected-owner", + &[ + producer, + ( + "main.align", + "\ +import views +fn consume(value: string) -> i64 = value.len() +fn main() -> i32 { + first := \"first\".clone() + second := \"second\".clone() + result := views.array_child_snapshot(first, second) + consume(first) + return result.len() as i32 +} +", + ), + ], + "main.align", + ); + assert!( + selected_array.diags.has_errors(), + "the deferred array fallback must retain the earlier runtime element's owner" + ); + + for (name, source) in [ + ( + "product", + "\ +Pair { left: str, right: str } +fn main() -> i32 { + mut owner := \"first\".clone() + current: str := owner + pair := Pair { + left: current + right: { + owner = \"second\".clone() + \"fixed\" + } + } + return pair.left.len() as i32 +} +", + ), + ( + "named-call", + "\ +fn take_first(first: str, ignored: str) -> str = first +fn main() -> i32 { + mut owner := \"first\".clone() + current: str := owner + result := take_first( + current, + { + owner = \"second\".clone() + \"fixed\" + }, + ) + return result.len() as i32 +} +", + ), + ( + "direct-result-use", + "\ +fn take_first(first: str, ignored: str) -> str = first +fn main() -> i32 { + mut owner := \"first\".clone() + current: str := owner + return take_first( + current, + { + owner = \"second\".clone() + \"fixed\" + }, + ).len() as i32 +} +", + ), + ( + "non-borrowing-call-result", + "\ +fn observe(first: str, ignored: str) -> i64 = first.len() +fn main() -> i32 { + mut owner := \"first\".clone() + current: str := owner + value := observe( + current, + { + owner = \"second\".clone() + \"fixed\" + }, + ) + return value as i32 +} +", + ), + ( + "loop-probe-action", + "\ +fn observe(first: str, ignored: str) -> i64 = first.len() +fn main() -> i32 { + loop { + mut owner := \"first\".clone() + current: str := owner + value := observe( + current, + { + owner = \"second\".clone() + \"fixed\" + }, + ) + break value as i32 + } +} +", + ), + ] { + assert!( + check_errs(&format!("l2b-a2-source-order-ended-{name}"), source), + "a later eager sibling must invalidate the captured earlier owner generation ({name})" + ); + } + + assert!( + !check_errs( + "l2b-a2-source-order-terminating-later-operand", + "\ +import std.process +fn observe(first: str, ignored: ()) -> i64 = first.len() +fn main() -> i32 { + mut owner := \"first\".clone() + current: str := owner + value := observe( + current, + { + owner = \"second\".clone() + process.abort() + }, + ) + return value as i32 +} +", + ), + "a terminating later operand performs no enclosing call action and must not validate its earlier snapshot" + ); + + assert!( + check_errs( + "l2b-a2-source-order-wrapper-expression-identity", + "\ +Row { owned: string, view: str } +fn main() -> i32 { + owner := \"source\".clone() + view: str := owner + mut keep: slice := [Row { owned: \"initial\".clone(), view: view }] + mut done := false + loop { + keep = [Row { owned: \"iteration\".clone(), view: view }] + if done { + break + } + done = true + } + return keep.len() as i32 +} +", + ), + "a synthetic ArrayToSlice wrapper must keep its own IterTemp root instead of aliasing its child by Span" + ); +} + +#[test] +fn imported_aggregate_projection_is_exact_only_at_parameter_root_granularity() { + let selected_files = &[ + ( + "views.align", + "\ +module views +pub Pair { selected: str, ignored: str } +pub fn selected(first: str, second: str) -> str { + pair := Pair { selected: first, ignored: second } + return pair.selected +} +", + ), + ( + "main.align", + "\ +import views +fn consume(value: string) -> i64 = value.len() +fn main() -> i32 { + first := \"first\".clone() + second := \"second\".clone() + result := views.selected(first, second) + consume(second) + return result.len() as i32 +} +", + ), + ]; + let selected = assert_same_verdict( + "l2b-a2-imported-selected-parameter", + selected_files, + "main.align", + ); + assert!( + !selected.diags.has_errors(), + "an imported scalar projection must not retain an unselected parameter" + ); + let selected_owner_files = &[ + selected_files[0], + ( + "main.align", + "\ +import views +fn consume(value: string) -> i64 = value.len() +fn main() -> i32 { + first := \"first\".clone() + second := \"second\".clone() + result := views.selected(first, second) + consume(first) + return result.len() as i32 +} +", + ), + ]; + let selected_owner = assert_same_verdict( + "l2b-a2-imported-selected-owner", + selected_owner_files, + "main.align", + ); + assert!( + selected_owner.diags.has_errors(), + "an imported scalar projection must retain its selected parameter owner" + ); + + let aggregate_files = &[ + ( + "views.align", + "\ +module views +pub Pair { selected: str, ignored: str } +pub fn selected(pair: Pair) -> str = pair.selected +", + ), + ( + "main.align", + "\ +import views +fn consume(value: string) -> i64 = value.len() +fn main() -> i32 { + first := \"first\".clone() + second := \"second\".clone() + pair := views.Pair { selected: first, ignored: second } + result := views.selected(pair) + consume(second) + return result.len() as i32 +} +", + ), + ]; + let aggregate = assert_same_verdict( + "l2b-a2-imported-aggregate-limit", + aggregate_files, + "main.align", + ); + assert!( + aggregate.diags.has_errors(), + "the parameter-index-only interface must conservatively retain every owner in one aggregate actual" + ); +} + #[test] fn caller_before_callee_and_mutual_recursion_converge_independently_of_declaration_order() { let files = &[ diff --git a/crates/align_sema/src/lib.rs b/crates/align_sema/src/lib.rs index 0d96bdf8..db7c7eb5 100644 --- a/crates/align_sema/src/lib.rs +++ b/crates/align_sema/src/lib.rs @@ -4671,7 +4671,11 @@ pub fn check_program_with_interface_facts( next_pipeline_snapshot: 0, loop_borrow_breaks: Vec::new(), loop_value_breaks: Vec::new(), - loop_value_roots: std::collections::HashMap::new(), + loop_value_facts: std::collections::HashMap::new(), + control_value_facts: std::collections::HashMap::new(), + walked_value_facts: std::collections::HashMap::new(), + value_snapshot_frames: Vec::new(), + reported_invalid_value_actions: std::collections::HashSet::new(), loop_iter_drops: Vec::new(), arena_depth: 0, return_roots: BorrowRoots::new(), @@ -4743,7 +4747,11 @@ fn summary_from_roots(roots: &BorrowRoots) -> hir::ReturnBorrowSummary { for root in roots { match root { BorrowRoot::Param(index) => params.push(*index), - BorrowRoot::Local(_) | BorrowRoot::IterTemp(_) => {} + BorrowRoot::Local(_) + | BorrowRoot::IterTemp(_) + | BorrowRoot::EndedLocal(_, _) + | BorrowRoot::EndedIterTemp(_, _) + | BorrowRoot::EndedParam(_, _) => {} } } if params.is_empty() { @@ -4826,7 +4834,11 @@ fn infer_return_provenance( next_pipeline_snapshot: 0, loop_borrow_breaks: Vec::new(), loop_value_breaks: Vec::new(), - loop_value_roots: std::collections::HashMap::new(), + loop_value_facts: std::collections::HashMap::new(), + control_value_facts: std::collections::HashMap::new(), + walked_value_facts: std::collections::HashMap::new(), + value_snapshot_frames: Vec::new(), + reported_invalid_value_actions: std::collections::HashSet::new(), loop_iter_drops: Vec::new(), arena_depth: 0, return_roots: BorrowRoots::new(), @@ -12354,10 +12366,23 @@ struct MoveCheck<'a> { next_pipeline_snapshot: usize, /// Borrow-state snapshots paired with [`Self::loop_breaks`] at each `break` edge. loop_borrow_breaks: Vec>, - /// Roots carried by value-bearing `break` edges for each active loop. - loop_value_breaks: Vec, - /// Settled value roots of each walked loop expression, keyed by source span. - loop_value_roots: std::collections::HashMap, + /// Projection-preserving facts carried by value-bearing `break` edges for each active loop. + loop_value_breaks: Vec, + /// Settled value facts of each walked loop expression, keyed by source span. + loop_value_facts: std::collections::HashMap, + /// Settled value facts of walked branch expressions, captured before arm states join. + control_value_facts: std::collections::HashMap, + /// Settled facts of walked expressions, captured immediately when that expression falls + /// through. Parent formation reads these snapshots so a later eager sibling cannot rewrite an + /// earlier runtime value's provenance in a product, residual aggregate, or call. + walked_value_facts: std::collections::HashMap, + /// Direct borrow-capable children completed while each enclosing expression is evaluated. + /// Eager operations validate these snapshots at their action boundary; transparent/control + /// containers defer validation to the operation that consumes their selected result. + value_snapshot_frames: Vec>, + /// Loop refinement may revisit the same action. Emit one invalid-snapshot diagnostic per + /// checked parent/child identity pair. + reported_invalid_value_actions: std::collections::HashSet<(usize, usize)>, /// Per-iteration owned locals of each enclosing `loop` (innermost last) — the locals MIR drops /// at that loop's back-edge and at every `break` bound to it. A `break` edge ends their borrow /// generation exactly as the back-edge does. @@ -12395,6 +12420,17 @@ enum MovedKey { type MovedSet = std::collections::HashSet; +/// How a borrow source's storage generation ended. Only the diagnostic wording depends on this — +/// both endings kill every view rooted in that source. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +enum BorrowEnd { + /// The source was moved away, reassigned, or reallocated in place. + Consumed, + /// The source's storage was **freed** where its binding scope ended — the per-iteration drop a + /// `loop` emits at its back-edge and at every `break` for owned locals declared in its body. + Dropped, +} + /// A storage generation a view may point into. Not every one has a name: MIR gives an **unbound /// Move temporary** a hidden owner slot (`new_synthetic_owner`) whose cleanup joins the innermost /// active loop, so `keep = "…".clone()` inside a `loop` borrows storage that the back-edge frees @@ -12409,19 +12445,173 @@ enum BorrowRoot { IterTemp(u32), /// Source-visible parameter whose embedded view provenance reaches the return. Param(u32), + /// An already-ended root carried by a completion-time value snapshot. Keeping this marker in + /// the fact lets projection and named-summary selection transport the invalidation without + /// widening it to an unselected sibling. + EndedLocal(LocalId, BorrowEnd), + EndedIterTemp(u32, BorrowEnd), + EndedParam(u32, BorrowEnd), } type BorrowRoots = std::collections::BTreeSet; -/// How a borrow source's storage generation ended. Only the diagnostic wording depends on this — -/// both endings kill every view rooted in that source. +impl BorrowRoot { + fn ended(self, how: BorrowEnd) -> Self { + match self { + Self::Local(local) => Self::EndedLocal(local, how), + Self::IterTemp(depth) => Self::EndedIterTemp(depth, how), + Self::Param(param) => Self::EndedParam(param, how), + already @ (Self::EndedLocal(..) + | Self::EndedIterTemp(..) + | Self::EndedParam(..)) => already, + } + } + + fn live(self) -> Option { + match self { + Self::Local(_) | Self::IterTemp(_) | Self::Param(_) => Some(self), + Self::EndedLocal(..) | Self::EndedIterTemp(..) | Self::EndedParam(..) => None, + } + } + + fn ended_source(self) -> Option<(Self, BorrowEnd)> { + match self { + Self::EndedLocal(local, how) => Some((Self::Local(local), how)), + Self::EndedIterTemp(depth, how) => Some((Self::IterTemp(depth), how)), + Self::EndedParam(param, how) => Some((Self::Param(param), how)), + Self::Local(_) | Self::IterTemp(_) | Self::Param(_) => None, + } + } +} + +/// One projection from an aggregate value to a recursively borrow-capable child. This is an +/// analysis-local fact: public summaries still contain only parameter/capture roots. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] -enum BorrowEnd { - /// The source was moved away, reassigned, or reallocated in place. - Consumed, - /// The source's storage was **freed** where its binding scope ended — the per-iteration drop a - /// `loop` emits at its back-edge and at every `break` for owned locals declared in its body. - Dropped, +enum BorrowProjection { + StructField(u32), + TupleElement(u32), + EnumPayload { variant: u32, index: u32 }, + OptionSome, + ResultOk, + ResultErr, +} + +/// Borrow roots carried by a value, separated by aggregate projection. `direct` applies to the +/// whole current value (and therefore every selected descendant); `projected` stores exact child +/// facts. The final public summary flattens this finite trie back to parameter roots. +#[derive(Clone, Default, PartialEq, Eq, Debug)] +struct BorrowFact { + direct: BorrowRoots, + projected: std::collections::BTreeMap, BorrowRoots>, +} + +impl BorrowFact { + fn from_direct(roots: BorrowRoots) -> Self { + Self { + direct: roots, + projected: std::collections::BTreeMap::new(), + } + } + + fn is_empty(&self) -> bool { + self.direct.is_empty() && self.projected.values().all(BorrowRoots::is_empty) + } + + fn flatten(&self) -> BorrowRoots { + let mut roots = self.direct.clone(); + for projected in self.projected.values() { + roots.extend(projected); + } + roots + } + + fn join(&self, other: &Self) -> Self { + let mut out = self.clone(); + out.direct.extend(&other.direct); + for (path, roots) in &other.projected { + out.projected + .entry(path.clone()) + .or_default() + .extend(roots); + } + out + } + + fn prefixed(mut self, projection: BorrowProjection) -> Self { + let mut projected = std::collections::BTreeMap::new(); + if !self.direct.is_empty() { + projected.insert(vec![projection], std::mem::take(&mut self.direct)); + } + for (mut path, roots) in self.projected { + path.insert(0, projection); + projected.entry(path).or_insert_with(BorrowRoots::new).extend(roots); + } + Self { + direct: BorrowRoots::new(), + projected, + } + } + + fn project_exact(&self, projection: BorrowProjection) -> Self { + let mut out = Self::from_direct(self.direct.clone()); + for (path, roots) in &self.projected { + let Some((first, rest)) = path.split_first() else { + out.direct.extend(roots); + continue; + }; + if *first != projection { + 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) + }); + let mut incoming = incoming; + if !incoming.direct.is_empty() { + self.projected + .entry(path.to_vec()) + .or_default() + .extend(std::mem::take(&mut 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) + .into_iter() + .map(|root| invalid.get(&root).map_or(root, |&how| root.ended(how))) + .collect(); + }; + mark(&mut self.direct); + for roots in self.projected.values_mut() { + mark(roots); + } + } + + fn live_roots(&self) -> BorrowRoots { + self.flatten() + .into_iter() + .filter_map(BorrowRoot::live) + .collect() + } + } /// Which ended source generations a borrower depends on, and how each ended. `Ord` on @@ -12436,19 +12626,48 @@ type EndedRoots = std::collections::BTreeMap; #[derive(Clone, Default, PartialEq, Eq)] struct BorrowState { sources: std::collections::HashMap, + facts: std::collections::HashMap, invalid: std::collections::HashMap, pipeline_sources: std::collections::HashMap, invalid_pipeline_sources: std::collections::HashMap, + value_sources: std::collections::HashMap, + invalid_value_sources: std::collections::HashMap, } impl BorrowState { - fn assign(&mut self, local: LocalId, roots: BorrowRoots) { + fn assign(&mut self, local: LocalId, fact: BorrowFact) { self.invalid.remove(&local); + self.update_fact(local, fact); + } + + fn update_fact(&mut self, local: LocalId, fact: BorrowFact) { + let flattened = fact.flatten(); + let roots = flattened + .iter() + .copied() + .filter_map(BorrowRoot::live) + .collect::(); + for root in flattened { + if let Some((source, how)) = root.ended_source() { + let entry = self + .invalid + .entry(local) + .or_default() + .entry(source) + .or_insert(how); + *entry = (*entry).min(how); + } + } if roots.is_empty() { self.sources.remove(&local); } else { self.sources.insert(local, roots); } + if fact.is_empty() { + self.facts.remove(&local); + } else { + self.facts.insert(local, fact); + } } fn begin_pipeline_source(&mut self, snapshot: usize, roots: BorrowRoots) { @@ -12463,6 +12682,20 @@ impl BorrowState { .unwrap_or_default() } + fn begin_value_source(&mut self, snapshot: usize, roots: BorrowRoots) { + self.invalid_value_sources.remove(&snapshot); + if roots.is_empty() { + self.value_sources.remove(&snapshot); + } else { + self.value_sources.insert(snapshot, roots); + } + } + + fn finish_value_source(&mut self, snapshot: usize) { + self.value_sources.remove(&snapshot); + self.invalid_value_sources.remove(&snapshot); + } + /// End every source generation matching `ended`. One entry point for both endings: a named /// local being consumed, and a whole class of roots (a loop depth's temporaries plus its owned /// locals) being dropped at an iteration edge. @@ -12484,6 +12717,17 @@ impl BorrowState { *entry = (*entry).min(how); } } + for (&snapshot, roots) in &self.value_sources { + for &root in roots.iter().filter(|&&r| ended(r)) { + let entry = self + .invalid_value_sources + .entry(snapshot) + .or_default() + .entry(root) + .or_insert(how); + *entry = (*entry).min(how); + } + } } fn invalidate_owner(&mut self, owner: LocalId, how: BorrowEnd) { @@ -12495,6 +12739,12 @@ impl BorrowState { for (&local, roots) in &b.sources { out.sources.entry(local).or_default().extend(roots); } + for (&local, fact) in &b.facts { + out.facts + .entry(local) + .and_modify(|current| *current = current.join(fact)) + .or_insert_with(|| fact.clone()); + } for (&local, roots) in &b.invalid { let into = out.invalid.entry(local).or_default(); for (&owner, &how) in roots { @@ -12518,6 +12768,22 @@ impl BorrowState { *entry = (*entry).min(how); } } + for (&snapshot, roots) in &b.value_sources { + out.value_sources + .entry(snapshot) + .or_default() + .extend(roots); + } + for (&snapshot, roots) in &b.invalid_value_sources { + let into = out + .invalid_value_sources + .entry(snapshot) + .or_default(); + for (&owner, &how) in roots { + let entry = into.entry(owner).or_insert(how); + *entry = (*entry).min(how); + } + } out } } @@ -12571,11 +12837,12 @@ impl<'a> MoveCheck<'a> { if !self.local_may_borrow(local) { continue; } - self.borrows - .sources - .entry(local) - .or_default() - .insert(BorrowRoot::Param(position as u32)); + let mut roots = BorrowRoots::new(); + roots.insert(BorrowRoot::Param(position as u32)); + let ty = self.f.locals[local as usize].ty; + let fact = + self.normalize_borrow_fact(ty, BorrowFact::from_direct(roots)); + self.borrows.assign(local, fact); } let mut moved: MovedSet = std::collections::HashSet::new(); // If the function returns a Move type, its body's trailing expression is consumed by @@ -12586,6 +12853,8 @@ impl<'a> MoveCheck<'a> { if falls_through && let Some(value) = &self.f.body.value { + let key = Self::expr_key(value); + self.validate_value_snapshot(key, key, value.span); self.return_roots.extend(self.borrow_sources(value)); } self.return_roots @@ -12638,6 +12907,159 @@ impl<'a> MoveCheck<'a> { roots } + 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) { + fact.direct.insert(BorrowRoot::Local(id)); + } + fact + } + + fn projection_ty(&self, ty: Ty, projection: BorrowProjection) -> Option { + match (expand_tagged_ty(ty, self.tagged_types), projection) { + (Ty::Struct(id), BorrowProjection::StructField(index)) => self + .structs + .get(id as usize) + .and_then(|def| def.fields.get(index as usize)) + .map(|field| field.ty), + (Ty::Tuple(id), BorrowProjection::TupleElement(index)) => self + .tuples + .get(id as usize) + .and_then(|def| def.elems.get(index as usize)) + .copied() + .map(scalar_to_ty), + ( + Ty::Enum(id), + BorrowProjection::EnumPayload { variant, index }, + ) => self + .enums + .get(id as usize) + .and_then(|def| def.variants.get(variant as usize)) + .and_then(|variant| variant.payload.get(index as usize)) + .copied() + .map(scalar_to_ty), + (Ty::Option(payload), BorrowProjection::OptionSome) => { + Some(scalar_to_ty(payload)) + } + (Ty::Result(ok, _), BorrowProjection::ResultOk) => Some(scalar_to_ty(ok)), + (Ty::Result(_, err), BorrowProjection::ResultErr) => Some(scalar_to_ty(err)), + _ => None, + } + } + + fn borrow_leaf_paths_inner( + &self, + ty: Ty, + visiting: &mut Vec, + ) -> Vec> { + if !ty_may_borrow( + ty, + self.structs, + self.tuples, + self.enums, + self.tagged_types, + ) { + return Vec::new(); + } + let ty = expand_tagged_ty(ty, self.tagged_types); + if visiting.contains(&ty) { + return vec![Vec::new()]; + } + visiting.push(ty); + let mut paths = Vec::new(); + let mut append = |projection: BorrowProjection, child: Ty| { + let children = self.borrow_leaf_paths_inner(child, visiting); + for mut path in children { + path.insert(0, projection); + paths.push(path); + } + }; + match ty { + Ty::Struct(id) => { + if let Some(def) = self.structs.get(id as usize) { + for (index, field) in def.fields.iter().enumerate() { + append(BorrowProjection::StructField(index as u32), field.ty); + } + } + } + Ty::Tuple(id) => { + if let Some(def) = self.tuples.get(id as usize) { + for (index, element) in def.elems.iter().enumerate() { + append( + BorrowProjection::TupleElement(index as u32), + scalar_to_ty(*element), + ); + } + } + } + Ty::Enum(id) => { + if let Some(def) = self.enums.get(id as usize) { + for (variant_index, variant) in def.variants.iter().enumerate() { + for (payload_index, payload) in variant.payload.iter().enumerate() { + append( + BorrowProjection::EnumPayload { + variant: variant_index as u32, + index: payload_index as u32, + }, + scalar_to_ty(*payload), + ); + } + } + } + } + Ty::Option(payload) => { + append(BorrowProjection::OptionSome, scalar_to_ty(payload)); + } + Ty::Result(ok, err) => { + append(BorrowProjection::ResultOk, scalar_to_ty(ok)); + append(BorrowProjection::ResultErr, scalar_to_ty(err)); + } + _ => paths.push(Vec::new()), + } + visiting.pop(); + if paths.is_empty() { + vec![Vec::new()] + } else { + paths + } + } + + 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(..)) { + return fact; + } + let mut out = BorrowFact::default(); + let mut entries = fact.projected.into_iter().collect::>(); + if !fact.direct.is_empty() { + entries.push((Vec::new(), fact.direct)); + } + for (path, roots) in entries { + let mut selected_ty = Some(ty); + for &projection in &path { + selected_ty = selected_ty.and_then(|ty| self.projection_ty(ty, projection)); + } + let Some(selected_ty) = selected_ty else { + out.direct.extend(roots); + continue; + }; + let suffixes = + self.borrow_leaf_paths_inner(selected_ty, &mut Vec::new()); + for suffix in suffixes { + let mut destination = path.clone(); + destination.extend(suffix); + if destination.is_empty() { + out.direct.extend(&roots); + } else { + out.projected + .entry(destination) + .or_default() + .extend(&roots); + } + } + } + out + } + /// The hidden-owner root of an **unbound Move temporary**, when one is materialized here. This /// is MIR's own condition (`lower_borrowed_owned`: [`needs_drop_flag`] + /// [`may_need_synthetic_owner`]) so the two stages cannot disagree about which expressions own @@ -12746,9 +13168,13 @@ impl<'a> MoveCheck<'a> { /// [`Self::storage_roots`]. A borrow-producing arm below therefore routes its borrowed operand /// through `storage_roots`, while a materializing/moving arm routes through `borrow_sources`. fn borrow_sources(&self, e: &Expr) -> BorrowRoots { - let mut roots = BorrowRoots::new(); + self.borrow_fact(e).flatten() + } + + fn borrow_fact(&self, e: &Expr) -> BorrowFact { + let mut fact = BorrowFact::default(); if self.non_fallthrough.contains(&e.span) { - return roots; + return fact; } if !ty_may_borrow( e.ty, @@ -12757,18 +13183,266 @@ impl<'a> MoveCheck<'a> { self.enums, self.tagged_types, ) { - return roots; + return fact; + } + let key = Self::expr_key(e); + if let Some(fact) = self.walked_value_facts.get(&key) { + let mut fact = fact.clone(); + if let Some(invalid) = self.borrows.invalid_value_sources.get(&key) { + fact.mark_ended(invalid); + } + return fact; } if let Some(stages) = pipeline_stages(&e.kind) { for capture in stage_capture_exprs(stages) { - roots.extend(self.borrow_sources(capture)); + fact.direct.extend(self.borrow_sources(capture)); } } for capture in node_captures(&e.kind) { - roots.extend(self.borrow_sources(capture)); + fact.direct.extend(self.borrow_sources(capture)); + } + fact.join(&self.borrow_fact_inner(e)) + } + + fn expr_key(e: &Expr) -> usize { + e as *const Expr as usize + } + + fn clear_value_snapshot(&mut self, key: usize) { + self.walked_value_facts.remove(&key); + self.borrows.finish_value_source(key); + for frame in &mut self.loop_borrow_breaks { + for state in frame { + state.finish_value_source(key); + } + } + } + + fn ended_value_snapshot(&self, key: usize) -> Option<(BorrowRoot, BorrowEnd)> { + self.borrows + .invalid_value_sources + .get(&key) + .and_then(|invalid| invalid.iter().next().map(|(&root, &how)| (root, how))) + .or_else(|| { + self.walked_value_facts.get(&key).and_then(|fact| { + fact.flatten() + .into_iter() + .find_map(BorrowRoot::ended_source) + }) + }) + } + + fn validate_value_snapshot(&mut self, action: usize, snapshot: usize, span: Span) { + let Some((root, how)) = self.ended_value_snapshot(snapshot) else { + return; + }; + if !self + .reported_invalid_value_actions + .insert((action, snapshot)) + { + return; + } + let message = match root { + BorrowRoot::Local(owner) => { + let owner = self + .f + .locals + .get(owner as usize) + .map_or("", |local| local.name.as_str()); + let why = match how { + BorrowEnd::Consumed => "was moved, reassigned, or reallocated by a later eager operand", + BorrowEnd::Dropped => "was dropped before the enclosing operation", + }; + format!( + "value snapshot was invalidated before the enclosing operation: owner \ + '{owner}' {why}" + ) + } + BorrowRoot::IterTemp(_) => { + "value snapshot was invalidated before the enclosing operation: its temporary owner was dropped".to_string() + } + BorrowRoot::Param(_) => { + "value snapshot was invalidated before the enclosing operation: its external owner ended unexpectedly".to_string() + } + BorrowRoot::EndedLocal(..) + | BorrowRoot::EndedIterTemp(..) + | BorrowRoot::EndedParam(..) => { + "value snapshot was invalidated before the enclosing operation".to_string() + } + }; + self.diags.error(message, span); + } + + fn defers_child_snapshot_validation(kind: &ExprKind) -> bool { + matches!( + kind, + ExprKind::Block(_) + | ExprKind::Arena(_) + | ExprKind::TaskGroup(_) + | ExprKind::Unsafe(_) + | ExprKind::If { .. } + | ExprKind::Match { .. } + | ExprKind::ElseUnwrap { .. } + | ExprKind::Loop { .. } + | ExprKind::Binary { + op: BinOp::And | BinOp::Or, + .. + } + ) + } + + fn project_fact_or_flatten( + &self, + ty: Ty, + fact: BorrowFact, + path: &[BorrowProjection], + result_ty: Ty, + ) -> BorrowFact { + let fact = self.normalize_borrow_fact(ty, fact); + let fallback = fact.flatten(); + let mut current_ty = ty; + let mut selected = fact; + for &projection in path { + let Some(next_ty) = self.projection_ty(current_ty, projection) else { + return BorrowFact::from_direct(fallback); + }; + selected = selected.project_exact(projection); + current_ty = next_ty; + } + if expand_tagged_ty(current_ty, self.tagged_types) + != expand_tagged_ty(result_ty, self.tagged_types) + { + return BorrowFact::from_direct(fallback); + } + selected + } + + fn local_projected_fact( + &self, + local: LocalId, + path: &[BorrowProjection], + result_ty: Ty, + ) -> BorrowFact { + let Some(ty) = self.f.locals.get(local as usize).map(|local| local.ty) else { + return BorrowFact::from_direct(self.local_storage_roots(local)); + }; + self.project_fact_or_flatten(ty, self.local_borrow_fact(local), path, result_ty) + } + + fn block_value_fact(&self, block: &Block) -> BorrowFact { + if block + .stmts + .iter() + .all(|statement| self.walked_stmt_falls_through(statement)) + { + block + .value + .as_ref() + .map_or_else(BorrowFact::default, |value| self.borrow_fact(value)) + } else { + BorrowFact::default() + } + } + + fn borrow_fact_inner(&self, e: &Expr) -> BorrowFact { + match &e.kind { + ExprKind::Local(id) => self.local_borrow_fact(*id), + ExprKind::Field { root, path } => { + let path = path + .iter() + .copied() + .map(BorrowProjection::StructField) + .collect::>(); + self.local_projected_fact(*root, &path, e.ty) + } + ExprKind::TupleIndex { recv, index } => self.project_fact_or_flatten( + recv.ty, + self.borrow_fact(recv), + &[BorrowProjection::TupleElement(*index)], + 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| { + definition.fields.len() == fields.len() + && definition.fields.iter().zip(fields).all( + |(expected, field)| { + expand_tagged_ty(expected.ty, self.tagged_types) + == expand_tagged_ty(field.ty, self.tagged_types) + }, + ) + }); + if !valid { + return BorrowFact::from_direct( + fields.iter().fold(BorrowRoots::new(), |mut roots, field| { + roots.extend(self.borrow_sources(field)); + roots + }), + ); + } + fields.iter().enumerate().fold( + BorrowFact::default(), + |fact, (index, field)| { + fact.join( + &self + .borrow_fact(field) + .prefixed(BorrowProjection::StructField(index as u32)), + ) + }, + ) + } + ExprKind::Tuple { tuple_id, elems } => { + let valid = e.ty == Ty::Tuple(*tuple_id) + && self.tuples.get(*tuple_id as usize).is_some_and(|definition| { + definition.elems.len() == elems.len() + && definition + .elems + .iter() + .zip(elems) + .all(|(expected, element)| { + expand_tagged_ty( + scalar_to_ty(*expected), + self.tagged_types, + ) == expand_tagged_ty(element.ty, self.tagged_types) + }) + }); + if !valid { + return BorrowFact::from_direct( + elems.iter().fold(BorrowRoots::new(), |mut roots, element| { + roots.extend(self.borrow_sources(element)); + roots + }), + ); + } + elems.iter().enumerate().fold( + BorrowFact::default(), + |fact, (index, element)| { + fact.join( + &self + .borrow_fact(element) + .prefixed(BorrowProjection::TupleElement(index as u32)), + ) + }, + ) + } + ExprKind::Block(block) + | ExprKind::Arena(block) + | ExprKind::TaskGroup(block) + | ExprKind::Unsafe(block) => self.block_value_fact(block), + ExprKind::If { .. } => self + .control_value_facts + .get(&e.span) + .cloned() + .unwrap_or_default(), + ExprKind::Loop { .. } => self + .loop_value_facts + .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. + _ => BorrowFact::from_direct(self.borrow_sources_inner(e)), } - roots.extend(self.borrow_sources_inner(e)); - roots } fn walked_stmt_falls_through(&self, statement: &Stmt) -> bool { @@ -13050,9 +13724,9 @@ impl<'a> MoveCheck<'a> { union(vec![opt, fallback]) } ExprKind::Loop { .. } => self - .loop_value_roots + .loop_value_facts .get(&e.span) - .cloned() + .map(BorrowFact::flatten) .unwrap_or_default(), // ── The remaining forms record NO provenance, and the list is EXHAUSTIVE: there is no // `_` tail, so a new `ExprKind` cannot silently inherit "borrows nothing" — which is @@ -13157,12 +13831,66 @@ impl<'a> MoveCheck<'a> { } } fn assign_borrow(&mut self, local: LocalId, value: &Expr) { - let roots = if self.local_may_borrow(local) { - self.borrow_sources(value) + let fact = if self.local_may_borrow(local) { + let fact = self.borrow_fact(value); + let ty = self.f.locals[local as usize].ty; + self.normalize_borrow_fact(ty, fact) } else { - BorrowRoots::new() + BorrowFact::default() }; - self.borrows.assign(local, roots); + self.borrows.assign(local, fact); + } + + fn replace_local_borrow_projection( + &mut self, + local: LocalId, + path: &[BorrowProjection], + value: &Expr, + ) { + if !self.local_may_borrow(local) { + return; + } + let local_ty = self.f.locals[local as usize].ty; + let current = self + .borrows + .facts + .get(&local) + .cloned() + .unwrap_or_default(); + let mut current = self.normalize_borrow_fact(local_ty, current); + let incoming = + self.normalize_borrow_fact(value.ty, self.borrow_fact(value)); + let mut destination_ty = Some(local_ty); + for &projection in path { + destination_ty = + destination_ty.and_then(|ty| self.projection_ty(ty, projection)); + } + if path.is_empty() + || 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; + } + current.replace_exact(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; + } + let mut current = self + .borrows + .facts + .get(&local) + .cloned() + .unwrap_or_default(); + current.direct.extend(self.borrow_sources(value)); + self.borrows.update_fact(local, current); } fn invalidate_owner(&mut self, owner: LocalId) { @@ -13209,6 +13937,9 @@ impl<'a> MoveCheck<'a> { // saying so here keeps the rule independent of that ordering. BorrowRoot::IterTemp(d) => d >= depth, BorrowRoot::Param(_) => false, + BorrowRoot::EndedLocal(..) + | BorrowRoot::EndedIterTemp(..) + | BorrowRoot::EndedParam(..) => false, }); } @@ -13257,6 +13988,14 @@ impl<'a> MoveCheck<'a> { (BorrowRoot::Param(_), _) => format!( "use of invalidated borrow '{borrower}': its external provenance ended unexpectedly" ), + ( + BorrowRoot::EndedLocal(..) + | BorrowRoot::EndedIterTemp(..) + | BorrowRoot::EndedParam(..), + _, + ) => format!( + "use of invalidated borrow '{borrower}': its source generation ended unexpectedly" + ), }; self.diags.error(msg, span); } @@ -13397,6 +14136,11 @@ impl<'a> MoveCheck<'a> { BorrowRoot::Param(_) => { "pipeline source snapshot was invalidated before terminal action: its external owner ended unexpectedly".to_string() } + BorrowRoot::EndedLocal(..) + | BorrowRoot::EndedIterTemp(..) + | BorrowRoot::EndedParam(..) => { + "pipeline source snapshot was invalidated before terminal action: its source generation had already ended".to_string() + } }; self.diags.error(message, span); } @@ -13426,10 +14170,12 @@ impl<'a> MoveCheck<'a> { // suppressed by swapping in a throwaway sink (restored after). let mut probe = entry.clone(); let mut sink = Diagnostics::new(); + let reported_invalid_value_actions = + self.reported_invalid_value_actions.clone(); std::mem::swap(self.diags, &mut sink); self.loop_breaks.push(Vec::new()); self.loop_borrow_breaks.push(Vec::new()); - self.loop_value_breaks.push(BorrowRoots::new()); + self.loop_value_breaks.push(BorrowFact::default()); self.borrows = entry_borrows.clone(); let probe_falls_through = self.block(body, &mut probe, false, false); self.loop_breaks.pop(); @@ -13460,7 +14206,7 @@ impl<'a> MoveCheck<'a> { let mut probe_state = &entry | &back_edge; self.loop_breaks.push(Vec::new()); self.loop_borrow_breaks.push(Vec::new()); - self.loop_value_breaks.push(BorrowRoots::new()); + self.loop_value_breaks.push(BorrowFact::default()); self.block(body, &mut probe_state, false, false); self.loop_breaks.pop(); self.loop_borrow_breaks.pop(); @@ -13476,13 +14222,17 @@ impl<'a> MoveCheck<'a> { } else { entry_borrows.clone() }; + // Probe diagnostics were intentionally discarded, so their dedup identities must be too; + // otherwise the real pass would suppress the only user-visible invalid-snapshot error. + self.reported_invalid_value_actions = + reported_invalid_value_actions; std::mem::swap(self.diags, &mut sink); // restore real diagnostics; discard probes' // Real pass from the back-edge fixpoint, collecting `break` snapshots. let mut body_state = &entry | &back_edge; self.borrows = fixed_borrows; self.loop_breaks.push(Vec::new()); self.loop_borrow_breaks.push(Vec::new()); - self.loop_value_breaks.push(BorrowRoots::new()); + self.loop_value_breaks.push(BorrowFact::default()); self.block(body, &mut body_state, false, false); let breaks = self.loop_breaks.pop().expect("loop-break frame balanced"); let falls_through = !breaks.is_empty(); @@ -13490,14 +14240,14 @@ impl<'a> MoveCheck<'a> { .loop_borrow_breaks .pop() .expect("loop borrow-break frame balanced"); - let value_roots = self + let value_fact = self .loop_value_breaks .pop() .expect("loop value-break frame balanced"); - if value_roots.is_empty() { - self.loop_value_roots.remove(&span); + if value_fact.is_empty() { + self.loop_value_facts.remove(&span); } else { - self.loop_value_roots.insert(span, value_roots); + self.loop_value_facts.insert(span, value_fact); } self.loop_iter_drops.pop().expect("loop iteration-drop frame balanced"); // Code after the loop runs only after a `break`; a local moved on *any* break path is @@ -13597,6 +14347,18 @@ impl<'a> MoveCheck<'a> { if !self_assign && self.is_move_ty(value.ty) { self.invalidate_owner(*root); } + if !self_assign { + let projections = path + .iter() + .copied() + .map(BorrowProjection::StructField) + .collect::>(); + self.replace_local_borrow_projection( + *root, + &projections, + value, + ); + } } // `base[index] = value` — primitive array elements are Copy, so the index and value // are read without consuming either. @@ -13608,6 +14370,7 @@ 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); } // 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 @@ -13625,12 +14388,15 @@ impl<'a> MoveCheck<'a> { if self.is_move_ty(value.ty) { self.invalidate_owner(*base); } + self.join_local_borrow_fallback(*base, value); } Stmt::AssignVecLane { value, .. } => { move_expr!(self, value, moved, false, false); } Stmt::Return(Some(e)) => { move_expr!(self, e, moved, true, true); + let key = Self::expr_key(e); + self.validate_value_snapshot(key, key, e.span); // Control expressions establish match-arm payload bindings while they are // walked. Query after that walk so an explicit `return match ...` observes the // same binding provenance as a trailing match expression. @@ -13644,9 +14410,11 @@ impl<'a> MoveCheck<'a> { if let Some(e) = value { move_expr!(self, e, moved, true, true); if *accepted { - let roots = self.borrow_sources(e); + let key = Self::expr_key(e); + self.validate_value_snapshot(key, key, e.span); + let fact = self.borrow_fact(e); if let Some(frame) = self.loop_value_breaks.last_mut() { - frame.extend(roots); + *frame = frame.join(&fact); } } } @@ -13675,15 +14443,25 @@ impl<'a> MoveCheck<'a> { // Destructure consumes its tuple source whole (see the `Local` arm in `expr`). Stmt::LetTuple { locals, init, .. } => { move_expr!(self, init, moved, true, true); - let roots = self.borrow_sources(init); - for l in locals.iter().flatten() { - let local_roots = if self.local_may_borrow(*l) { - roots.clone() + let fact = self.borrow_fact(init); + for (index, local) in locals.iter().enumerate() { + let Some(local) = local else { continue }; + let local_fact = if self.local_may_borrow(*local) { + let selected = self.project_fact_or_flatten( + init.ty, + fact.clone(), + &[BorrowProjection::TupleElement(index as u32)], + self.f.locals[*local as usize].ty, + ); + self.normalize_borrow_fact( + self.f.locals[*local as usize].ty, + selected, + ) } else { - BorrowRoots::new() + BorrowFact::default() }; - self.borrows.assign(*l, local_roots); - clear_moved(moved, *l); + self.borrows.assign(*local, local_fact); + clear_moved(moved, *local); } } } @@ -13810,10 +14588,45 @@ impl<'a> MoveCheck<'a> { consuming: bool, direct: bool, ) -> bool { + let may_borrow = ty_may_borrow( + e.ty, + self.structs, + self.tuples, + self.enums, + self.tagged_types, + ); + let key = Self::expr_key(e); + // A loop refinement or another repeated structural walk may revisit the same source node + // under a stronger state. Never let that prior pass answer this evaluation's fact query. + if may_borrow { + self.clear_value_snapshot(key); + } + self.value_snapshot_frames.push(Vec::new()); let falls_through = self.expr_inner(e, moved, consuming, direct); + let child_snapshots = self + .value_snapshot_frames + .pop() + .expect("value snapshot frame for walked expression"); + if falls_through + && !Self::defers_child_snapshot_validation(&e.kind) + { + for snapshot in child_snapshots { + self.validate_value_snapshot(key, snapshot, e.span); + } + } if !falls_through { self.non_fallthrough.insert(e.span); + } else if may_borrow { + // Every eager child has already stored its own completion-time fact. Forming this + // parent from those child snapshots preserves runtime source order even if a later + // sibling reassigned a local mentioned by an earlier child. + let fact = self.borrow_fact(e); + self.borrows.begin_value_source(key, fact.live_roots()); + self.walked_value_facts.insert(key, fact); + if let Some(parent) = self.value_snapshot_frames.last_mut() { + parent.push(key); + } } falls_through } @@ -13979,8 +14792,8 @@ impl<'a> MoveCheck<'a> { } } ExprKind::StructLit { fields, .. } => { - for f in fields { - move_expr!(self, f, moved, true, true); + for field in fields { + move_expr!(self, field, moved, true, true); } } ExprKind::OptionSome(i) @@ -14404,7 +15217,8 @@ impl<'a> MoveCheck<'a> { } else { BorrowRoots::new() }; - self.borrows.assign(*binding, roots); + self.borrows + .assign(*binding, BorrowFact::from_direct(roots)); // An arm binding is INITIALIZED here from the scrutinee's payload, exactly // like a `Let` — clear any stale moved bit. Without this, an arm that // consumes its own binding inside a `loop` body poisons the back-edge @@ -14457,12 +15271,26 @@ impl<'a> MoveCheck<'a> { self.borrows = incoming_borrows.clone(); let then_falls_through = self.block(then, &mut m1, consuming, false); + let then_fact = then_falls_through + .then(|| self.block_value_fact(then)); let b1 = self.borrows.clone(); let mut m2 = moved.clone(); self.borrows = incoming_borrows.clone(); let else_falls_through = self.block(els, &mut m2, consuming, false); + let else_fact = else_falls_through + .then(|| self.block_value_fact(els)); let b2 = self.borrows.clone(); + let value_fact = match (then_fact, else_fact) { + (Some(then), Some(els)) => then.join(&els), + (Some(fact), None) | (None, Some(fact)) => fact, + (None, None) => BorrowFact::default(), + }; + if value_fact.is_empty() { + self.control_value_facts.remove(&e.span); + } else { + self.control_value_facts.insert(e.span, value_fact); + } // Join the branch states — but a branch that always diverges (`return`) contributes // nothing past the `if`, so its moves must not poison the fall-through. (Without this, // `if c { return x }; use(x)` wrongly reports `x` moved.) When both diverge the code @@ -14848,8 +15676,8 @@ impl<'a> MoveCheck<'a> { // PR1 tuple elements are primitive (Copy) — a tuple literal moves nothing; tuple index // borrows. Recurse to catch moves in element subexpressions. ExprKind::Tuple { elems, .. } => { - for el in elems { - move_expr!(self, el, moved, true, true); + for element in elems { + move_expr!(self, element, moved, true, true); } } ExprKind::MathOp { operands, .. } => { @@ -30280,6 +31108,218 @@ mod tests { (p, d) } + #[test] + fn projected_return_provenance_fails_closed() { + let (program, diagnostics) = check( + "\ +Pair { left: str, right: str } +fn probe(value: Pair, replacement: str) -> str = value.left +fn tuple(value: Pair, replacement: str) -> (str, str) = (value.left, replacement) +fn main() -> i32 = 0 +", + ); + assert!(!diagnostics.has_errors()); + let function = program + .fns + .iter() + .find(|function| function.name == "probe") + .expect("probe function"); + let named = std::collections::HashMap::new(); + let mut sink = Diagnostics::new(); + let mut checker = MoveCheck { + f: function, + diags: &mut sink, + named_return_borrow: &named, + summary_dependencies: None, + tuples: &program.tuples, + structs: &program.structs, + enums: &program.enums, + tagged_types: &program.tagged_types, + loop_breaks: Vec::new(), + borrows: BorrowState::default(), + next_pipeline_snapshot: 0, + loop_borrow_breaks: Vec::new(), + loop_value_breaks: Vec::new(), + loop_value_facts: std::collections::HashMap::new(), + control_value_facts: std::collections::HashMap::new(), + walked_value_facts: std::collections::HashMap::new(), + value_snapshot_frames: Vec::new(), + reported_invalid_value_actions: std::collections::HashSet::new(), + loop_iter_drops: Vec::new(), + arena_depth: 0, + return_roots: BorrowRoots::new(), + non_fallthrough: std::collections::HashSet::new(), + }; + let mut fact = BorrowFact::default(); + fact.projected.insert( + vec![BorrowProjection::StructField(0)], + [BorrowRoot::Param(0)].into_iter().collect(), + ); + fact.projected.insert( + vec![BorrowProjection::StructField(1)], + [BorrowRoot::Param(1)].into_iter().collect(), + ); + let all = fact.flatten(); + let pair_ty = function.locals[function.params[0] as usize].ty; + let tuple_ty = program + .fns + .iter() + .find(|function| function.name == "tuple") + .expect("tuple function") + .ret; + for (ty, path) in [ + ( + Ty::Struct(u32::MAX), + vec![BorrowProjection::StructField(0)], + ), + ( + Ty::Tuple(u32::MAX), + vec![BorrowProjection::TupleElement(0)], + ), + ( + Ty::Tagged(u32::MAX), + vec![BorrowProjection::OptionSome], + ), + ( + pair_ty, + vec![BorrowProjection::StructField(u32::MAX)], + ), + (Ty::Str, vec![BorrowProjection::StructField(0)]), + ] { + assert_eq!( + checker + .project_fact_or_flatten(ty, fact.clone(), &path, Ty::Str) + .flatten(), + all, + "malformed projection metadata must retain every current-value root: {ty:?} {path:?}" + ); + } + assert_eq!( + checker + .project_fact_or_flatten( + pair_ty, + fact.clone(), + &[BorrowProjection::StructField(0)], + Ty::Int(IntTy { + bits: 64, + signed: true, + }), + ) + .flatten(), + all, + "a projection/result type disagreement must retain every current-value root" + ); + let mut malformed_fact = fact.clone(); + malformed_fact.projected.insert( + vec![BorrowProjection::StructField(u32::MAX)], + [BorrowRoot::Param(2)].into_iter().collect(), + ); + assert_eq!( + checker + .project_fact_or_flatten( + pair_ty, + malformed_fact, + &[BorrowProjection::StructField(0)], + Ty::Str, + ) + .flatten(), + [BorrowRoot::Param(0), BorrowRoot::Param(2)] + .into_iter() + .collect(), + "an impossible stored path must become a whole-value root before later projection" + ); + + let pair_local = function.params[0]; + let replacement_local = function.params[1]; + let pair_fact = checker.normalize_borrow_fact( + pair_ty, + BorrowFact::from_direct([BorrowRoot::Param(0)].into_iter().collect()), + ); + checker.borrows.assign(pair_local, pair_fact.clone()); + checker.borrows.assign( + replacement_local, + BorrowFact::from_direct([BorrowRoot::Param(1)].into_iter().collect()), + ); + let replacement = Expr { + kind: ExprKind::Local(replacement_local), + ty: Ty::Str, + span: function.span, + }; + let left = Expr { + kind: ExprKind::Field { + root: pair_local, + path: vec![0], + }, + ty: Ty::Str, + span: function.span, + }; + for malformed in [ + Expr { + kind: ExprKind::StructLit { + struct_id: u32::MAX, + fields: vec![left.clone(), replacement.clone()], + }, + ty: pair_ty, + span: function.span, + }, + Expr { + kind: ExprKind::Tuple { + tuple_id: u32::MAX, + elems: vec![left, replacement.clone()], + }, + ty: tuple_ty, + span: function.span, + }, + ] { + assert_eq!( + checker + .project_fact_or_flatten( + malformed.ty, + checker.borrow_fact(&malformed), + &[match malformed.kind { + ExprKind::StructLit { .. } => BorrowProjection::StructField(0), + _ => BorrowProjection::TupleElement(0), + }], + Ty::Str, + ) + .flatten(), + all, + "a malformed product constructor must flatten every child root" + ); + } + + for (path, value) in [ + ( + vec![ + BorrowProjection::StructField(0), + BorrowProjection::StructField(0), + ], + replacement.clone(), + ), + ( + vec![BorrowProjection::StructField(0)], + Expr { + ty: pair_ty, + ..replacement + }, + ), + ] { + checker.borrows.assign(pair_local, pair_fact.clone()); + checker.replace_local_borrow_projection(pair_local, &path, &value); + assert_eq!( + checker + .local_projected_fact( + pair_local, + &[BorrowProjection::StructField(0)], + Ty::Str, + ) + .flatten(), + all, + "a malformed product write must retain old and incoming roots" + ); + } + } + #[test] fn lifted_function_origin_metadata_is_explicit() { let (program, diagnostics) = check( diff --git a/docs/impl/17-library-boundary-prerequisites.md b/docs/impl/17-library-boundary-prerequisites.md index 87e7c137..5fd86c5a 100644 --- a/docs/impl/17-library-boundary-prerequisites.md +++ b/docs/impl/17-library-boundary-prerequisites.md @@ -1490,15 +1490,18 @@ Scope: - interface codec/hash support; - per-unit parity. -L2 ships through seven closed implementation slices. A slice may add dormant representation or -tighten existing provenance, but it must not accept source syntax whose complete safety contract -belongs to a later slice. +L2 ships through seven conceptual milestones in nine closed implementation PRs; L2b-a2 is split +into product, array/pipeline, and tagged verticals below. A PR may add dormant representation or tighten +existing provenance, but it must not accept source syntax whose complete safety contract belongs to +a later milestone. | Slice | Exact closure | Public exposure at merge | Required gate | |---|---|---|---| | L2a | Replace `is_out`/bare parameter-type lists with `ParamMode`; add span-free return-borrow and return-region records to `FnTy`, named/imported signatures, HIR/MIR, interface codecs, hashes, and ABI fingerprints | Existing `ByValue` and `Out` behavior only; `borrow` and `borrow mut` remain identifiers outside parameter-mode lookahead and are rejected as modes | codec byte/hash goldens, corrupt-tag rejection, whole/per-unit identity, and an exhaustive consumer audit | | L2b-a1 | Infer parameter roots for named functions and preserve conservative flattened roots across recursion, direct/imported calls, control flow, and interfaces | No new borrow mode; aggregate projections and indirect calls retain all-compatible-input unions | scalar direct/imported matrix, semantic interface validation, and summary-inference size/time evidence | -| L2b-a2 | Refine named summaries through struct, tuple, fixed-array, tagged, `else`, `?`, `map_err`, branch/loop, and synthetic-selector projections | No new borrow mode; indirect calls retain the pre-L2b all-compatible-input fallback | direct/imported nested-view projection matrix and per-unit parity | +| L2b-a2-s | Add the projection fact and refine named summaries through structs, tuples, block/`if`/loop, field assignment, and destructuring | No new borrow mode; array, pipeline, tagged/control residuals, and indirect calls retain the L2b-a1 all-compatible-input fallback | direct/imported product-view projection matrix and per-unit parity | +| L2b-a2-a | Extend the projection fact through fixed arrays, element reads/writes, and pipeline field projections | No new borrow mode; tagged/control residuals and indirect calls retain the L2b-a1 all-compatible-input fallback | direct/imported array/pipeline-view projection matrix and per-unit parity | +| L2b-a2-t | Complete user-sum/`Option`/`Result`, `match`, `else`, `?`, and `map_err` projection | Complete L2b-a2 behavior; no new borrow mode; indirect calls retain the pre-L2b fallback | direct/imported tagged-view projection matrix and per-unit parity | | L2b-b | Extend the same inference to capture roots, closures, function-value joins/moves, direct/indirect targets, and unresolved higher-order fallback | Complete L2b behavior; no borrow mode | indirect/captured/joined nested-view matrix, malformed capture-domain rejection, and indirect-return evidence | | L2c | Add `ReturnCleanupAbi` to function and interface identity and implement `DynamicBit` for every recursively Move direct, indirect, and imported return; forward the selected bit on every return edge and store it in the caller slot | No borrow syntax; metadata and physical ABI land atomically before borrowed mutation can construct path-selected values | codec/hash goldens, `Result, Error>` None/Some/Err matrix, ABI mismatch rejection, per-unit parity, and return-cost evidence | | L2d | Contextually accept shared `borrow`, preserve the mode in function types/interfaces, pass non-null caller storage, prohibit callee move/drop, and apply the completed return-root summaries | Shared borrow only; `borrow mut` remains unavailable and shared borrowing Copy is rejected as redundant | reusable Move owner, move-from-borrow rejection, returned-view invalidation, function-value/import parity | @@ -1547,11 +1550,14 @@ both the closed and open-world effects `Impure`; a direct external callback para field call remains legal. L2b replaces those conservative boundaries with recursive target-relative provenance through function-value joins. -L2b is implemented as three independently sound vertical PRs. L2b-a1 owns named/direct/imported +L2b is implemented as five independently sound vertical PRs. L2b-a1 owns named/direct/imported parameter-root inference, semantic interface validation, and whole/per-unit parity while retaining flattened all-compatible-input unions for aggregates, indirect calls, and unanalyzed extern -targets. L2b-a2 refines named -aggregate/control projections without weakening that fallback. L2b-b then adds +targets. L2b-a2-s adds the projection fact and closes struct/tuple construction, selection, +replacement, destructuring, and ordinary block/branch/loop flow while retaining conservative +array, pipeline, and tagged residuals. L2b-a2-a then closes fixed arrays, element reads/writes, and +pipeline field projection. L2b-a2-t closes tagged values, `match`, `else`, `?`, and `map_err` +without weakening the indirect/unanalyzed fallback. L2b-b finally adds target-relative capture roots and function-value target sets, validates the explicit-parameter and capture domains in MIR, removes the now-obsolete unresolved-internal-target effect restriction, and closes the indirect/captured benchmark row. This split keeps each hand-written diff near the @@ -1566,6 +1572,67 @@ weighted-cycle rejection, closure-matrix owner tests, and import-validation benc fail-closed vertical. Splitting before the weighted-cycle gate would accept a malformed or non-terminating public type graph; splitting the gate from its shape/fixed-point prerequisites would reintroduce the same false accepts and false rejections that reopened this matrix. + +L2b-a2 has one implementation closure matrix. It refines the analysis-local value fact before that +fact is flattened into the already-shipped parameter-root interface summary; it does not add a +serialized projection path. A source parameter seeds one root at the whole-value boundary, so any +nested selection from that single aggregate still names the same parameter. A value assembled +inside the function instead records roots at its exact member/tag path, so selecting one member does +not retain roots that exist only in a sibling. Direct/imported callers consume the resulting +canonical parameter set exactly as in L2b-a1. An imported function that returns a scalar or one +selected nested view is precise at parameter-root granularity: selection from a value assembled +from distinct parameters can remove unselected parameter roots, but selection within one aggregate +actual still names that whole parameter and therefore every caller owner embedded in that actual. +An aggregate returned across the interface likewise remains conservatively rooted in every +parameter represented by that aggregate. L2b-b later applies the same projection algebra to +target-relative captures and indirect calls. + +L2b-a2-s owns the base fact shape, parameter/local formation, struct/tuple +construction/selection/replacement, destructuring, ordinary block/`if`/loop flow, liveness parity, +and the product half of the public boundary. L2b-a2-a adds fixed-array paths, exact/dynamic element +selection and replacement, pipeline `Project`/`WhereField`, and their source-order termination +rules. L2b-a2-t owns tagged construction/binding, `match`, `else`, `?`, `map_err`, and the final +public/malformed-boundary pass. Every extending PR must add malformed type/path/ordinal fallback +owners for its new projection kinds and selected/unselected liveness owners to the shared focused +targets. All three retain the scope-boundary row. + +| L2b-a2 path | Exact analysis contract | Owner evidence | +|---|---|---| +| fact shape and flattening | A finite projection trie distinguishes struct fields, tuple elements, fixed-array elements, user-sum `(variant, payload)` slots, `Option.Some`, `Result.Ok`, and `Result.Err`. Each node may also carry a whole-value root. Projection inherits whole-value roots and selects only matching descendants; final return/interface formation flattens the selected trie to canonical parameter roots. A known inactive tag/payload projects to empty. A path/type disagreement, missing definition/id, or out-of-range ordinal instead returns the complete flattened fact at the current value, so malformed checked HIR can add conservative roots but cannot drop one. | direct checked-summary assertions; sema malformed-HIR projection tests for wrong kind, missing id, and out-of-range ordinal; whole/per-unit interface parity | +| parameter and local formation | A recursively borrow-capable parameter seeds its whole-value parameter root. `let`, whole-local assignment, generic monomorph locals, and direct named/imported call results replace the destination fact after RHS evaluation; branch/loop joins union matching nodes. | parameter aggregate, local replacement, generic, direct, and imported fixtures | +| aggregate construction and selection | Struct/tuple/fixed-array literals place each reachable child fact under its exact ordinal path. Every eager child/argument fact is captured immediately after that expression falls through, before any later sibling can mutate its source locals; precise product formation, residual aggregate fallback, and named-summary argument mapping all consume those completion-time facts. A snapshot is keyed by checked-expression identity, not source span, because synthetic view wrappers share their child's span. It participates in the same branch/loop invalidation state as a local: if a later sibling ends an owner generation, an ended-root marker remains at the exact projection path and invalidates the eventual destination. Every eager operation, including one whose result cannot borrow, validates its completed operand snapshots at the action boundary; a terminating later operand has no enclosing action and performs no validation. Loop probes isolate diagnostic-dedup state so only the real pass records that validation. Field, nested-field, tuple-index, fixed-array index, element-field, and pipeline selectors read the corresponding path. An index is exact only when checked HIR contains an in-range `ExprKind::Int`; no separate constant folding is performed. Every other index unions all reachable elements. `StageKind::Project { field }` selects that element-struct field, while `WhereField` preserves the complete incoming element fact. Receiver/source evaluation precedes index or stage action; a terminating predecessor produces no result fact. | nested struct, tuple, product/residual-array/named-call child-source-reassignment, direct and non-borrowing-result action use, later-sibling owner invalidation, loop-probe/real-pass diagnostic parity, terminating-later-operand negative twin, same-span array-to-slice wrapper, imported selected-owner liveness, fixed-array literal/dynamic index, element-field, `Project`, `WhereField`, and terminating receiver/index fixtures | +| aggregate replacement and destructuring | A whole-local write replaces the complete fact. A struct-field or exact fixed-array/element-field write kills only the exact destination subtree and installs the RHS fact there after RHS evaluation. A dynamic-index write cannot identify the replaced slot, so every possible destination retains its old fact and joins the RHS fact; no old root is killed. Whole-element writes use the same rule before nested selection. Root/index/RHS evaluation follows HIR source order and performs no install when an earlier child terminates. Exact self-assignment preserves the old fact. Tuple destructuring projects each present binding's exact element after one successful initializer evaluation; an omitted binding receives nothing. This slice does not widen the tuple element types accepted by type formation. | field/nested-field replacement, exact/dynamic element replacement, whole-element replacement, self-assignment, terminating index/RHS, control-produced tuple, and tuple-destructure-with-hole fixtures | +| tagged construction and pattern binding | User-sum, `Option`, and `Result` constructors place facts only below the active tag/payload path. A simple `match` binding selects its exact variant/payload ordinal; wildcard/or-pattern binds nothing. Branch arms start from the same evaluated scrutinee state and only fallthrough arms join. | user-sum, `Option`, `Result`, wildcard/or-pattern, mixed/all-diverging match fixtures | +| `else` and `?` | `else` success selects `Option.Some` or `Result.Ok` and joins only a fallthrough fallback. `?` continues with only `Result.Ok`; its implicit early-return edge contributes only `Result.Err`, and only when the enclosing return can carry a borrow. The operand is evaluated once and a terminating operand contributes neither edge. | success/fallback, Ok/Err, terminating/mixed control, and direct/imported summary fixtures | +| `map_err` | The output `Ok` projection preserves only the receiver's `Result.Ok`. For a statically named mapper, only the receiver's `Result.Err` is mapped through the mapper's settled parameter-root summary into the output `Result.Err`; mapper captures and unresolved function-value targets retain the L2b-a1 all-compatible fallback until L2b-b. Formation order and terminating receiver/mapper behavior remain the L2b-a1 contract. | named mapper fixed/identity, unresolved fallback, terminating mapper, and whole/per-unit fixtures | +| branch, block, and loop result | Transparent block/arena/task-group/unsafe tails preserve the complete selected fact. `if` and `match` union only fallthrough alternatives. Each accepted `break value` contributes its complete fact to the target loop result; rejected, unreachable, or payload-terminating breaks contribute nothing. Loop backedge/local assignment state reaches the existing finite fixpoint without widening a selected member to its siblings. | block/branch/match/loop twins, mixed termination, reassignment fixpoint, and rejected-break regressions | +| liveness and ownership parity | Existing invalidation continues to use the flattened owner-root set. Projection refinement may remove a sibling root but may never remove the selected value's owner, hidden temporary, or parameter root. Move/null, Drop, cleanup-bit, pipeline-source snapshot, escape, and effect behavior do not change in this slice. | owner-use diagnostics for selected versus unselected siblings plus the cumulative L2b-a1 gates | +| public and malformed boundary | `ReturnBorrowSummary` and `ReturnRegionSummary` remain the L2a codec and hash shape and remain equal in L2b-a2. Semantic import keeps the L2b-a1 validation order. No projection trie, local id, span, raw nominal id, or control-state bit is serialized. Because the codec carries parameter indices only, an imported aggregate result and any later projection from one aggregate actual deliberately retain that actual's complete compatible owner set. | unchanged codec/hash goldens, interface corruption suite, aggregate-actual precision-limit fixture, and summary byte-size benchmark row | +| scope boundary | Indirect calls, closure captures, function-value joins/moves, target-relative capture slots, and direct calls without a settled named/imported summary—including unanalyzed extern targets—retain the documented all-compatible-input fallback. No `borrow`, `borrow mut`, cleanup ABI, resource, region, or database surface is enabled. | existing deferred-function-value and compatibility/extern fixtures plus disabled-mode regressions | + +L2b-a2-s, L2b-a2-a, and L2b-a2-t are the smallest independently correct verticals. The first PR +publishes an exact product summary while array, pipeline, and tagged/control forms deliberately +retain the shipped flattened result. It must include product construction, reads, partial writes, +destructuring, ordinary control joins, direct/imported consumption, and whole/per-unit parity +together: omitting a writer or join can under-approximate the same public product fact. The second +PR adds array formation, exact/dynamic reads and writes, and pipeline projection together; shipping +exact array reads before the corresponding writes could retain only an overwritten owner, while +shipping writes before formation would provide no precise destination paths. The third PR replaces +the remaining tagged fallbacks atomically across constructors, pattern bindings, `else`, `?`, and +`map_err`: splitting its explicit and implicit `Result` edges would let one value produce +contradictory summaries. No PR may exceed roughly 1,000 changed hand-written lines without first +updating this matrix with a narrower safe boundary and a concrete reason that boundary fails. +The final L2b-a2-s diff is approximately 1,900 changed hand-written lines after adversarial review +required fail-closed constructor/read/write validation, common eager-child source-order snapshots, +snapshot-generation invalidation, checked-expression identity, action-boundary validation, and +discriminating residual/liveness owners. +Those checks cannot form a later PR: publishing the product fact first would let malformed checked +HIR discard a root, while omitting the residual-write owner would let a deferred array write publish +`None`; omitting snapshot invalidation or expression identity would respectively accept a dangling +earlier child or drop a synthetic wrapper's temporary owner. Product formation, mutation, control +joins, malformed fallback, source-order value lifetime, and their owner evidence therefore remain +one independently sound vertical. + Through L2b-a1, a `?` occurrence conservatively joins its flattened operand roots into the enclosing function only when that function's return type can recursively carry a borrow; its `Ok` projection continues through the ordinary expression and explicit/implicit return paths. L2b-a2 @@ -1861,7 +1928,9 @@ their first owning slice and remain cumulative gates afterward. |---|---|---| | L2a | `cargo test -p align_interface --test summary`; `cargo test -p align_driver --test fn_values --test out_params --test interface_param_modes` | `bench/library_boundary/run.sh interface`: `interface-size`, `decode-throughput` | | L2b-a1 | `cargo test -p align_interface --test summary`; `cargo test -p align_sema ty_may_borrow_is_cycle_safe_for_header_mediated_nominals`; `cargo test -p align_sema lifted_function_origin_metadata_is_explicit`; `cargo test -p align_sema checked_break_acceptance_is_preserved_in_hir`; `cargo test -p align_sema rejected_break_effect_payload_is_visited_without_loop_result_join`; `cargo test -p align_sema effect_source_order_closure_matrix`; `cargo test -p align_sema pipeline_terminal_snapshot_action_order_matrix`; `cargo test -p align_sema pipeline_terminal_diagnostic_order`; `cargo test -p align_sema pipeline_terminal_dead_state_isolated_across_analyses`; `cargo test -p align_sema pipeline_terminal_dead_hir_is_finalized_and_linted`; `cargo test -p align_sema pipeline_capture_owner_invalidation_is_rejected`; `cargo test -p align_sema pipeline_source_snapshot_owner_invalidation_matrix`; `cargo test -p align_codegen_llvm malformed_mir_type_graphs_fail_before_llvm_construction`; `cargo test -p align_mir rejected_checked_break_lowers_to_unreachable`; `cargo test -p align_mir terminating_break_payload_emits_no_outer_edge`; `cargo test -p align_mir mixed_break_payload_preserves_outer_edge`; `cargo test -p align_mir terminating_pipeline_operand_emits_no_terminal_state`; `cargo test -p align_mir pipeline_terminal_snapshot_action_order_matrix`; `cargo test -p align_mir pipeline_terminal_source_shape_parity`; `cargo test -p align_driver --test return_provenance --test analysis_coverage --test interface_param_modes --test per_unit`; `cargo test -p align_driver --test m5 json_scan_reduce_fold`; `cargo test -p align_driver --test zip_pipeline pipeline_terminal_source_order` | `bench/library_boundary/run.sh provenance`: `summary-inference`, `import-validation` | -| L2b-a2 | `cargo test -p align_driver --test return_provenance --test per_unit` | `bench/library_boundary/run.sh provenance`: `summary-inference` | +| L2b-a2-s | `cargo test -p align_sema projected_return_provenance_fails_closed`; `cargo test -p align_driver --test return_provenance --test per_unit` | `bench/library_boundary/run.sh provenance`: `summary-inference` | +| L2b-a2-a | `cargo test -p align_sema projected_return_provenance_fails_closed`; `cargo test -p align_driver --test return_provenance --test per_unit` | `bench/library_boundary/run.sh provenance`: `summary-inference` | +| L2b-a2-t | `cargo test -p align_sema projected_return_provenance_fails_closed`; `cargo test -p align_driver --test return_provenance --test per_unit` | `bench/library_boundary/run.sh provenance`: `summary-inference` | | L2b-b | `cargo test -p align_driver --test return_provenance --test fn_values --test per_unit` | `bench/library_boundary/run.sh provenance`: `summary-inference`, `indirect-return` | | L2c | `cargo test -p align_driver --test move_return_cleanup --test owned_tagged_payloads --test per_unit_codegen` | `bench/library_boundary/run.sh move-return`: `copy-return-control`, `move-return-none`, `move-return-some`, `move-return-err` | | L2d | `cargo test -p align_driver --test borrowed_params shared_`; `cargo test -p align_driver --test return_provenance` | `bench/library_boundary/run.sh shared-borrow`: `by-value-call-control`, `shared-borrow-call` |