diff --git a/crates/hir/src/analysis/semantic/borrowck/access.rs b/crates/hir/src/analysis/semantic/borrowck/access.rs new file mode 100644 index 0000000000..1083b5e618 --- /dev/null +++ b/crates/hir/src/analysis/semantic/borrowck/access.rs @@ -0,0 +1,203 @@ +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::analysis::{ + semantic::{SLocalId, SemOrigin}, + ty::ty_def::BorrowKind, +}; + +use super::{ + canon::BorrowCanonCx, + guard::{Guard, IndexParamId}, + loan::{LoanDef, LoanId, LoanRef}, + region::RegionSet, + shape::{SlotPath, SlotProjection}, + transfer::BorrowState, +}; + +#[derive(Clone, Debug)] +pub(super) struct MoveSite<'db> { + pub(super) origin: SemOrigin<'db>, + pub(super) note: String, +} + +pub(super) type MovedPlaces<'db> = FxHashMap, MoveSite<'db>>; + +pub(super) struct CallAccess<'db> { + group: usize, + projection: Option>, + kind: BorrowKind, + region: RegionSet<'db>, + origin: SemOrigin<'db>, +} + +impl<'db> CallAccess<'db> { + pub(super) fn new( + group: usize, + projection: Option>, + kind: BorrowKind, + region: RegionSet<'db>, + origin: SemOrigin<'db>, + ) -> Self { + Self { + group, + projection, + kind, + region, + origin, + } + } + + pub(super) fn conflicts_with( + &self, + group: usize, + projection: Option<&SlotPath>, + kind: BorrowKind, + region: &RegionSet<'db>, + ) -> bool { + (self.group != group + || self.projection.is_some() + && projection.is_some() + && !variant_slots_are_mutually_exclusive(self.projection.as_ref(), projection)) + && !matches!((self.kind, kind), (BorrowKind::Ref, BorrowKind::Ref)) + && self.region.may_overlap(region).is_some() + } + + pub(super) fn origin(&self) -> SemOrigin<'db> { + self.origin + } +} + +pub(super) struct ActiveLoan<'db> { + reference: LoanRef, + holder_guard: Guard, + region: RegionSet<'db>, + suspended: RegionSet<'db>, +} + +impl<'db> ActiveLoan<'db> { + pub(super) fn id(&self) -> LoanId { + self.reference.id + } + + pub(super) fn reference(&self) -> &LoanRef { + &self.reference + } + + pub(super) fn holder_guard(&self) -> &Guard { + &self.holder_guard + } + + pub(super) fn region(&self) -> &RegionSet<'db> { + &self.region + } + + pub(super) fn matches(&self, reference: &LoanRef, guard: &Guard) -> Option { + self.holder_guard + .and(guard) + .and_then(|guard| self.reference.unify(reference, &guard)) + } + + pub(super) fn overlaps(&self, region: &RegionSet<'db>) -> bool { + let overlap = self.region.intersection(region); + !overlap.is_empty() && !self.suspended.provably_covers(&overlap) + } +} + +pub(super) fn active_loans_in<'db>( + canon: &BorrowCanonCx<'_, 'db>, + state: &BorrowState<'db>, + local: SLocalId, +) -> Vec> { + state + .leaves_in(local, super::guard::ValueScope::Local(local)) + .into_iter() + .map(|leaf| ActiveLoan { + reference: leaf.payload.clone(), + holder_guard: leaf.guard.clone(), + region: canon.active_region_for_held(&leaf.payload, &leaf.guard), + suspended: RegionSet::empty(), + }) + .collect() +} + +pub(super) fn effective_loans<'db>( + canon: &BorrowCanonCx<'_, 'db>, + loans: &[LoanDef<'db>], + state: &BorrowState<'db>, + live: &FxHashSet, +) -> Vec> { + let mut active = state + .locals() + .filter(|local| live.contains(local)) + .flat_map(|local| active_loans_in(canon, state, local)) + .collect::>(); + let mut suspended = vec![RegionSet::empty(); active.len()]; + let mut worklist = active + .iter() + .map(|loan| { + ( + loan.reference.clone(), + loan.holder_guard.clone(), + loan.region.clone(), + ) + }) + .collect::>(); + let mut seen = FxHashSet::default(); + while let Some((reference, guard, region)) = worklist.pop() { + if !seen.insert((reference.clone(), guard.clone(), region.clone())) { + continue; + } + let parents = loans[reference.id.0 as usize].instantiate_parents(&reference, &guard); + for parent in parents.iter() { + let parent_region = region.with_guard(parent.guard()); + if parent_region.is_empty() { + continue; + } + for (idx, active_parent) in active.iter().enumerate() { + let Some(match_guard) = active_parent.matches(parent.reference(), parent.guard()) + else { + continue; + }; + let matched = parent_region.with_guard(&match_guard); + let joined = suspended[idx].union(&matched); + if joined != suspended[idx] { + suspended[idx] = joined; + } + } + worklist.push(( + parent.reference().clone(), + parent.guard().clone(), + parent_region, + )); + } + } + for (loan, suspended) in active.iter_mut().zip(suspended) { + loan.suspended = suspended; + } + active.sort_by_key(|loan| loan.id().0); + active +} + +fn variant_slots_are_mutually_exclusive( + lhs: Option<&SlotPath>, + rhs: Option<&SlotPath>, +) -> bool { + let (Some(lhs), Some(rhs)) = (lhs, rhs) else { + return false; + }; + let Some((lhs, rhs)) = lhs + .as_slice() + .iter() + .zip(rhs.as_slice()) + .find(|(lhs, rhs)| lhs != rhs) + else { + return false; + }; + matches!( + (lhs, rhs), + ( + SlotProjection::VariantField { variant: lhs, .. }, + SlotProjection::VariantField { variant: rhs, .. } + ) if lhs != rhs + ) +} diff --git a/crates/hir/src/analysis/semantic/borrowck/analyses.rs b/crates/hir/src/analysis/semantic/borrowck/analyses.rs index a0d3878699..e3f1ebd39c 100644 --- a/crates/hir/src/analysis/semantic/borrowck/analyses.rs +++ b/crates/hir/src/analysis/semantic/borrowck/analyses.rs @@ -3,148 +3,159 @@ use std::convert::Infallible; use cranelift_entity::{EntityRef, SecondaryMap}; use dataflow::{BackwardCfgAnalysis, ForwardCfgAnalysis, JoinSemiLattice, SparseAnalysis}; use rustc_hash::{FxHashMap, FxHashSet}; +use smallvec::SmallVec; use crate::analysis::{ HirAnalysisDb, semantic::{ - SBlockId, SLocalId, SemanticInstance, + SBlockId, SLocalId, SStmtId, borrowck::ir::{NExpr, NSStmtKind}, - get_or_build_semantic_instance, }, }; use super::{ - canon::{BorrowCanonCx, CanonPlace, CfgAdjacency, Loan, LoanId, MovedPlaces, State}, - check::{Borrowck, provisional_borrow_summary_voucher, semantic_borrow_summary_voucher}, - ir::{BorrowInputRef, NormalizedSemanticBody, SemanticBorrowDiagnostic}, + access::MovedPlaces, + canon::BorrowCanonCx, + check::Borrowck, + ir::{NormalizedSemanticBody, SemanticBorrowDiagnostic}, + loan::{LoanDef, LoanId, ParentSet}, + region::RegionSet, + summary::{BorrowSourceClause, SummaryPath}, + transfer::{BorrowState, BorrowTransferCx}, }; +pub(super) type BlockAdjacency = SmallVec; +pub(super) type CfgAdjacency = SecondaryMap; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(super) enum BorrowSummaryMode { - Final, + FinalCheck, + FinalSummary, Provisional, } pub(super) struct BorrowLoanTargetState<'a, 'db> { - pub(super) loans: &'a mut [Loan<'db>], + pub(super) loans: &'a mut [LoanDef<'db>], } pub(super) struct BorrowLoanTargetAnalysis<'a, 'db> { db: &'db dyn HirAnalysisDb, - instance: SemanticInstance<'db>, body: &'a NormalizedSemanticBody<'db>, - entry_state: &'a SecondaryMap, + entry_state: &'a SecondaryMap>, loan_for_local: &'a FxHashMap, - summary_mode: BorrowSummaryMode, + constant_indices: &'a SecondaryMap>, + call_result_loans: &'a FxHashMap>, + call_loan_sources: &'a FxHashMap>, } impl<'a, 'db> BorrowLoanTargetAnalysis<'a, 'db> { pub(super) fn new( db: &'db dyn HirAnalysisDb, - instance: SemanticInstance<'db>, body: &'a NormalizedSemanticBody<'db>, - entry_state: &'a SecondaryMap, + entry_state: &'a SecondaryMap>, loan_for_local: &'a FxHashMap, - summary_mode: BorrowSummaryMode, + constant_indices: &'a SecondaryMap>, + call_result_loans: &'a FxHashMap>, + call_loan_sources: &'a FxHashMap>, ) -> Self { Self { db, - instance, body, entry_state, loan_for_local, - summary_mode, + constant_indices, + call_result_loans, + call_loan_sources, } } - fn canon<'b>(&'b self, loans: &'b [Loan<'db>]) -> BorrowCanonCx<'b, 'db> { + fn canon<'b>(&'b self, loans: &'b [LoanDef<'db>]) -> BorrowCanonCx<'b, 'db> { BorrowCanonCx::new( self.db, - self.instance, + self.body.owner, self.body, loans, - self.loan_for_local, + self.constant_indices, ) } fn extend_loan( &self, - loans: &mut [Loan<'db>], + loans: &mut [LoanDef<'db>], loan_id: LoanId, - targets: FxHashSet>, - parents: FxHashSet, + region: RegionSet<'db>, + parents: ParentSet, ) -> bool { - let loan = &mut loans[loan_id.0 as usize]; - let before_targets = loan.targets.len(); - let before_parents = loan.parents.len(); - loan.targets.extend(targets); - loan.parents.extend(parents); - before_targets != loan.targets.len() || before_parents != loan.parents.len() + loans[loan_id.0 as usize].extend(region, parents) } fn update_loan_from_stmt( &self, - loans: &mut [Loan<'db>], - state: &State, + loans: &mut [LoanDef<'db>], + state: &BorrowState<'db>, stmt: &super::ir::NSStmt<'db>, ) -> Result> { let NSStmtKind::Assign { dst, expr } = &stmt.kind else { return Ok(false); }; - let Some(&loan_id) = self.loan_for_local.get(dst) else { - return Ok(false); - }; match expr { - NExpr::Borrow { place, .. } => { - let (targets, parents) = { + NExpr::Borrow { place, .. } | NExpr::ReadPlace { place, .. } => { + let Some(&loan_id) = self.loan_for_local.get(dst) else { + return Ok(false); + }; + let (region, parents) = { let canon = self.canon(loans); + let region = canon.resolve_place(state, place, stmt.origin)?; ( - canon.canonicalize_place(state, place, stmt.origin)?, - canon.mut_loans_for_place(state, place), + region.clone(), + canon.mut_parent_refs_for_place(state, place, ®ion), ) }; - Ok(self.extend_loan(loans, loan_id, targets, parents)) + Ok(self.extend_loan(loans, loan_id, region, parents)) } - NExpr::Call { callee, args, .. } => { - let callee_instance = get_or_build_semantic_instance(self.db, callee.key); - let summary = match self.summary_mode { - BorrowSummaryMode::Final => { - semantic_borrow_summary_voucher(self.db, callee_instance) - } - BorrowSummaryMode::Provisional => { - provisional_borrow_summary_voucher(self.db, callee_instance) - } - }?; - let Some(summary) = summary else { - return Ok(false); - }; - let (targets, parents) = { + NExpr::Call { args, .. } => { + let mut loan_ids = self + .loan_for_local + .get(dst) + .copied() + .into_iter() + .collect::>(); + loan_ids.extend( + self.call_result_loans + .get(&stmt.id) + .into_iter() + .flatten() + .map(|(_, loan)| *loan), + ); + let mut changed = false; + for loan_id in loan_ids { + let Some(sources) = self.call_loan_sources.get(&loan_id) else { + continue; + }; + let mut region = RegionSet::empty(); + let mut parents = ParentSet::default(); let canon = self.canon(loans); - let mut targets = FxHashSet::default(); - let mut parents = FxHashSet::default(); - for transform in &summary { - let BorrowInputRef::Param(idx) = transform.input; - if let Some(arg) = args.get(idx as usize) { - for base in canon.canonicalize_value_base(state, arg.local) { - targets.insert(CanonPlace { - root: base.root, - proj: base.proj.concat(&transform.proj), - }); - } - parents.extend(canon.mut_loans_for_value(state, arg.local)); - } + for source in sources { + let (source_region, source_parents) = + canon.instantiate_call_source(state, args, source); + region = region.union(&source_region); + parents.union(source_parents); } - (targets, parents) - }; - Ok(self.extend_loan(loans, loan_id, targets, parents)) + changed |= self.extend_loan(loans, loan_id, region, parents); + } + Ok(changed) } NExpr::Use(value) => { + let Some(&loan_id) = self.loan_for_local.get(dst) else { + return Ok(false); + }; let canon = self.canon(loans); + let region = canon.borrow_local_region(state, value.local); Ok(self.extend_loan( loans, loan_id, - canon.canonicalize_value_base(state, value.local), - canon.mut_loans_for_value(state, value.local), + region.clone(), + canon.mut_parent_refs_for_value(state, value.local, ®ion), )) } _ => Ok(false), @@ -170,8 +181,17 @@ impl<'a, 'db> SparseAnalysis for BorrowLoanTargetAnalysis<'a, 'db> { let mut changed = false; for stmt in &self.body.blocks[node.index()].stmts { changed |= self.update_loan_from_stmt(&mut *state.loans, &local_state, stmt)?; - self.canon(state.loans) - .apply_stmt_state(&mut local_state, stmt); + BorrowTransferCx::new( + self.db, + self.body, + self.loan_for_local, + self.constant_indices, + ) + .apply_stmt( + &mut local_state, + stmt, + self.call_result_loans.get(&stmt.id).map(Vec::as_slice), + ); } Ok(changed) } @@ -195,9 +215,9 @@ impl<'a, 'db> BorrowEntryStateAnalysis<'a, 'db> { } } -impl ForwardCfgAnalysis for BorrowEntryStateAnalysis<'_, '_> { +impl<'db> ForwardCfgAnalysis for BorrowEntryStateAnalysis<'_, 'db> { type Block = SBlockId; - type State = State; + type State = BorrowState<'db>; type Error = Infallible; fn block_count(&self) -> usize { @@ -212,7 +232,7 @@ impl ForwardCfgAnalysis for BorrowEntryStateAnalysis<'_, '_> { } fn bottom(&self) -> Self::State { - State::default() + BorrowState::new(self.borrowck.value_interner.clone()) } fn initialize( @@ -221,8 +241,8 @@ impl ForwardCfgAnalysis for BorrowEntryStateAnalysis<'_, '_> { ) -> Result<(), Self::Error> { if !self.borrowck.body.blocks.is_empty() { let entry = &mut entry_states[SBlockId::new(0)]; - for (&local, &loan) in &self.borrowck.param_loan_for_local { - entry.assign_loans(local, FxHashSet::from_iter([loan])); + for (&local, value) in &self.borrowck.param_values_for_local { + entry.assign(local, *value); } } Ok(()) @@ -235,7 +255,7 @@ impl ForwardCfgAnalysis for BorrowEntryStateAnalysis<'_, '_> { ) -> Result { let mut state = in_state.clone(); for stmt in &self.borrowck.body.blocks[block.index()].stmts { - self.borrowck.canon().apply_stmt_state(&mut state, stmt); + self.borrowck.apply_stmt_state(&mut state, stmt); } Ok(state) } @@ -302,7 +322,7 @@ impl<'db> ForwardCfgAnalysis for BorrowMovedStateAnalysis<'_, 'db> { for stmt in &self.borrowck.body.blocks[block.index()].stmts { self.borrowck .update_moved_for_stmt(&state, &mut moved, stmt)?; - self.borrowck.canon().apply_stmt_state(&mut state, stmt); + self.borrowck.apply_stmt_state(&mut state, stmt); } Ok(MovedState(moved)) } diff --git a/crates/hir/src/analysis/semantic/borrowck/callsite.rs b/crates/hir/src/analysis/semantic/borrowck/callsite.rs index 3be7d83ec0..2167078ae9 100644 --- a/crates/hir/src/analysis/semantic/borrowck/callsite.rs +++ b/crates/hir/src/analysis/semantic/borrowck/callsite.rs @@ -1,5 +1,4 @@ use cranelift_entity::EntityRef; -use rustc_hash::FxHashSet; use crate::analysis::{ HirAnalysisDb, @@ -14,13 +13,15 @@ use crate::analysis::{ }; use super::{ - canon::{CanonPlace, State, address_space_for_borrow_root}, + canon::address_space_for_region_root, check::Borrowck, diagnostics::operand_origin, ir::{ NEffectArg, NEffectArgValue, NExpr, NOperand, NSStmt, NSStmtKind, SemanticBorrowDiagnostic, }, normalize::normalize_provisional_semantic_body, + region::RegionSet, + transfer::BorrowState, }; pub(crate) fn provisional_call_site_provider_refinements<'db>( @@ -50,7 +51,7 @@ impl<'db> CallSiteProviderRefiner<'db> { let mut state = self.borrowck.entry_state[SBlockId::new(bb_idx)].clone(); for stmt in &block.stmts { self.refine_stmt(&state, stmt, &mut out)?; - self.borrowck.canon().apply_stmt_state(&mut state, stmt); + self.borrowck.apply_stmt_state(&mut state, stmt); } } Ok(out) @@ -58,7 +59,7 @@ impl<'db> CallSiteProviderRefiner<'db> { fn refine_stmt( &self, - state: &State, + state: &BorrowState<'db>, stmt: &NSStmt<'db>, out: &mut Vec, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { @@ -95,16 +96,15 @@ impl<'db> CallSiteProviderRefiner<'db> { fn effect_arg_address_space( &self, - state: &State, + state: &BorrowState<'db>, origin: SemOrigin<'db>, arg: &NEffectArg<'db>, ) -> Result, SemanticBorrowDiagnostic<'db>> { let targets = match &arg.arg { - NEffectArgValue::Place(place) => self - .borrowck - .canon() - .canonicalize_place(state, place, origin)?, - NEffectArgValue::Value(value) => self.value_targets(state, *value), + NEffectArgValue::Place(place) => { + self.borrowck.canon().resolve_place(state, place, origin)? + } + NEffectArgValue::Value(value) => self.value_region(state, *value), }; if targets.is_empty() { return Ok(arg.provider); @@ -113,24 +113,22 @@ impl<'db> CallSiteProviderRefiner<'db> { .map(Some) } - fn value_targets(&self, state: &State, value: NOperand) -> FxHashSet> { - self.borrowck - .canon() - .canonicalize_value_base(state, value.local) + fn value_region(&self, state: &BorrowState<'db>, value: NOperand) -> RegionSet<'db> { + self.borrowck.canon().value_region(state, value.local) } fn address_space_for_targets( &self, - targets: &FxHashSet>, + targets: &RegionSet<'db>, origin: SemOrigin<'db>, ) -> Result> { let mut spaces = Vec::new(); - for target in targets { - let space = address_space_for_borrow_root( + for (_, target) in targets.guarded_places() { + let space = address_space_for_region_root( self.borrowck.db, self.borrowck.instance, &self.borrowck.body, - &target.root, + target.root(), origin, )?; if !spaces.contains(&space) { diff --git a/crates/hir/src/analysis/semantic/borrowck/canon.rs b/crates/hir/src/analysis/semantic/borrowck/canon.rs index d2cc15d3e5..32b0791336 100644 --- a/crates/hir/src/analysis/semantic/borrowck/canon.rs +++ b/crates/hir/src/analysis/semantic/borrowck/canon.rs @@ -1,38 +1,49 @@ use cranelift_entity::SecondaryMap; -use dataflow::JoinSemiLattice; -use rustc_hash::{FxHashMap, FxHashSet}; -use smallvec::SmallVec; use crate::{ analysis::{ HirAnalysisDb, - semantic::{SBlockId, SLocalId, SemOrigin, SemanticInstance}, + place::projectable_place_ty, + semantic::{FieldIndex, LayoutBackingProjection, SLocalId, SemOrigin, SemanticInstance}, ty::{ + adt_def::{AdtRef, instantiate_adt_field_shape}, provider::{ProviderAddressSpace, ProviderKind}, - ty_def::BorrowKind, + ty_check::LocalBinding, + ty_def::{BorrowKind, TyId}, + ty_is_noesc, }, }, - projection::Aliasing, + projection::{IndexSource, Projection}, }; use super::{ diagnostics::normalized_body_internal_diag, + guard::{ExistentialId, Guard, IndexExpr}, ir::{ - NBorrowRoot, NBorrowRootId, NExpr, NSPlace, NSPlaceRoot, NSProjectionPath, NSStmt, - NSStmtKind, NormalizedBindingLowering, NormalizedSemanticBody, SemanticBorrowDiagnostic, + NBorrowRoot, NBorrowRootId, NSPlace, NSPlaceRoot, NSProjectionPath, + NormalizedBindingLowering, NormalizedSemanticBody, SemanticBorrowDiagnostic, + layout_path_for_semantic_projection, resolved_layout_backing_places, + semantic_projection_for_layout_path, semantic_projection_ty, }, + loan::{AuthoritySet, LoanDef, LoanRef, ParentSet}, + region::{RegionProjection, RegionRoot, RegionSet, SymbolicPlace}, + shape::capability_shape, + summary::{BorrowSource, BorrowSourceClause, SummaryPath, SummaryProjection}, + transfer::{BorrowState, BorrowStateValueId, slot_path_for_layout}, }; -pub(super) fn address_space_for_borrow_root<'db>( +pub(super) fn address_space_for_region_root<'db>( db: &'db dyn HirAnalysisDb, instance: SemanticInstance<'db>, body: &NormalizedSemanticBody<'db>, - root: &BorrowRoot<'db>, + root: &RegionRoot<'db>, origin: SemOrigin<'db>, ) -> Result> { match root { - BorrowRoot::Param(_) | BorrowRoot::Local(_) => Ok(ProviderAddressSpace::Memory), - BorrowRoot::Provider(binding) => match binding.semantics.address_space { + RegionRoot::ParamPlace(_) | RegionRoot::ParamCapability { .. } | RegionRoot::Local(_) => { + Ok(ProviderAddressSpace::Memory) + } + RegionRoot::Provider(binding) => match binding.semantics.address_space { Some(space) => Ok(space), None if matches!(binding.semantics.kind, ProviderKind::RootObject) => { Ok(ProviderAddressSpace::Memory) @@ -51,78 +62,99 @@ pub(super) fn address_space_for_borrow_root<'db>( } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub(super) struct LoanId(pub(super) u32); - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub(super) enum BorrowRoot<'db> { - Param(u32), - Local(SLocalId), - Provider(crate::semantic::ProviderBinding<'db>), -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub(super) struct CanonPlace<'db> { - pub(super) root: BorrowRoot<'db>, - pub(super) proj: NSProjectionPath<'db>, -} - -#[derive(Clone, Debug)] -pub(super) struct Loan<'db> { - pub(super) kind: BorrowKind, - pub(super) targets: FxHashSet>, - pub(super) parents: FxHashSet, - pub(super) origin: SemOrigin<'db>, -} - -#[derive(Clone, Debug)] -pub(super) struct MoveSite<'db> { - pub(super) origin: SemOrigin<'db>, - pub(super) note: String, -} - -pub(super) type MovedPlaces<'db> = FxHashMap, MoveSite<'db>>; -pub(super) type BlockAdjacency = SmallVec; -pub(super) type CfgAdjacency = SecondaryMap; - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub(super) struct State { - pub(super) local_loans: FxHashMap>, -} - -impl State { - pub(super) fn loans_in(&self, local: SLocalId) -> FxHashSet { - self.local_loans.get(&local).cloned().unwrap_or_default() - } - - pub(super) fn assign_loans(&mut self, local: SLocalId, loans: FxHashSet) { - if loans.is_empty() { - self.local_loans.remove(&local); - } else { - self.local_loans.insert(local, loans); - } +fn region_projection_from_semantic<'db>( + path: &NSProjectionPath<'db>, +) -> Option> { + let mut out = Vec::new(); + for projection in path.iter() { + out.push(match projection { + Projection::Field(field) => { + RegionProjection::Field(FieldIndex(u16::try_from(*field).ok()?)) + } + Projection::VariantField { + variant, field_idx, .. + } => RegionProjection::VariantField { + variant: *variant, + field: FieldIndex(u16::try_from(*field_idx).ok()?), + }, + Projection::Index(IndexSource::Constant(index)) => { + RegionProjection::Index(IndexExpr::Const(*index)) + } + Projection::Index(IndexSource::Dynamic(index)) => { + RegionProjection::Index(IndexExpr::Runtime(*index)) + } + Projection::Discriminant => continue, + Projection::Deref => return None, + }); } + Some(out) } -impl JoinSemiLattice for State { - fn join_into(&mut self, other: &Self) -> bool { - let mut changed = false; - for (local, loans) in &other.local_loans { - let entry = self.local_loans.entry(*local).or_default(); - let before = entry.len(); - entry.extend(loans.iter().copied()); - changed |= before != entry.len(); +fn region_projection_for_layout_path<'db>( + db: &'db dyn HirAnalysisDb, + mut ty: TyId<'db>, + path: &[LayoutBackingProjection], +) -> Option> { + let mut out = Vec::new(); + let mut next_existential = 0; + for step in path { + ty = projectable_place_ty(db, ty); + match *step { + LayoutBackingProjection::Field(field) => { + ty = *ty.field_types(db).get(field.0 as usize)?; + out.push(RegionProjection::Field(field)); + } + LayoutBackingProjection::VariantField { variant, field } => { + let adt = ty.adt_def(db)?; + if !matches!(adt.adt_ref(db), AdtRef::Enum(_)) { + return None; + } + let field_ty = instantiate_adt_field_shape( + db, + adt, + variant.0 as usize, + field.0 as usize, + ty.generic_args(db), + ); + out.push(RegionProjection::VariantField { variant, field }); + ty = field_ty; + } + LayoutBackingProjection::Index(index) => { + if !ty.is_array(db) + || index.is_some_and(|index| ty.array_len(db).is_some_and(|len| index >= len)) + { + return None; + } + ty = *ty.generic_args(db).first()?; + out.push(RegionProjection::Index(index.map_or_else( + || { + let id = ExistentialId(next_existential); + next_existential += 1; + IndexExpr::Existential(id) + }, + IndexExpr::Const, + ))); + } + LayoutBackingProjection::IndexFamily(_) => { + if !ty.is_array(db) { + return None; + } + ty = *ty.generic_args(db).first()?; + let id = ExistentialId(next_existential); + next_existential += 1; + out.push(RegionProjection::Index(IndexExpr::Existential(id))); + } } - changed } + Some(out) } pub(super) struct BorrowCanonCx<'a, 'db> { db: &'db dyn HirAnalysisDb, instance: SemanticInstance<'db>, body: &'a NormalizedSemanticBody<'db>, - loans: &'a [Loan<'db>], - loan_for_local: &'a FxHashMap, + loans: &'a [LoanDef<'db>], + constant_indices: &'a SecondaryMap>, } impl<'a, 'db> BorrowCanonCx<'a, 'db> { @@ -130,207 +162,558 @@ impl<'a, 'db> BorrowCanonCx<'a, 'db> { db: &'db dyn HirAnalysisDb, instance: SemanticInstance<'db>, body: &'a NormalizedSemanticBody<'db>, - loans: &'a [Loan<'db>], - loan_for_local: &'a FxHashMap, + loans: &'a [LoanDef<'db>], + constant_indices: &'a SecondaryMap>, ) -> Self { Self { db, instance, body, loans, - loan_for_local, + constant_indices, } } - pub(super) fn apply_stmt_state(&self, state: &mut State, stmt: &NSStmt<'db>) { - let NSStmtKind::Assign { dst, expr } = &stmt.kind else { - return; - }; - let loans = match expr { - NExpr::Use(src) => { - let loans = state.loans_in(src.local); - if loans.is_empty() { - self.loan_for_local - .get(dst) - .copied() - .map(|loan| FxHashSet::from_iter([loan])) - .unwrap_or_default() - } else { - loans + fn materialize_constant_indices(&self, path: &NSProjectionPath<'db>) -> NSProjectionPath<'db> { + let mut out = NSProjectionPath::new(); + for projection in path.iter() { + out.push(match projection { + Projection::Index(IndexSource::Dynamic(index)) + if let Some(index) = self.constant_indices[*index] => + { + Projection::Index(IndexSource::Constant(index)) } - } - NExpr::Borrow { .. } | NExpr::Call { .. } => self - .loan_for_local - .get(dst) - .copied() - .map(|loan| FxHashSet::from_iter([loan])) - .unwrap_or_default(), - _ => FxHashSet::default(), - }; - state.assign_loans(*dst, loans); + projection => projection.clone(), + }); + } + out + } + + fn layout_path(&self, path: &NSProjectionPath<'db>) -> Option> { + layout_path_for_semantic_projection(&self.materialize_constant_indices(path)) + } + + pub(super) fn active_region_for_held(&self, held: &LoanRef, guard: &Guard) -> RegionSet<'db> { + self.loans[held.id.0 as usize] + .instantiate(held) + .with_guard(guard) } - pub(super) fn canonicalize_value_base( + fn deepest_held_projection_region( &self, - state: &State, + state: &BorrowState<'db>, local: SLocalId, - ) -> FxHashSet> { + path: &NSProjectionPath<'db>, + ) -> Option> { + let path = self.materialize_constant_indices(path); + let projection = self.layout_path(&path)?; + let shape = self.local_shape(local)?; + let (depth, value) = (0..=projection.len()).rev().find_map(|depth| { + let path = slot_path_for_layout(self.db, shape, &projection[..depth])?; + let value = state.project(local, &path, super::guard::ValueScope::Local(local))?; + (!state + .leaves(value, super::guard::ValueScope::Local(local)) + .is_empty()) + .then_some((depth, value)) + })?; + let mut consumed = 0; + let mut suffix = NSProjectionPath::default(); + for projection in path.iter() { + if consumed < depth { + if !matches!(projection, Projection::Deref) { + consumed += 1; + } + continue; + } + if suffix.is_empty() && matches!(projection, Projection::Deref) { + continue; + } + suffix.push(projection.clone()); + } + let suffix = region_projection_from_semantic(&suffix)?; + let mut region = RegionSet::empty(); + for leaf in state.leaves(value, super::guard::ValueScope::Local(local)) { + region = region.union( + &self + .active_region_for_held(&leaf.payload, &leaf.guard) + .project(&suffix), + ); + } + Some(region) + } + + fn local_shape(&self, local: SLocalId) -> Option> { + self.body + .local(local) + .map(|local| capability_shape(self.db, local.ty)) + } + + fn place_base_local(&self, place: &NSPlace<'db>) -> Option { + match place.root { + NSPlaceRoot::CarrierDerefLocal(local) => Some(local), + NSPlaceRoot::Root(root) => match self.body.root(root)? { + NBorrowRoot::Param { local, .. } | NBorrowRoot::LocalSlot { local } => Some(*local), + NBorrowRoot::Provider { .. } => None, + }, + } + } + + pub(super) fn value_region(&self, state: &BorrowState<'db>, local: SLocalId) -> RegionSet<'db> { if self .body .local(local) .is_some_and(|local| local.ty.as_borrow(self.db).is_some()) { - return self.borrow_local_targets(state, local); + return self.borrow_local_region(state, local); } let Some(local_data) = self.body.local(local) else { - return FxHashSet::default(); + return RegionSet::empty(); }; if let Some(place) = local_data.lowering.place() { - return self.canonicalize_place_targets(state, place); + return self.place_region(state, place); } let root = match &local_data.lowering { NormalizedBindingLowering::CarrierLocal { root, provider, .. } => provider .clone() - .map(BorrowRoot::Provider) - .or_else(|| root.and_then(|root| self.root_to_borrow_root(root))), + .map(RegionRoot::Provider) + .or_else(|| root.and_then(|root| self.root_to_region_root(root))), NormalizedBindingLowering::Erased => None, NormalizedBindingLowering::ValueLocal { .. } | NormalizedBindingLowering::PlaceBoundValue { .. } => unreachable!(), }; - root.into_iter() - .map(|root| CanonPlace { - root, - proj: NSProjectionPath::default(), - }) - .collect() + root.map_or_else(RegionSet::empty, |root| { + RegionSet::singleton(SymbolicPlace::new(root, [])) + }) } - pub(super) fn borrow_local_targets( + pub(super) fn value_projection_region( &self, - state: &State, + state: &BorrowState<'db>, local: SLocalId, - ) -> FxHashSet> { - let mut out = FxHashSet::default(); - for loan in state.loans_in(local) { - out.extend(self.loans[loan.0 as usize].targets.iter().cloned()); + projection: &NSProjectionPath<'db>, + ) -> RegionSet<'db> { + let Some(local_data) = self.body.local(local) else { + return RegionSet::empty(); + }; + let projection = self.materialize_constant_indices(projection); + let traverses_capability = semantic_projection_ty(self.db, local_data.ty, &projection) + .is_none_or(|(_, traverses_capability)| traverses_capability); + if traverses_capability + && let Some(region) = self.deepest_held_projection_region(state, local, &projection) + { + return region; + } + if local_data.ty.as_borrow(self.db).is_none() { + let resolved = + resolved_layout_backing_places(local_data.layout_backing_sources(), &projection); + if !resolved.is_empty() { + return resolved + .iter() + .map(|place| self.place_region(state, place)) + .fold(RegionSet::empty(), |region, source| region.union(&source)); + } } - if !out.is_empty() { - return out; + if local_data.ty.as_borrow(self.db).is_none() + && ty_is_noesc(self.db, local_data.ty) + && !matches!( + local_data.source, + Some(LocalBinding::Param { .. } | LocalBinding::EffectParam { .. }) + ) + && traverses_capability + { + return RegionSet::empty(); } + region_projection_from_semantic(&projection).map_or_else(RegionSet::empty, |projection| { + self.value_region(state, local).project(&projection) + }) + } + + pub(super) fn value_layout_region( + &self, + state: &BorrowState<'db>, + local: SLocalId, + projection: &[LayoutBackingProjection], + ) -> RegionSet<'db> { + let Some(local_ty) = self.body.local(local).map(|local| local.ty) else { + return RegionSet::empty(); + }; + if let Some(projection) = semantic_projection_for_layout_path(self.db, local_ty, projection) + { + return self.value_projection_region(state, local, &projection); + } let Some(local_data) = self.body.local(local) else { - return FxHashSet::default(); + return RegionSet::empty(); }; + if let Some(shape) = self.local_shape(local) + && let Some(path) = slot_path_for_layout(self.db, shape, projection) + && let Some(value) = state.project(local, &path, super::guard::ValueScope::Local(local)) + { + let region = state + .leaves(value, super::guard::ValueScope::Local(local)) + .into_iter() + .fold(RegionSet::empty(), |region, leaf| { + region.union(&self.active_region_for_held(&leaf.payload, &leaf.guard)) + }); + if !region.is_empty() { + return region; + } + } + let Some(suffix) = region_projection_for_layout_path(self.db, local_ty, projection) else { + return self.borrow_local_region(state, local); + }; + if local_data.ty.as_borrow(self.db).is_none() + && ty_is_noesc(self.db, local_data.ty) + && !matches!( + local_data.source, + Some(LocalBinding::Param { .. } | LocalBinding::EffectParam { .. }) + ) + { + return RegionSet::empty(); + } + self.value_region(state, local).project(&suffix) + } + + pub(super) fn instantiate_call_source( + &self, + state: &BorrowState<'db>, + args: &[super::ir::NOperand], + clause: &BorrowSourceClause, + ) -> (RegionSet<'db>, ParentSet) { + let Some(arg) = args.get(clause.source.param() as usize) else { + return (RegionSet::empty(), ParentSet::default()); + }; + let (region, parents) = match &clause.source { + BorrowSource::ParamCapability { param, slot } => self + .local_shape(arg.local) + .and_then(|shape| super::transfer::slot_path_for_summary(self.db, shape, slot)) + .and_then(|path| { + state.project(arg.local, &path, super::guard::ValueScope::Argument(*param)) + }) + .map(|value| self.regions_and_parents_for_value(state, value, *param)) + .unwrap_or_default(), + BorrowSource::ParamPlace { path, .. } => { + let region = region_projection_for_summary_path(path) + .map(|path| self.value_region(state, arg.local).project(&path)) + .unwrap_or_default(); + (region, ParentSet::default()) + } + BorrowSource::AnyAccessible { param, class } => { + let mut resolved = self + .local_shape(arg.local) + .zip(state.value(arg.local)) + .map(|(_, value)| self.regions_and_parents_for_value(state, value, *param)) + .unwrap_or_default(); + let direct = self.value_region(state, arg.local); + resolved.0 = resolved.0.union(&direct); + if matches!(class, super::summary::AccessClass::Shared) { + resolved.1 = ParentSet::default(); + } + resolved + } + }; + (region.with_guard(&clause.guard), parents) + } + + fn regions_and_parents_for_value( + &self, + state: &BorrowState<'db>, + value: BorrowStateValueId<'db>, + param: u32, + ) -> (RegionSet<'db>, ParentSet) { + let mut region = RegionSet::empty(); + let mut parents = Vec::new(); + for leaf in state.leaves(value, super::guard::ValueScope::Argument(param)) { + region = region.union(&self.active_region_for_held(&leaf.payload, &leaf.guard)); + if self.loans[leaf.payload.id.0 as usize].kind() == BorrowKind::Mut { + parents.push((leaf.guard, leaf.payload)); + } + } + (region, ParentSet::from_guarded_references(parents)) + } + + pub(super) fn place_layout_region( + &self, + state: &BorrowState<'db>, + place: &NSPlace<'db>, + target_ty: TyId<'db>, + projection: &[LayoutBackingProjection], + ) -> RegionSet<'db> { + if let Some(suffix) = semantic_projection_for_layout_path(self.db, target_ty, projection) { + if let Some(local) = self.place_base_local(place) { + let path = self + .materialize_constant_indices(&place.path) + .concat(&suffix); + return self.value_projection_region(state, local, &path); + } + let mut projected = place.clone(); + projected.path = projected.path.concat(&suffix); + return self.place_region(state, &projected); + } + + if let Some(local) = self.place_base_local(place) { + let mut path = self + .layout_path(&self.materialize_constant_indices(&place.path)) + .unwrap_or_default(); + path.extend_from_slice(projection); + let region = self + .local_shape(local) + .and_then(|shape| slot_path_for_layout(self.db, shape, &path)) + .and_then(|path| { + state.project(local, &path, super::guard::ValueScope::Local(local)) + }) + .into_iter() + .flat_map(|value| state.leaves(value, super::guard::ValueScope::Local(local))) + .fold(RegionSet::empty(), |region, leaf| { + region.union(&self.active_region_for_held(&leaf.payload, &leaf.guard)) + }); + if !region.is_empty() { + return region; + } + if self.body.local(local).is_some_and(|local| { + ty_is_noesc(self.db, local.ty) + && !matches!( + local.source, + Some(LocalBinding::Param { .. } | LocalBinding::EffectParam { .. }) + ) + }) { + return RegionSet::empty(); + } + } + + self.place_region(state, place) + } + + pub(super) fn borrow_local_region( + &self, + state: &BorrowState<'db>, + local: SLocalId, + ) -> RegionSet<'db> { + let Some(local_data) = self.body.local(local) else { + return RegionSet::empty(); + }; + let held_loans = state.leaves_in(local, super::guard::ValueScope::Local(local)); + let has_tracked_loan = !held_loans.is_empty(); + let mut region = RegionSet::empty(); + for leaf in held_loans { + region = region.union(&self.active_region_for_held(&leaf.payload, &leaf.guard)); + } + if !region.is_empty() || has_tracked_loan { + return region; + } + if let Some(place) = local_data.lowering.place() { - return self.canonicalize_place_targets(state, place); + return self.place_region(state, place); } match &local_data.lowering { NormalizedBindingLowering::CarrierLocal { root, provider, .. } => provider .clone() - .map(BorrowRoot::Provider) - .or_else(|| root.and_then(|root| self.root_to_borrow_root(root))) - .into_iter() - .map(|root| CanonPlace { - root, - proj: NSProjectionPath::default(), - }) - .collect(), - NormalizedBindingLowering::Erased => FxHashSet::default(), + .map(RegionRoot::Provider) + .or_else(|| root.and_then(|root| self.root_to_region_root(root))) + .map_or_else(RegionSet::empty, |root| { + RegionSet::singleton(SymbolicPlace::new(root, [])) + }), + NormalizedBindingLowering::Erased => RegionSet::empty(), NormalizedBindingLowering::ValueLocal { .. } - | NormalizedBindingLowering::PlaceBoundValue { .. } => FxHashSet::default(), + | NormalizedBindingLowering::PlaceBoundValue { .. } => RegionSet::empty(), } } - pub(super) fn canonicalize_place( + pub(super) fn resolve_place( &self, - state: &State, + state: &BorrowState<'db>, place: &NSPlace<'db>, origin: SemOrigin<'db>, - ) -> Result>, SemanticBorrowDiagnostic<'db>> { - let out = self.canonicalize_place_targets(state, place); - if out.is_empty() { + ) -> Result, SemanticBorrowDiagnostic<'db>> { + let region = self.place_region(state, place); + if region.is_empty() { return Err(self.internal_diag( origin, "cannot canonicalize carrier-rooted place".to_string(), )); } - Ok(out) + Ok(region) } - fn canonicalize_place_targets( - &self, - state: &State, - place: &NSPlace<'db>, - ) -> FxHashSet> { + fn place_region(&self, state: &BorrowState<'db>, place: &NSPlace<'db>) -> RegionSet<'db> { + let Some(path) = + region_projection_from_semantic(&self.materialize_constant_indices(&place.path)) + else { + return RegionSet::empty(); + }; match place.root { - NSPlaceRoot::Root(root) => FxHashSet::from_iter([CanonPlace { - root: self - .root_to_borrow_root(root) - .expect("normalized borrow root"), - proj: place.path.clone(), - }]), + NSPlaceRoot::Root(root) => { + let root = self + .root_to_region_root(root) + .expect("normalized borrow root"); + RegionSet::singleton(SymbolicPlace::new(root, path)) + } NSPlaceRoot::CarrierDerefLocal(local) => { - let suffix = place.path.clone(); - let mut out = FxHashSet::default(); + let suffix = path; + let mut region = RegionSet::empty(); let mut resolved = false; - for loan in state.loans_in(local) { + for leaf in state.leaves_in(local, super::guard::ValueScope::Local(local)) { resolved = true; - for target in &self.loans[loan.0 as usize].targets { - out.insert(CanonPlace { - root: target.root.clone(), - proj: target.proj.concat(&suffix), - }); - } + region = region.union( + &self + .active_region_for_held(&leaf.payload, &leaf.guard) + .project(&suffix), + ); } if !resolved && let Some(NormalizedBindingLowering::CarrierLocal { root, provider, .. }) = self.body.local(local).map(|local| &local.lowering) { if let Some(provider) = provider { - out.insert(CanonPlace { - root: BorrowRoot::Provider(provider.clone()), - proj: suffix.clone(), - }); - } else if let Some(root) = root.and_then(|root| self.root_to_borrow_root(root)) + region = region.union(&RegionSet::singleton(SymbolicPlace::new( + RegionRoot::Provider(provider.clone()), + suffix.clone(), + ))); + } else if let Some(root) = root.and_then(|root| self.root_to_region_root(root)) { - out.insert(CanonPlace { root, proj: suffix }); + region = + region.union(&RegionSet::singleton(SymbolicPlace::new(root, suffix))); } } - out + region } } } - pub(super) fn root_to_borrow_root(&self, root: NBorrowRootId) -> Option> { + pub(super) fn root_to_region_root(&self, root: NBorrowRootId) -> Option> { match self.body.root(root)? { - NBorrowRoot::Param { param_idx, .. } => Some(BorrowRoot::Param(*param_idx)), - NBorrowRoot::LocalSlot { local } => Some(BorrowRoot::Local(*local)), - NBorrowRoot::Provider { binding, .. } => Some(BorrowRoot::Provider(binding.clone())), + NBorrowRoot::Param { param_idx, .. } => Some(RegionRoot::ParamPlace(*param_idx)), + NBorrowRoot::LocalSlot { local } => Some(RegionRoot::Local(*local)), + NBorrowRoot::Provider { binding, .. } => Some(RegionRoot::Provider(binding.clone())), } } - pub(super) fn mut_loans_for_place( + pub(super) fn mut_authority_for_place( &self, - state: &State, + state: &BorrowState<'db>, place: &NSPlace<'db>, - ) -> FxHashSet { - let active_loans = match place.root { - NSPlaceRoot::CarrierDerefLocal(local) => state.loans_in(local), - NSPlaceRoot::Root(_) => FxHashSet::default(), - }; - active_loans - .into_iter() - .filter(|loan| self.loans[loan.0 as usize].kind == BorrowKind::Mut) - .collect() + ) -> AuthoritySet { + match place.root { + NSPlaceRoot::CarrierDerefLocal(local) => { + self.authority_for_value(state, local, Some(BorrowKind::Mut), None) + } + NSPlaceRoot::Root(_) => AuthoritySet::default(), + } } - pub(super) fn mut_loans_for_value(&self, state: &State, local: SLocalId) -> FxHashSet { - state - .loans_in(local) - .into_iter() - .filter(|loan| self.loans[loan.0 as usize].kind == BorrowKind::Mut) - .collect() + pub(super) fn mut_parent_refs_for_place( + &self, + state: &BorrowState<'db>, + place: &NSPlace<'db>, + region: &RegionSet<'db>, + ) -> ParentSet { + self.place_base_local(place) + .map_or_else(ParentSet::default, |local| { + self.mut_parent_refs_for_value(state, local, region) + }) + } + + pub(super) fn mut_authority_for_place_targets( + &self, + state: &BorrowState<'db>, + place: &NSPlace<'db>, + region: &RegionSet<'db>, + ) -> AuthoritySet { + self.place_base_local(place).map_or_else( + || self.mut_authority_for_place(state, place), + |local| self.authority_for_value(state, local, Some(BorrowKind::Mut), Some(region)), + ) + } + + pub(super) fn authority_for_place_targets( + &self, + state: &BorrowState<'db>, + place: &NSPlace<'db>, + region: &RegionSet<'db>, + ) -> AuthoritySet { + self.place_base_local(place).map_or_else( + || self.authority_for_place(state, place), + |local| self.authority_for_value(state, local, None, Some(region)), + ) + } + + pub(super) fn authority_for_place( + &self, + state: &BorrowState<'db>, + place: &NSPlace<'db>, + ) -> AuthoritySet { + match place.root { + NSPlaceRoot::CarrierDerefLocal(local) => { + self.authority_for_value(state, local, None, None) + } + NSPlaceRoot::Root(_) => AuthoritySet::default(), + } + } + + pub(super) fn mut_parent_refs_for_value( + &self, + state: &BorrowState<'db>, + local: SLocalId, + region: &RegionSet<'db>, + ) -> ParentSet { + self.value_authorities(state, local, Some(BorrowKind::Mut), Some(region)) + } + + pub(super) fn mut_authority_for_value_targets( + &self, + state: &BorrowState<'db>, + local: SLocalId, + region: &RegionSet<'db>, + ) -> AuthoritySet { + self.authority_for_value(state, local, Some(BorrowKind::Mut), Some(region)) + } + + pub(super) fn authority_for_value_targets( + &self, + state: &BorrowState<'db>, + local: SLocalId, + region: &RegionSet<'db>, + ) -> AuthoritySet { + self.authority_for_value(state, local, None, Some(region)) + } + + fn authority_for_value( + &self, + state: &BorrowState<'db>, + local: SLocalId, + kind: Option, + region: Option<&RegionSet<'db>>, + ) -> AuthoritySet { + AuthoritySet::from_parents(self.value_authorities(state, local, kind, region)) + } + + fn value_authorities( + &self, + state: &BorrowState<'db>, + local: SLocalId, + kind: Option, + region: Option<&RegionSet<'db>>, + ) -> ParentSet { + let mut leaves = state.leaves_in(local, super::guard::ValueScope::Local(local)); + if leaves.is_empty() + && let Some(source) = self + .body + .local(local) + .and_then(|local| local.snapshot_source_place()) + .and_then(|source| self.place_base_local(source)) + { + leaves = state.leaves_in(source, super::guard::ValueScope::Local(source)); + } + ParentSet::from_guarded_references(leaves.into_iter().filter_map(|leaf| { + (kind.is_none_or(|kind| self.loans[leaf.payload.id.0 as usize].kind() == kind) + && region.is_none_or(|region| { + self.active_region_for_held(&leaf.payload, &leaf.guard) + .may_overlap(region) + .is_some() + })) + .then_some((leaf.guard, leaf.payload)) + })) } fn internal_diag( @@ -342,14 +725,18 @@ impl<'a, 'db> BorrowCanonCx<'a, 'db> { } } -pub(super) fn place_set_overlaps<'db>( - lhs: &FxHashSet>, - rhs: &FxHashSet>, -) -> bool { - lhs.iter() - .any(|lhs| rhs.iter().any(|rhs| places_overlap(lhs, rhs))) -} - -pub(super) fn places_overlap<'db>(lhs: &CanonPlace<'db>, rhs: &CanonPlace<'db>) -> bool { - lhs.root == rhs.root && !matches!(lhs.proj.may_alias(&rhs.proj), Aliasing::No) +fn region_projection_for_summary_path(path: &SummaryPath) -> Option> { + path.as_slice() + .iter() + .map(|projection| match projection { + SummaryProjection::Field(field) => Some(RegionProjection::Field(*field)), + SummaryProjection::VariantField { variant, field } => { + Some(RegionProjection::VariantField { + variant: *variant, + field: *field, + }) + } + SummaryProjection::Index(index) => Some(RegionProjection::Index(*index)), + }) + .collect() } diff --git a/crates/hir/src/analysis/semantic/borrowck/check.rs b/crates/hir/src/analysis/semantic/borrowck/check.rs index 5006b81765..0c4f992e14 100644 --- a/crates/hir/src/analysis/semantic/borrowck/check.rs +++ b/crates/hir/src/analysis/semantic/borrowck/check.rs @@ -1,6 +1,9 @@ +use std::collections::{BTreeMap, VecDeque}; + use common::diagnostics::CompleteDiagnostic; use cranelift_entity::{EntityRef, SecondaryMap}; use dataflow::{solve_backward_cfg, solve_forward_cfg, try_solve_forward_cfg, try_solve_sparse}; +use num_traits::ToPrimitive; use rustc_hash::{FxHashMap, FxHashSet}; use crate::{ @@ -9,35 +12,49 @@ use crate::{ analysis_pass::ModuleAnalysisPass, diagnostics::{DiagnosticVoucher, SpannedHirAnalysisDb}, semantic::{ - SBlockId, SemOrigin, SemanticInstance, get_or_build_semantic_instance, - identity_semantic_instance_key, + BorrowActivation, LayoutBackingProjection, SBlockId, SConst, SStmtId, SemConstScalar, + SemConstValue, SemOrigin, SemanticInstance, SemanticInstanceKey, + get_or_build_semantic_instance, identity_semantic_instance_key, + }, + ty::{ + ty_check::{BodyOwner, EffectParamSite}, + ty_def::{BorrowKind, TyId}, + ty_is_borrow, }, - ty::{ty_check::BodyOwner, ty_def::BorrowKind}, }, + core::semantic::EffectEnvView, hir_def::{Body, Expr, FuncParamMode, ItemKind, Partial, TopLevelMod}, - projection::{IndexSource, Projection}, }; use super::{ + access::{ActiveLoan, CallAccess, MoveSite, MovedPlaces, active_loans_in, effective_loans}, analyses::{ - BorrowEntryStateAnalysis, BorrowLivenessAnalysis, BorrowLoanTargetAnalysis, - BorrowLoanTargetState, BorrowMovedStateAnalysis, BorrowSummaryMode, - }, - canon::{ - BlockAdjacency, BorrowCanonCx, BorrowRoot, CanonPlace, CfgAdjacency, Loan, LoanId, - MoveSite, MovedPlaces, State, place_set_overlaps, places_overlap, + BlockAdjacency, BorrowEntryStateAnalysis, BorrowLivenessAnalysis, BorrowLoanTargetAnalysis, + BorrowLoanTargetState, BorrowMovedStateAnalysis, BorrowSummaryMode, CfgAdjacency, }, + canon::BorrowCanonCx, diagnostics::operand_origin, facts::NormalizedBodyFacts, + guard::{ExistentialId, Guard, IndexExpr, IndexParamId, IndexSubst, ResultIndexId}, ir::{ - BorrowDiagnosticId, BorrowInputRef, BorrowSummary, BorrowSummaryId, BorrowTransform, - NBorrowRoot, NBorrowRootId, NExpr, NOperand, NSPlace, NSPlaceRoot, NSProjectionPath, - NSStmtKind, NSTerminatorKind, NormalizedBindingLowering, NormalizedSemanticBody, ReadMode, - SemanticBorrowCheckResult, SemanticBorrowDiagKind, SemanticBorrowDiagnostic, + BorrowDiagnosticId, BorrowSummaryId, NBorrowRoot, NBorrowRootId, NEffectArgValue, NExpr, + NOperand, NSPlace, NSPlaceRoot, NSStmtKind, NSTerminatorKind, NormalizedSemanticBody, + ReadMode, SemanticBorrowCheckResult, SemanticBorrowDiagKind, SemanticBorrowDiagnostic, SemanticBorrowDiagnosticSpan, SemanticBorrowSummaryResult, - local_has_runtime_move_semantics, + local_has_runtime_move_semantics, semantic_projection_ty, }, + loan::{AuthoritySet, LoanDef, LoanId, LoanRef, ParentSet}, normalize::{normalize_provisional_semantic_body, normalize_semantic_body}, + region::{RegionProjection, RegionRoot, RegionSet, SymbolicPlace}, + shape::{SlotPath, SlotProjection, capability_shape, capability_slots}, + summary::{ + BorrowSource, BorrowSourceClause, BorrowSummary, BorrowSummaryLeaf, SummaryPath, + SummaryProjection, validate_borrow_summary, + }, + transfer::{ + BorrowState, BorrowStateValueId, BorrowTransferCx, SharedBorrowValueInterner, + shared_value_interner, slot_loan_value, + }, verify::verify_normalized_semantic_body, }; @@ -49,10 +66,16 @@ fn semantic_borrow_summary_query<'db>( db: &'db dyn HirAnalysisDb, instance: SemanticInstance<'db>, ) -> SemanticBorrowSummaryResult<'db> { - if !instance_returns_borrow(db, instance) { + if !instance_returns_borrowing_value(db, instance) { return SemanticBorrowSummaryResult::Ok(None); } - match Borrowck::new(db, instance).and_then(Borrowck::borrow_summary) { + if instance.key(db).owner(db).body(db).is_none() { + return SemanticBorrowSummaryResult::Ok(Some(BorrowSummaryId::new( + db, + conservative_signature_borrow_summary(db, instance), + ))); + } + match Borrowck::new_for_summary(db, instance).and_then(Borrowck::borrow_summary) { Ok(summary) => SemanticBorrowSummaryResult::Ok( summary.map(|summary| BorrowSummaryId::new(db, summary)), ), @@ -68,9 +91,15 @@ fn provisional_borrow_summary_query<'db>( db: &'db dyn HirAnalysisDb, instance: SemanticInstance<'db>, ) -> SemanticBorrowSummaryResult<'db> { - if !instance_returns_borrow(db, instance) { + if !instance_returns_borrowing_value(db, instance) { return SemanticBorrowSummaryResult::Ok(None); } + if instance.key(db).owner(db).body(db).is_none() { + return SemanticBorrowSummaryResult::Ok(Some(BorrowSummaryId::new( + db, + conservative_signature_borrow_summary(db, instance), + ))); + } let body = match normalize_provisional_semantic_body(db, instance) { Ok(body) => body, Err(diag) => return SemanticBorrowSummaryResult::Err(BorrowDiagnosticId::new(db, diag)), @@ -88,17 +117,17 @@ fn provisional_borrow_summary_query<'db>( pub fn semantic_borrow_summary<'db>( db: &'db dyn SpannedHirAnalysisDb, instance: SemanticInstance<'db>, -) -> Result>, CompleteDiagnostic> { +) -> Result, CompleteDiagnostic> { semantic_borrow_summary_voucher(db, instance).map_err(|diag| diag.to_complete(db)) } pub(super) fn semantic_borrow_summary_voucher<'db>( db: &'db dyn HirAnalysisDb, instance: SemanticInstance<'db>, -) -> Result>, SemanticBorrowDiagnostic<'db>> { +) -> Result, SemanticBorrowDiagnostic<'db>> { match semantic_borrow_summary_query(db, instance) { SemanticBorrowSummaryResult::Ok(summary) => { - Ok(summary.map(|summary| summary.items(db).clone())) + Ok(summary.map(|summary| summary.summary(db).clone())) } SemanticBorrowSummaryResult::Err(diag) => Err(diag.diag(db).clone()), } @@ -107,10 +136,10 @@ pub(super) fn semantic_borrow_summary_voucher<'db>( pub(super) fn provisional_borrow_summary_voucher<'db>( db: &'db dyn HirAnalysisDb, instance: SemanticInstance<'db>, -) -> Result>, SemanticBorrowDiagnostic<'db>> { +) -> Result, SemanticBorrowDiagnostic<'db>> { match provisional_borrow_summary_query(db, instance) { SemanticBorrowSummaryResult::Ok(summary) => { - Ok(summary.map(|summary| summary.items(db).clone())) + Ok(summary.map(|summary| summary.summary(db).clone())) } SemanticBorrowSummaryResult::Err(diag) => Err(diag.diag(db).clone()), } @@ -154,24 +183,23 @@ pub fn collect_semantic_borrow_diagnostic_vouchers<'db>( top_mod: TopLevelMod<'db>, ) -> Vec> { let mut diags = Vec::new(); - let mut seen_owners = FxHashSet::default(); + let mut pending = VecDeque::new(); let mut seen_diags = FxHashSet::default(); - collect_top_mod_semantic_borrow_diagnostic_vouchers( - db, - top_mod, - &mut seen_owners, - &mut seen_diags, - &mut diags, - ); + collect_top_mod_semantic_borrow_diagnostic_vouchers(db, top_mod, &mut pending); + let mut seen_instances = FxHashSet::default(); + while let Some(instance) = pending.pop_front() { + if !seen_instances.insert(instance.key(db)) { + continue; + } + collect_instance(db, instance, &mut pending, &mut seen_diags, &mut diags); + } diags } fn collect_top_mod_semantic_borrow_diagnostic_vouchers<'db>( db: &'db dyn HirAnalysisDb, top_mod: TopLevelMod<'db>, - seen_owners: &mut FxHashSet>, - seen_diags: &mut FxHashSet>, - diags: &mut Vec>, + pending: &mut VecDeque>, ) { for item in top_mod .all_items(db) @@ -179,39 +207,37 @@ fn collect_top_mod_semantic_borrow_diagnostic_vouchers<'db>( .filter(|item| item.top_mod(db) == top_mod) { match item { - ItemKind::Func(func) => { - collect_owner(db, BodyOwner::Func(*func), seen_owners, seen_diags, diags) - } - ItemKind::Const(const_) => collect_owner( + ItemKind::Func(func) => pending.push_back(get_or_build_semantic_instance( db, - BodyOwner::Const(*const_), - seen_owners, - seen_diags, - diags, - ), + identity_semantic_instance_key(db, BodyOwner::Func(*func)), + )), + ItemKind::Const(const_) => pending.push_back(get_or_build_semantic_instance( + db, + identity_semantic_instance_key(db, BodyOwner::Const(*const_)), + )), ItemKind::Contract(contract) => { - collect_owner( + pending.push_back(get_or_build_semantic_instance( db, - BodyOwner::ContractInit { - contract: *contract, - }, - seen_owners, - seen_diags, - diags, - ); + identity_semantic_instance_key( + db, + BodyOwner::ContractInit { + contract: *contract, + }, + ), + )); for (recv_idx, recv) in contract.recvs(db).data(db).iter().enumerate() { for arm_idx in 0..recv.arms.data(db).len() { - collect_owner( + pending.push_back(get_or_build_semantic_instance( db, - BodyOwner::ContractRecvArm { - contract: *contract, - recv_idx: recv_idx as u32, - arm_idx: arm_idx as u32, - }, - seen_owners, - seen_diags, - diags, - ); + identity_semantic_instance_key( + db, + BodyOwner::ContractRecvArm { + contract: *contract, + recv_idx: recv_idx as u32, + arm_idx: arm_idx as u32, + }, + ), + )); } } } @@ -230,18 +256,13 @@ fn collect_top_mod_semantic_borrow_diagnostic_vouchers<'db>( } } -fn collect_owner<'db>( +fn collect_instance<'db>( db: &'db dyn HirAnalysisDb, - owner: BodyOwner<'db>, - seen_owners: &mut FxHashSet>, + instance: SemanticInstance<'db>, + pending: &mut VecDeque>, seen_diags: &mut FxHashSet>, diags: &mut Vec>, ) { - if !seen_owners.insert(owner) { - return; - } - let key = identity_semantic_instance_key(db, owner); - let instance = get_or_build_semantic_instance(db, key); if let SemanticBorrowCheckResult::Err(diag) = semantic_borrow_check_query(db, instance) && seen_diags.insert(diag) { @@ -253,6 +274,50 @@ fn collect_owner<'db>( { diags.push(Box::new(diag)); } + // Address spaces supplied by effect handles exist only on finalized callee + // instances. Walk through every reachable specialization because an ordinary + // generic wrapper may sit between the root and a closure-bearing effect call. + // Unrelated monomorphizations are left to their parametric identity-owner + // check above. + pending.extend( + instance + .callees(db) + .iter() + .filter(|callee| is_fully_instantiated_key(db, callee.key)) + .map(|callee| get_or_build_semantic_instance(db, callee.key)), + ); +} + +fn is_fully_instantiated_key<'db>( + db: &'db dyn HirAnalysisDb, + key: SemanticInstanceKey<'db>, +) -> bool { + let args_are_concrete = key + .subst(db) + .generic_args(db) + .iter() + .all(|arg| !arg.has_param(db) && !arg.has_var(db)); + let providers = key.effect_providers(db).providers(db); + let providers_are_concrete = providers.iter().all(|specialization| { + let provider = &specialization.provider; + [ + provider.provider_ty, + provider.semantics.provider_ty, + provider.effective_target_ty(), + ] + .into_iter() + .all(|ty| !ty.has_param(db) && !ty.has_var(db)) + }); + let has_all_effect_providers = match key.owner(db) { + BodyOwner::Func(func) => { + EffectEnvView::new(EffectParamSite::Func(func)) + .requirements(db) + .is_empty() + || !providers.is_empty() + } + _ => true, + }; + args_are_concrete && providers_are_concrete && has_all_effect_providers } pub(super) struct Borrowck<'db> { @@ -265,9 +330,14 @@ pub(super) struct Borrowck<'db> { param_modes: Vec, param_index_of_local: FxHashMap, pub(super) loan_for_local: FxHashMap, - pub(super) param_loan_for_local: FxHashMap, - loans: Vec>, - pub(super) entry_state: SecondaryMap, + pub(super) param_values_for_local: + FxHashMap>, + pub(super) value_interner: SharedBorrowValueInterner<'db>, + loans: Vec>, + pub(super) entry_state: SecondaryMap>, + call_result_loans: FxHashMap>, + call_loan_sources: FxHashMap>, + constant_indices: SecondaryMap>, moved_entry: SecondaryMap>, live_before: Vec>>, live_before_term: SecondaryMap>, @@ -279,7 +349,15 @@ impl<'db> Borrowck<'db> { instance: SemanticInstance<'db>, ) -> Result> { let body = normalize_semantic_body(db, instance)?; - Self::new_with_body(db, instance, body, BorrowSummaryMode::Final) + Self::new_with_body(db, instance, body, BorrowSummaryMode::FinalCheck) + } + + fn new_for_summary( + db: &'db dyn HirAnalysisDb, + instance: SemanticInstance<'db>, + ) -> Result> { + let body = normalize_semantic_body(db, instance)?; + Self::new_with_body(db, instance, body, BorrowSummaryMode::FinalSummary) } pub(super) fn new_with_body( @@ -305,6 +383,55 @@ impl<'db> Borrowck<'db> { } } let facts = NormalizedBodyFacts::new(&body); + let mut constant_candidates = FxHashMap::default(); + let mut stored_locals = FxHashSet::default(); + for stmt in body.blocks.iter().flat_map(|block| &block.stmts) { + match &stmt.kind { + NSStmtKind::Assign { dst, expr } => { + let value = match expr { + NExpr::Const(SConst::Value(value)) => match value.value(db) { + SemConstValue::Scalar { + value: SemConstScalar::Int { value }, + .. + } => value.to_usize(), + _ => None, + }, + _ => None, + }; + constant_candidates + .entry(*dst) + .and_modify(|candidate| { + if *candidate != value { + *candidate = None; + } + }) + .or_insert(value); + } + NSStmtKind::Store { + dst: + NSPlace { + root: NSPlaceRoot::Root(root), + .. + }, + .. + } => match body.root(*root) { + Some(NBorrowRoot::Param { local, .. }) + | Some(NBorrowRoot::LocalSlot { local }) => { + stored_locals.insert(*local); + } + Some(NBorrowRoot::Provider { .. }) | None => {} + }, + NSStmtKind::Store { .. } => {} + } + } + let mut constant_indices = SecondaryMap::new(); + constant_indices.resize(body.locals.len()); + for (local, value) in constant_candidates { + if !stored_locals.contains(&local) { + constant_indices[local] = value; + } + } + let value_interner = shared_value_interner(db); let mut checker = Self { db, instance, @@ -315,14 +442,18 @@ impl<'db> Borrowck<'db> { param_modes, param_index_of_local, loan_for_local: FxHashMap::default(), - param_loan_for_local: FxHashMap::default(), + param_values_for_local: FxHashMap::default(), + value_interner: value_interner.clone(), loans: Vec::new(), - entry_state: SecondaryMap::new(), + entry_state: SecondaryMap::with_default(BorrowState::new(value_interner)), + call_result_loans: FxHashMap::default(), + call_loan_sources: FxHashMap::default(), + constant_indices, moved_entry: SecondaryMap::new(), live_before: Vec::new(), live_before_term: SecondaryMap::new(), }; - checker.init_loans(); + checker.init_loans()?; Ok(checker) } @@ -332,16 +463,15 @@ impl<'db> Borrowck<'db> { self.instance, &self.body, &self.loans, - &self.loan_for_local, + &self.constant_indices, ) } - fn borrow_summary( - mut self, - ) -> Result>, SemanticBorrowDiagnostic<'db>> { - let owner = self.instance.key(self.db).owner(self.db); - let typed_body = self.instance.key(self.db).instantiate_typed_body(self.db); - if typed_body.result_ty().as_borrow(self.db).is_none() || owner.body(self.db).is_none() { + fn borrow_summary(mut self) -> Result, SemanticBorrowDiagnostic<'db>> { + let key = self.instance.key(self.db); + if !instance_returns_borrowing_value(self.db, self.instance) + || key.owner(self.db).body(self.db).is_none() + { return Ok(None); } self.compute_entry_states(); @@ -355,14 +485,7 @@ impl<'db> Borrowck<'db> { self.compute_moved_states()?; self.compute_liveness(); self.check_conflicts()?; - if self - .instance - .key(self.db) - .instantiate_typed_body(self.db) - .result_ty() - .as_borrow(self.db) - .is_some() - { + if instance_returns_borrowing_value(self.db, self.instance) { let _ = self.compute_return_summary()?; } Ok(()) @@ -409,68 +532,161 @@ impl<'db> Borrowck<'db> { live } - fn init_loans(&mut self) { + fn init_loans(&mut self) -> Result<(), SemanticBorrowDiagnostic<'db>> { for local_id in 0..self.body.locals.len() { let local_id = crate::analysis::semantic::SLocalId::from_u32(local_id as u32); let Some(local) = self.body.local(local_id) else { continue; }; - if let Some((kind, _)) = local.ty.as_borrow(self.db) - && let Some(¶m_idx) = self.param_index_of_local.get(&local_id) - && !matches!( - local.lowering, - NormalizedBindingLowering::CarrierLocal { .. } - ) + let local_ty = local.ty; + let Some(¶m_idx) = self.param_index_of_local.get(&local_id) else { + continue; + }; + let shape = capability_shape(self.db, local_ty); + let direct_place = match self.instance.key(self.db).owner(self.db) { + BodyOwner::Func(func) => param_idx == 0 && func.receiver_ty(self.db).is_some(), + _ => false, + }; + let mut leaves = Vec::new(); + for slot in capability_slots(self.db, shape, false) { + let slot_path = slot.path.map_indices(|param| IndexExpr::LoanParam(*param)); + let mut loan = LoanDef::for_slot( + slot.kind, + &slot.path, + BorrowActivation::Immediate, + crate::analysis::semantic::SemOrigin::Body(self.body.template_owner), + ); + let root = if direct_place && slot.path.is_empty() { + RegionRoot::ParamPlace(param_idx) + } else { + RegionRoot::ParamCapability { + param: param_idx, + slot: slot_path.clone(), + } + }; + loan.extend( + RegionSet::singleton(SymbolicPlace::new(root, [])), + ParentSet::default(), + ); + let loan = self.allocate_loan(loan); + leaves.push((slot_path, LoanRef::for_slot(loan, &slot.path))); + } + if let Some(value) = slot_loan_value(&self.value_interner, shape, leaves) + && !self.value_interner.borrow().is_empty(value) { - let loan = LoanId(self.loans.len() as u32); - let mut targets = FxHashSet::default(); - targets.insert(CanonPlace { - root: BorrowRoot::Param(param_idx), - proj: NSProjectionPath::default(), - }); - self.loans.push(Loan { - kind, - targets, - parents: FxHashSet::default(), - origin: crate::analysis::semantic::SemOrigin::Body(self.body.template_owner), - }); - self.param_loan_for_local.insert(local_id, loan); + self.param_values_for_local.insert(local_id, value); } } - for block in &self.body.blocks { - for stmt in &block.stmts { - let NSStmtKind::Assign { dst, expr } = &stmt.kind else { + let stmts = self + .body + .blocks + .iter() + .flat_map(|block| block.stmts.iter().cloned()) + .collect::>(); + for stmt in stmts { + let NSStmtKind::Assign { dst, expr } = &stmt.kind else { + continue; + }; + let Some(result_ty) = self.body.local(*dst).map(|local| local.ty) else { + continue; + }; + if let NExpr::Call { callee, args, .. } = expr + && capability_shape(self.db, result_ty).contains_borrow(self.db) + { + let Some(summary) = self.call_borrow_summary(callee.key)? else { continue; }; - if self - .body - .local(*dst) - .is_some_and(|local| local.ty.as_borrow(self.db).is_some()) - && matches!( - expr, - NExpr::Borrow { .. } | NExpr::Call { .. } | NExpr::Use(_) - ) - { - let kind = self - .body - .local(*dst) - .and_then(|local| local.ty.as_borrow(self.db)) - .map(|(kind, _)| kind) - .expect("borrow local"); - let loan = LoanId(self.loans.len() as u32); - self.loan_for_local.insert(*dst, loan); - self.loans.push(Loan { - kind, - targets: FxHashSet::default(), - parents: FxHashSet::default(), - origin: stmt.origin, - }); + self.validate_call_borrow_summary(result_ty, args, &summary, stmt.origin)?; + for leaf in summary.leaves() { + let loan = self.allocate_loan(LoanDef::for_summary( + leaf.kind, + &leaf.path, + BorrowActivation::Immediate, + stmt.origin, + )); + self.call_loan_sources.insert(loan, leaf.sources.clone()); + if ty_is_borrow(self.db, result_ty).is_some() && leaf.path.is_empty() { + self.loan_for_local.insert(*dst, loan); + } else { + self.call_result_loans + .entry(stmt.id) + .or_default() + .push((leaf.path.clone(), loan)); + } } + continue; + } + + let direct_loan = match expr { + NExpr::Borrow { + kind, activation, .. + } => Some((*kind, *activation)), + NExpr::ReadPlace { .. } | NExpr::Use(_) => ty_is_borrow(self.db, result_ty) + .map(|(kind, _)| (kind, BorrowActivation::Immediate)), + _ => None, + }; + if let Some((kind, activation)) = direct_loan + && !matches!( + expr, + NExpr::ReadPlace { place, .. } + if self.read_place_copies_capability(place) + ) + { + let loan = self.allocate_loan(LoanDef::plain(kind, activation, stmt.origin)); + self.loan_for_local.insert(*dst, loan); + } + } + Ok(()) + } + + fn read_place_copies_capability(&self, place: &NSPlace<'db>) -> bool { + self.body + .place_root_ty(&place.root) + .and_then(|ty| semantic_projection_ty(self.db, ty, &place.path)) + .is_some_and(|(ty, _)| ty.as_capability(self.db).is_some()) + } + + fn allocate_loan(&mut self, loan: LoanDef<'db>) -> LoanId { + let id = LoanId(self.loans.len() as u32); + self.loans.push(loan); + id + } + + fn call_borrow_summary( + &self, + key: SemanticInstanceKey<'db>, + ) -> Result, SemanticBorrowDiagnostic<'db>> { + let instance = get_or_build_semantic_instance(self.db, key); + match self.summary_mode { + BorrowSummaryMode::FinalCheck | BorrowSummaryMode::FinalSummary => { + semantic_borrow_summary_voucher(self.db, instance) } + BorrowSummaryMode::Provisional => provisional_borrow_summary_voucher(self.db, instance), } } + fn validate_call_borrow_summary( + &self, + result_ty: crate::analysis::ty::ty_def::TyId<'db>, + args: &[NOperand], + summary: &BorrowSummary, + origin: SemOrigin<'db>, + ) -> Result<(), SemanticBorrowDiagnostic<'db>> { + let argument_tys = args + .iter() + .map(|arg| self.body.local(arg.local).map(|local| local.ty)) + .collect::>>() + .ok_or_else(|| { + self.internal_diag( + origin, + "callee borrow summary argument is missing".to_string(), + ) + })?; + validate_borrow_summary(self.db, result_ty, &argument_tys, summary) + .map_err(|message| self.internal_diag(origin, message)) + } + pub(super) fn compute_entry_states(&mut self) { self.entry_state = solve_forward_cfg(&mut BorrowEntryStateAnalysis::new(self)); } @@ -478,11 +694,12 @@ impl<'db> Borrowck<'db> { pub(super) fn compute_loan_targets(&mut self) -> Result<(), SemanticBorrowDiagnostic<'db>> { let mut analysis = BorrowLoanTargetAnalysis::new( self.db, - self.instance, &self.body, &self.entry_state, &self.loan_for_local, - self.summary_mode, + &self.constant_indices, + &self.call_result_loans, + &self.call_loan_sources, ); let mut state = BorrowLoanTargetState { loans: &mut self.loans, @@ -490,6 +707,24 @@ impl<'db> Borrowck<'db> { try_solve_sparse(&mut analysis, &mut state) } + pub(super) fn apply_stmt_state( + &self, + state: &mut BorrowState<'db>, + stmt: &super::ir::NSStmt<'db>, + ) { + BorrowTransferCx::new( + self.db, + &self.body, + &self.loan_for_local, + &self.constant_indices, + ) + .apply_stmt( + state, + stmt, + self.call_result_loans.get(&stmt.id).map(Vec::as_slice), + ); + } + fn compute_moved_states(&mut self) -> Result<(), SemanticBorrowDiagnostic<'db>> { self.moved_entry = try_solve_forward_cfg(&mut BorrowMovedStateAnalysis::new(self))? .iter() @@ -506,7 +741,7 @@ impl<'db> Borrowck<'db> { for (stmt_idx, stmt) in block.stmts.iter().enumerate() { self.check_stmt(&state, &moved, &self.live_before[bb_idx][stmt_idx], stmt)?; self.update_moved_for_stmt(&state, &mut moved, stmt)?; - self.canon().apply_stmt_state(&mut state, stmt); + self.apply_stmt_state(&mut state, stmt); } self.check_terminator( &state, @@ -520,105 +755,536 @@ impl<'db> Borrowck<'db> { fn check_stmt( &self, - state: &State, + state: &BorrowState<'db>, moved: &MovedPlaces<'db>, live: &FxHashSet, stmt: &super::ir::NSStmt<'db>, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { - let active = self.effective_loans(state, live); + let active = effective_loans(&self.canon(), &self.loans, state, live); match &stmt.kind { - NSStmtKind::Assign { dst, expr } => match expr { - NExpr::ReadPlace { place, mode } => { - let targets = self.canon().canonicalize_place(state, place, stmt.origin)?; - self.check_moved_overlap( - moved, - &targets, - stmt.origin, - "cannot use a value after it was moved", - )?; - if *mode == ReadMode::Move { - self.check_move_out(&active, place, &targets, stmt.origin)?; + NSStmtKind::Assign { dst, expr } => { + match expr { + NExpr::ReadPlace { place, mode } => { + self.check_place_read(state, moved, &active, place, *mode, stmt.origin)?; } - } - NExpr::Borrow { place, kind, .. } => { - let targets = self.canon().canonicalize_place(state, place, stmt.origin)?; - self.check_moved_overlap( - moved, - &targets, - stmt.origin, - "cannot borrow a moved value", - )?; - if let Some(conflict) = self.first_loan_conflict( - &active, - self.loan_for_local.get(dst).copied(), - *kind, - &targets, - ) { - return Err(self.borrow_conflict_diag( + NExpr::Borrow { place, kind, .. } => { + let targets = self.canon().resolve_place(state, place, stmt.origin)?; + let authorized = self.canon().authority_for_place(state, place); + self.check_moved_overlap( + moved, + &targets, + &authorized, stmt.origin, - self.overlapping_loans_msg(conflict, *kind), - conflict, - )); + "cannot borrow a moved value", + )?; + let loan = self.loan_for_local.get(dst).copied(); + if let Some(kind) = + loan.map_or(Some(*kind), |loan| self.loan_conflict_kind(loan)) + { + let reference = loan.map(LoanRef::new); + self.check_loan_conflict( + &active, + reference.as_ref(), + kind, + &targets, + stmt.origin, + )?; + } + } + NExpr::ExtractEnumField { + value, + variant, + field, + } => { + let targets = + self.extract_enum_field_move_region(state, *value, *variant, *field); + let authorized = + self.canon() + .authority_for_value_targets(state, value.local, &targets); + self.check_moved_overlap( + moved, + &targets, + &authorized, + stmt.origin, + "cannot use a value after it was moved", + )?; + if value.mode == ReadMode::Move { + self.check_move_targets_out( + &active, + &authorized, + &targets, + stmt.origin, + )?; + } else { + self.check_read_targets(&active, &authorized, &targets, stmt.origin)?; + } + } + _ => { + let expression_moved = + self.check_expr_operands(state, moved, &active, stmt.origin, expr)?; + let mut call_accesses = + self.check_call_argument_accesses(state, &active, stmt.origin, expr)?; + self.check_effect_place_accesses( + state, + &expression_moved, + &active, + stmt.origin, + expr, + &mut call_accesses, + )?; } } - NExpr::ExtractEnumField { - value, - variant, - field, - } => { - let targets = - self.extract_enum_field_move_targets(state, *value, *variant, *field); + if matches!(expr, NExpr::Call { .. } | NExpr::Use(_)) { + self.check_assigned_loan_conflicts(&active, stmt.id, *dst, stmt.origin)?; + } + self.check_assignment_write(state, &active, *dst, stmt.origin)?; + } + NSStmtKind::Store { dst, src } => { + self.check_operand( + state, + moved, + &active, + *src, + stmt.origin, + "cannot use a value after it was moved", + )?; + let targets = self.canon().resolve_place(state, dst, stmt.origin)?; + let mut authorized = self + .canon() + .mut_authority_for_place_targets(state, dst, &targets); + if src.mode == ReadMode::Move { + authorized.union( + self.canon() + .mut_authority_for_value_targets(state, src.local, &targets), + ); + } + self.check_moved_parent(moved, &targets, &authorized, stmt.origin)?; + self.check_write_targets(&active, &authorized, &targets, stmt.origin)?; + } + } + Ok(()) + } + + fn check_assignment_write( + &self, + state: &BorrowState<'db>, + active: &[ActiveLoan<'db>], + dst: crate::analysis::semantic::SLocalId, + origin: SemOrigin<'db>, + ) -> Result<(), SemanticBorrowDiagnostic<'db>> { + let Some(place) = self + .body + .local(dst) + .filter(|local| local.source.is_some_and(|binding| binding.is_mut())) + .and_then(|local| local.lowering.place()) + else { + return Ok(()); + }; + let targets = self.canon().resolve_place(state, place, origin)?; + self.check_write_targets(active, &AuthoritySet::default(), &targets, origin) + } + + fn check_place_read( + &self, + state: &BorrowState<'db>, + moved: &MovedPlaces<'db>, + active: &[ActiveLoan<'db>], + place: &NSPlace<'db>, + mode: ReadMode, + origin: SemOrigin<'db>, + ) -> Result<(), SemanticBorrowDiagnostic<'db>> { + let targets = self.canon().resolve_place(state, place, origin)?; + let authorized = self.canon().authority_for_place(state, place); + self.check_moved_overlap( + moved, + &targets, + &authorized, + origin, + "cannot use a value after it was moved", + )?; + if mode == ReadMode::Move { + self.check_move_out(active, &authorized, place, &targets, origin) + } else { + self.check_read_targets(active, &authorized, &targets, origin) + } + } + + fn check_effect_place_accesses( + &self, + state: &BorrowState<'db>, + moved: &MovedPlaces<'db>, + active: &[ActiveLoan<'db>], + origin: SemOrigin<'db>, + expr: &NExpr<'db>, + accesses: &mut Vec>, + ) -> Result<(), SemanticBorrowDiagnostic<'db>> { + let NExpr::Call { + args, effect_args, .. + } = expr + else { + return Ok(()); + }; + for (idx, effect_arg) in effect_args.iter().enumerate() { + let group = args.len() + idx; + let (targets, authorized, arg_origin) = match &effect_arg.arg { + NEffectArgValue::Place(place) => { + let targets = self.canon().resolve_place(state, place, origin)?; + let authorized = if effect_arg.required_mut { + self.canon().mut_authority_for_place(state, place) + } else { + self.canon().authority_for_place(state, place) + }; self.check_moved_overlap( moved, &targets, - stmt.origin, + &authorized, + origin, "cannot use a value after it was moved", )?; - if value.mode == ReadMode::Move { - self.check_move_targets_out(&active, &targets, stmt.origin)?; + (targets, authorized, origin) + } + NEffectArgValue::Value(value) => { + let targets = self.canon().value_region(state, value.local); + let authorized = if effect_arg.required_mut { + self.canon() + .mut_authority_for_value_targets(state, value.local, &targets) + } else { + self.canon() + .authority_for_value_targets(state, value.local, &targets) + }; + (targets, authorized, operand_origin(*value, origin)) + } + }; + if effect_arg.required_mut { + self.check_write_targets(active, &authorized, &targets, arg_origin)?; + self.record_call_access( + accesses, + group, + None, + BorrowKind::Mut, + targets, + arg_origin, + )?; + } else { + self.check_read_targets(active, &authorized, &targets, arg_origin)?; + self.record_call_access( + accesses, + group, + None, + BorrowKind::Ref, + targets, + arg_origin, + )?; + } + + let target_ty = effect_arg.target_ty.or_else(|| match &effect_arg.arg { + NEffectArgValue::Value(value) => self.body.local(value.local).map(|local| local.ty), + NEffectArgValue::Place(_) => None, + }); + let Some(target_ty) = target_ty else { + continue; + }; + let shape = capability_shape(self.db, target_ty); + for slot in capability_slots(self.db, shape, false) { + let projection = layout_path_for_slot_template(&slot.path); + let targets = match &effect_arg.arg { + NEffectArgValue::Place(place) => { + self.canon() + .place_layout_region(state, place, target_ty, &projection) + } + NEffectArgValue::Value(value) => { + self.canon() + .value_layout_region(state, value.local, &projection) + } + }; + let authorized = match &effect_arg.arg { + NEffectArgValue::Place(place) if slot.kind == BorrowKind::Mut => self + .canon() + .mut_authority_for_place_targets(state, place, &targets), + NEffectArgValue::Place(place) => self + .canon() + .authority_for_place_targets(state, place, &targets), + NEffectArgValue::Value(value) if slot.kind == BorrowKind::Mut => self + .canon() + .mut_authority_for_value_targets(state, value.local, &targets), + NEffectArgValue::Value(value) => { + self.canon() + .authority_for_value_targets(state, value.local, &targets) } + }; + if slot.kind == BorrowKind::Mut { + self.check_write_targets(active, &authorized, &targets, arg_origin)?; + } else { + self.check_read_targets(active, &authorized, &targets, arg_origin)?; } - _ => self.check_expr_operands(state, moved, stmt.origin, expr)?, - }, - NSStmtKind::Store { dst, .. } => { - let targets = self.canon().canonicalize_place(state, dst, stmt.origin)?; - self.check_moved_parent(moved, &targets, stmt.origin)?; + self.record_call_access( + accesses, + group, + Some(&slot.path), + slot.kind, + targets, + arg_origin, + )?; } } Ok(()) } + fn check_call_argument_accesses( + &self, + state: &BorrowState<'db>, + active: &[ActiveLoan<'db>], + origin: SemOrigin<'db>, + expr: &NExpr<'db>, + ) -> Result>, SemanticBorrowDiagnostic<'db>> { + let NExpr::Call { callee, args, .. } = expr else { + return Ok(Vec::new()); + }; + let instance = get_or_build_semantic_instance(self.db, callee.key); + let BodyOwner::Func(func) = callee.key.owner(self.db) else { + return Ok(Vec::new()); + }; + let mut accesses = Vec::with_capacity(args.len()); + for (idx, arg) in args.iter().copied().enumerate() { + let Some(param) = func.params(self.db).nth(idx) else { + return Err( + self.internal_diag(origin, format!("callee is missing value parameter {idx}")) + ); + }; + let ty = instance.normalized_ty(self.db, param.ty(self.db)); + let moves_value = + arg.mode == ReadMode::Move && self.local_has_runtime_move_semantics(arg.local); + let arg_origin = operand_origin(arg, origin); + let mutably_passed_by_place = + param.mode(self.db) != FuncParamMode::Own && param.is_mut(self.db); + if ty.as_borrow(self.db).is_none() + && (arg.mode != ReadMode::Copy || mutably_passed_by_place) + { + let kind = if mutably_passed_by_place || moves_value { + BorrowKind::Mut + } else { + BorrowKind::Ref + }; + let targets = self.canon().value_region(state, arg.local); + if kind == BorrowKind::Mut && !moves_value { + let authorized = self + .canon() + .mut_authority_for_value_targets(state, arg.local, &targets); + self.check_write_targets(active, &authorized, &targets, arg_origin)?; + } + self.record_call_access(&mut accesses, idx, None, kind, targets, arg_origin)?; + } + let shape = capability_shape(self.db, ty); + for slot in capability_slots(self.db, shape, false) { + let projection = layout_path_for_slot_template(&slot.path); + let targets = self + .canon() + .value_layout_region(state, arg.local, &projection); + let authorized = if slot.kind == BorrowKind::Mut { + self.canon() + .mut_authority_for_value_targets(state, arg.local, &targets) + } else { + self.canon() + .authority_for_value_targets(state, arg.local, &targets) + }; + if slot.kind == BorrowKind::Mut { + self.check_write_targets(active, &authorized, &targets, arg_origin)?; + } else { + self.check_read_targets(active, &authorized, &targets, arg_origin)?; + } + self.record_call_access( + &mut accesses, + idx, + Some(&slot.path), + slot.kind, + targets, + arg_origin, + )?; + } + } + Ok(accesses) + } + + fn record_call_access( + &self, + accesses: &mut Vec>, + group: usize, + projection: Option<&SlotPath>, + kind: BorrowKind, + targets: RegionSet<'db>, + origin: SemOrigin<'db>, + ) -> Result<(), SemanticBorrowDiagnostic<'db>> { + // An argument owns its container and the capability slots stored inside + // it, but distinct capability slots must still obey aliasing rules. + let conflict = accesses + .iter() + .find(|access| access.conflicts_with(group, projection, kind, &targets)); + if let Some(conflict) = conflict { + let mut diag = SemanticBorrowDiagnostic::new( + self.instance, + SemanticBorrowDiagKind::BorrowConflict, + "call arguments require conflicting access to the same place".to_string(), + SemanticBorrowDiagnosticSpan::Origin { + owner: self.instance.key(self.db).owner(self.db), + origin, + }, + ); + self.push_secondary_origin( + &mut diag, + conflict.origin(), + "overlapping argument access occurs here".to_string(), + ); + return Err(diag); + } + if !targets.is_empty() { + accesses.push(CallAccess::new( + group, + projection.cloned(), + kind, + targets, + origin, + )); + } + Ok(()) + } + + fn check_assigned_loan_conflicts( + &self, + active: &[ActiveLoan<'db>], + stmt: SStmtId, + local: crate::analysis::semantic::SLocalId, + origin: crate::analysis::semantic::SemOrigin<'db>, + ) -> Result<(), SemanticBorrowDiagnostic<'db>> { + let mut references = self + .loan_for_local + .get(&local) + .copied() + .map(LoanRef::new) + .into_iter() + .collect::>(); + references.extend( + self.call_result_loans + .get(&stmt) + .into_iter() + .flatten() + .map(|(path, loan)| LoanRef::for_summary(*loan, path)), + ); + for reference in references { + let loan = &self.loans[reference.id.0 as usize]; + let targets = self + .canon() + .active_region_for_held(&reference, &Guard::always()); + self.check_loan_conflict(active, Some(&reference), loan.kind(), &targets, origin)?; + } + Ok(()) + } + + fn check_loan_conflict( + &self, + active: &[ActiveLoan<'db>], + new_loan: Option<&LoanRef>, + kind: BorrowKind, + targets: &RegionSet<'db>, + origin: crate::analysis::semantic::SemOrigin<'db>, + ) -> Result<(), SemanticBorrowDiagnostic<'db>> { + if let Some(conflict) = self.first_loan_conflict(active, new_loan, kind, targets) { + return Err(self.borrow_conflict_diag( + origin, + self.overlapping_loans_msg(conflict, kind), + conflict, + )); + } + Ok(()) + } + + fn check_read_targets( + &self, + active: &[ActiveLoan<'db>], + authorized: &AuthoritySet, + targets: &RegionSet<'db>, + origin: SemOrigin<'db>, + ) -> Result<(), SemanticBorrowDiagnostic<'db>> { + let conflict = active.iter().find(|loan| { + !authorized.matches(loan.reference(), loan.holder_guard()) + && self.loan_conflict_kind(loan.id()) == Some(BorrowKind::Mut) + && loan.overlaps(targets) + }); + if let Some(conflict) = conflict { + return Err(self.borrow_conflict_diag( + origin, + "cannot read this place while a mutable borrow is active".to_string(), + conflict.id(), + )); + } + Ok(()) + } + + fn check_write_targets( + &self, + active: &[ActiveLoan<'db>], + authorized: &AuthoritySet, + targets: &RegionSet<'db>, + origin: SemOrigin<'db>, + ) -> Result<(), SemanticBorrowDiagnostic<'db>> { + let conflict = active.iter().find(|loan| { + !authorized.matches(loan.reference(), loan.holder_guard()) + && self.loan_conflict_kind(loan.id()).is_some() + && loan.overlaps(targets) + }); + if let Some(conflict) = conflict { + return Err(self.borrow_conflict_diag( + origin, + "cannot write to this place while it is borrowed".to_string(), + conflict.id(), + )); + } + Ok(()) + } + fn check_terminator( &self, - state: &State, + state: &BorrowState<'db>, moved: &MovedPlaces<'db>, live: &FxHashSet, term: &super::ir::NSTerminator<'db>, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { match &term.kind { NSTerminatorKind::Goto(_) | NSTerminatorKind::Assert { .. } => {} - NSTerminatorKind::Branch { cond, .. } - | NSTerminatorKind::MatchEnum { value: cond, .. } - | NSTerminatorKind::Return(Some(cond)) => { - let _ = live; + NSTerminatorKind::Branch { cond, .. } | NSTerminatorKind::Return(Some(cond)) => { + let active = effective_loans(&self.canon(), &self.loans, state, live); self.check_operand( state, moved, + &active, *cond, term.origin, "cannot use a value after it was moved", )?; } + NSTerminatorKind::MatchEnum { value, .. } => { + let active = effective_loans(&self.canon(), &self.loans, state, live); + self.check_operand( + state, + moved, + &active, + NOperand { + mode: ReadMode::Read, + ..*value + }, + term.origin, + "cannot use a value after it was moved", + )?; + } NSTerminatorKind::Return(None) => {} } if let NSTerminatorKind::Return(Some(value)) = term.kind && self .body .local(value.local) - .is_some_and(|local| local.ty.as_borrow(self.db).is_some()) + .is_some_and(|local| ty_is_borrow(self.db, local.ty).is_some()) && self .canon() - .borrow_local_targets(state, value.local) + .borrow_local_region(state, value.local) .is_empty() { return Err(self.internal_diag( @@ -626,109 +1292,278 @@ impl<'db> Borrowck<'db> { "borrow return local has no tracked loan targets".to_string(), )); } + if let NSTerminatorKind::Return(Some(value)) = term.kind + && self + .body + .local(value.local) + .is_some_and(|local| ty_is_borrow(self.db, local.ty).is_none()) + { + for loan in active_loans_in(&self.canon(), state, value.local) { + let region = self.resolve_return_region(state, loan.region(), term.origin)?; + if let Some(local) = region.guarded_places().find_map(|(_, target)| { + if let RegionRoot::Local(local) = target.root() { + Some(*local) + } else { + None + } + }) { + let name = self.pretty_local_name(local); + let mut diag = self.invalid_return_diag( + term.origin, + format!("cannot return a value that holds a borrow of local `{name}`"), + ); + self.push_secondary_origin( + &mut diag, + self.loan_origin(loan.id()), + "borrow created here".to_string(), + ); + return Err(diag); + } + } + } Ok(()) } - fn compute_return_summary(&self) -> Result, SemanticBorrowDiagnostic<'db>> { - let mut out = Vec::new(); + fn resolve_return_region( + &self, + state: &BorrowState<'db>, + region: &RegionSet<'db>, + origin: SemOrigin<'db>, + ) -> Result, SemanticBorrowDiagnostic<'db>> { + let mut pending = region.clauses().collect::>(); + let mut seen = FxHashSet::default(); + let mut resolved = RegionSet::empty(); + while let Some(target) = pending.pop_front() { + if !seen.insert(target.clone()) { + continue; + } + let (guard, place) = target + .guarded_places() + .next() + .expect("a split region contains one clause"); + let RegionRoot::Local(local) = place.root() else { + resolved = resolved.union(&target); + continue; + }; + let Some(local) = self.body.local(*local) else { + resolved = resolved.union(&target); + continue; + }; + // Snapshot provenance describes a value's physical source. Layout + // backing is deliberately not used here: it can point at the + // argument that supplied a fresh aggregate field's layout without + // making that freshly allocated field an alias of the argument. + let Some(source) = local.snapshot_source_place() else { + resolved = resolved.union(&target); + continue; + }; + let source = self + .canon() + .resolve_place(state, source, origin)? + .project(place.projection()) + .with_guard(guard); + let mut advanced = false; + for source in source.clauses() { + if source == target { + continue; + } + advanced = true; + pending.push_back(source); + } + if !advanced { + resolved = resolved.union(&target); + } + } + Ok(resolved) + } + + fn compute_return_summary(&self) -> Result> { + let mut out = BTreeMap::<(BorrowKind, SummaryPath), Vec>::new(); + let mut families = BTreeMap::new(); for (bb_idx, block) in self.body.blocks.iter().enumerate() { let NSTerminatorKind::Return(Some(value)) = block.terminator.kind else { continue; }; let mut state = self.entry_state[SBlockId::new(bb_idx)].clone(); for stmt in &block.stmts { - self.canon().apply_stmt_state(&mut state, stmt); + self.apply_stmt_state(&mut state, stmt); } - for target in self.canon().borrow_local_targets(&state, value.local) { - for proj in target.proj.iter() { - if matches!(proj, Projection::Index(IndexSource::Dynamic(_))) { - return Err(self.invalid_return_diag( - block.terminator.origin, - "return borrows with dynamic indices are not supported".to_string(), - )); + let origin = block.terminator.origin; + for leaf in state.leaves_in(value.local, super::guard::ValueScope::Summary) { + let kind = self.loans[leaf.payload.id.0 as usize].kind(); + let (path, subst) = summary_path_for_leaf(&leaf.path, &mut families); + let held = leaf.payload.substitute(&subst); + let Some(guard) = leaf.payload_guard.substitute(&subst) else { + continue; + }; + let region = self.canon().active_region_for_held(&held, &guard); + if region.is_empty() { + if self.summary_mode != BorrowSummaryMode::FinalCheck { + out.entry((kind, path)).or_default(); + continue; } + return Err(self.internal_diag( + origin, + format!("borrow result slot {:?} has no tracked source", path), + )); } - match &target.root { - BorrowRoot::Param(idx) => { - let transform = BorrowTransform { - input: BorrowInputRef::Param(*idx), - proj: target.proj.clone(), - }; - if !out.contains(&transform) { - out.push(transform); + let region = self.resolve_return_region(&state, ®ion, origin)?; + for (source_guard, target) in region.guarded_places() { + let (source_guard, target) = + self.normalize_summary_source(source_guard, target, origin)?; + match target.root() { + RegionRoot::ParamPlace(idx) => { + let source_path = + summary_path_for_region_projection(target.projection()); + out.entry((kind, path.clone())) + .or_default() + .push(BorrowSourceClause { + guard: source_guard.clone(), + source: BorrowSource::ParamPlace { + param: *idx, + path: source_path, + }, + }); + } + RegionRoot::ParamCapability { param, slot } => { + out.entry((kind, path.clone())) + .or_default() + .push(BorrowSourceClause { + guard: source_guard.clone(), + source: BorrowSource::ParamCapability { + param: *param, + slot: summary_path_for_slot(slot), + }, + }); + } + RegionRoot::Provider(_) => { + return Err(self.invalid_return_diag( + origin, + "cannot return a borrow derived from an effect parameter" + .to_string(), + )); + } + RegionRoot::Local(local) => { + let name = self.pretty_local_name(*local); + return Err(self.invalid_return_diag( + origin, + format!("cannot return a borrow to local `{name}`"), + )); } - } - BorrowRoot::Provider(_) => { - return Err(self.invalid_return_diag( - block.terminator.origin, - "cannot return a borrow derived from an effect parameter".to_string(), - )); - } - BorrowRoot::Local(local) => { - let name = self.pretty_local_name(*local); - return Err(self.invalid_return_diag( - block.terminator.origin, - format!("cannot return a borrow to local `{name}`"), - )); } } } } - Ok(out) + Ok(BorrowSummary::new( + out.into_iter() + .map(|((kind, path), sources)| BorrowSummaryLeaf::new(kind, path, sources)) + .collect(), + )) } - fn effective_loans( + fn normalize_summary_source( &self, - state: &State, - live: &FxHashSet, - ) -> Vec { - let active = state - .local_loans + guard: &Guard, + place: &SymbolicPlace<'db>, + origin: SemOrigin<'db>, + ) -> Result<(Guard, SymbolicPlace<'db>), SemanticBorrowDiagnostic<'db>> { + let mut expressions = guard + .index_exprs() + .into_iter() + .chain(place.index_exprs()) + .collect::>(); + expressions.sort_unstable(); + expressions.dedup(); + let mut next_existential = expressions .iter() - .filter(|(local, _)| live.contains(local)) - .flat_map(|(_, loans)| loans.iter().copied()) - .collect::>(); - let mut suspended = FxHashSet::default(); - let mut worklist: Vec<_> = active.iter().copied().collect(); - while let Some(loan) = worklist.pop() { - for parent in &self.loans[loan.0 as usize].parents { - if suspended.insert(*parent) { - worklist.push(*parent); + .filter_map(|expr| match expr { + IndexExpr::Existential(id) => Some(id.0), + _ => None, + }) + .max() + .and_then(|id| id.checked_add(1)) + .unwrap_or(0); + let mut subst = IndexSubst::new(); + for expression in expressions { + match expression { + IndexExpr::Runtime(local) => { + let replacement = self.param_index_of_local.get(&local).copied().map_or_else( + || { + let existential = ExistentialId(next_existential); + next_existential = next_existential + .checked_add(1) + .expect("summary existential space exhausted"); + IndexExpr::Existential(existential) + }, + IndexExpr::InputParam, + ); + subst.insert(expression, replacement); + } + IndexExpr::ValueParam(_) | IndexExpr::LoanParam(_) => { + return Err(self.internal_diag( + origin, + "borrow summary contains an unbound internal index".to_string(), + )); } + IndexExpr::Const(_) + | IndexExpr::ResultParam(_) + | IndexExpr::InputParam(_) + | IndexExpr::Existential(_) => {} } } - let mut active: Vec<_> = active - .into_iter() - .filter(|loan| !suspended.contains(loan)) - .collect(); - active.sort_by_key(|loan| loan.0); - active + let guard = guard.substitute(&subst).ok_or_else(|| { + self.internal_diag( + origin, + "borrow summary source has contradictory index constraints".to_string(), + ) + })?; + Ok((guard, place.substitute(&subst))) } fn first_loan_conflict( &self, - active: &[LoanId], - new_loan: Option, + active: &[ActiveLoan<'db>], + new_loan: Option<&LoanRef>, new_kind: BorrowKind, - targets: &FxHashSet>, + targets: &RegionSet<'db>, ) -> Option { - let reborrow_parents = new_loan.map(|loan| &self.loans[loan.0 as usize].parents); + let reborrow_parents = new_loan.map(|reference| { + self.loans[reference.id.0 as usize].instantiate_parents(reference, &Guard::always()) + }); active .iter() - .copied() - .filter(|loan| reborrow_parents.is_none_or(|parents| !parents.contains(loan))) + .filter(|loan| { + reborrow_parents.as_ref().is_none_or(|parents| { + !parents + .iter() + .any(|parent| loan.matches(parent.reference(), parent.guard()).is_some()) + }) + }) .find(|loan| { - let loan = &self.loans[loan.0 as usize]; - !matches!((loan.kind, new_kind), (BorrowKind::Ref, BorrowKind::Ref)) - && place_set_overlaps(&loan.targets, targets) + self.loan_conflict_kind(loan.id()).is_some_and(|kind| { + !matches!((kind, new_kind), (BorrowKind::Ref, BorrowKind::Ref)) + }) && loan.overlaps(targets) }) + .map(ActiveLoan::id) + } + + fn loan_conflict_kind(&self, loan: LoanId) -> Option { + let loan = &self.loans[loan.0 as usize]; + // Receiver reservations remain dormant while later arguments are + // evaluated. The call-access checks perform their activation. + if loan.activation() == BorrowActivation::AtCall { + None + } else { + Some(loan.kind()) + } } fn check_move_out( &self, - active: &[LoanId], + active: &[ActiveLoan<'db>], + authorized: &AuthoritySet, place: &NSPlace<'db>, - targets: &FxHashSet>, + targets: &RegionSet<'db>, origin: crate::analysis::semantic::SemOrigin<'db>, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { if let NSPlaceRoot::CarrierDerefLocal(local) = place.root { @@ -754,21 +1589,22 @@ impl<'db> Borrowck<'db> { "cannot move out through a borrow handle".to_string(), )); } - self.check_move_targets_out(active, targets, origin)?; + self.check_move_targets_out(active, authorized, targets, origin)?; Ok(()) } fn check_move_targets_out( &self, - active: &[LoanId], - targets: &FxHashSet>, + active: &[ActiveLoan<'db>], + authorized: &AuthoritySet, + targets: &RegionSet<'db>, origin: crate::analysis::semantic::SemOrigin<'db>, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { - for target in targets { - if let BorrowRoot::Param(idx) = target.root + for (_, target) in targets.guarded_places() { + if let RegionRoot::ParamPlace(idx) = target.root() && self .param_modes - .get(idx as usize) + .get(*idx as usize) .copied() .is_some_and(|mode| mode == FuncParamMode::View) { @@ -778,15 +1614,13 @@ impl<'db> Borrowck<'db> { )); } } - if let Some(loan) = active - .iter() - .copied() - .find(|loan| place_set_overlaps(&self.loans[loan.0 as usize].targets, targets)) - { + if let Some(loan) = active.iter().find(|loan| { + !authorized.matches(loan.reference(), loan.holder_guard()) && loan.overlaps(targets) + }) { return Err(self.borrow_conflict_diag( origin, "cannot move out of a value while it is borrowed".to_string(), - loan, + loan.id(), )); } Ok(()) @@ -794,7 +1628,7 @@ impl<'db> Borrowck<'db> { pub(super) fn update_moved_for_stmt( &self, - state: &State, + state: &BorrowState<'db>, moved: &mut MovedPlaces<'db>, stmt: &super::ir::NSStmt<'db>, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { @@ -802,9 +1636,9 @@ impl<'db> Borrowck<'db> { NSStmtKind::Assign { dst, expr } => { if let Some(root) = self .local_root(*dst) - .and_then(|root| self.canon().root_to_borrow_root(root)) + .and_then(|root| self.canon().root_to_region_root(root)) { - moved.retain(|place, _| place.root != root); + moved.retain(|region, _| !region.has_root(&root)); } if let NExpr::ReadPlace { place, @@ -815,9 +1649,11 @@ impl<'db> Borrowck<'db> { origin: stmt.origin, note: "value is moved here".to_string(), }; - for place in self.canon().canonicalize_place(state, place, stmt.origin)? { - moved.insert(place, site.clone()); - } + self.record_move_region( + moved, + self.canon().resolve_place(state, place, stmt.origin)?, + site, + ); } if let NExpr::ExtractEnumField { value, @@ -827,90 +1663,88 @@ impl<'db> Borrowck<'db> { { if value.mode == ReadMode::Move { let site = self.move_site(*value, operand_origin(*value, stmt.origin)); - for place in - self.extract_enum_field_move_targets(state, *value, *variant, *field) - { - moved.insert(place, site.clone()); - } + self.record_move_region( + moved, + self.extract_enum_field_move_region(state, *value, *variant, *field), + site, + ); } } else { self.record_expr_moves(state, moved, stmt.origin, expr)?; } } - NSStmtKind::Store { dst, .. } => { - let written = self.canon().canonicalize_place(state, dst, stmt.origin)?; - moved.retain(|place, _| { - !written.iter().any(|written| { - written.root == place.root && written.proj.is_prefix_of(&place.proj) - }) - }); + NSStmtKind::Store { dst, src } => { + self.record_operand_move(state, moved, *src, stmt.origin)?; + let written = self.canon().resolve_place(state, dst, stmt.origin)?; + moved.retain(|region, _| !written.provably_covers(region)); } } Ok(()) } - fn extract_enum_field_move_targets( + fn extract_enum_field_move_region( &self, - state: &State, + state: &BorrowState<'db>, source: NOperand, variant: crate::analysis::semantic::VariantIndex, field: crate::analysis::semantic::FieldIndex, - ) -> FxHashSet> { - let Some(source_local) = self.body.local(source.local) else { - return FxHashSet::default(); - }; - let projection = Projection::VariantField { - variant, - enum_ty: source_local.ty, - field_idx: field.0 as usize, - }; + ) -> RegionSet<'db> { self.canon() - .canonicalize_value_base(state, source.local) - .into_iter() - .map(|mut target| { - target.proj.push(projection.clone()); - target - }) - .collect() + .value_region(state, source.local) + .project(&[super::region::RegionProjection::VariantField { variant, field }]) } fn check_expr_operands( &self, - state: &State, + state: &BorrowState<'db>, moved: &MovedPlaces<'db>, + active: &[ActiveLoan<'db>], origin: crate::analysis::semantic::SemOrigin<'db>, expr: &NExpr<'db>, - ) -> Result<(), SemanticBorrowDiagnostic<'db>> { + ) -> Result, SemanticBorrowDiagnostic<'db>> { + let mut moved = moved.clone(); expr.try_for_each_value_operand(|value| { self.check_operand( state, - moved, + &moved, + active, value, origin, "cannot use a value after it was moved", - ) - }) + )?; + self.record_operand_move(state, &mut moved, value, origin) + })?; + Ok(moved) } fn check_operand( &self, - state: &State, + state: &BorrowState<'db>, moved: &MovedPlaces<'db>, + active: &[ActiveLoan<'db>], operand: NOperand, origin: crate::analysis::semantic::SemOrigin<'db>, message: &str, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { let origin = operand_origin(operand, origin); - let targets = self.canon().canonicalize_value_base(state, operand.local); + let targets = self.canon().value_region(state, operand.local); if targets.is_empty() { return Ok(()); } - self.check_moved_overlap(moved, &targets, origin, message) + let authorized = self + .canon() + .authority_for_value_targets(state, operand.local, &targets); + self.check_moved_overlap(moved, &targets, &authorized, origin, message)?; + if operand.mode == ReadMode::Move && self.local_has_runtime_move_semantics(operand.local) { + self.check_move_targets_out(active, &authorized, &targets, origin) + } else { + self.check_read_targets(active, &authorized, &targets, origin) + } } fn record_expr_moves( &self, - state: &State, + state: &BorrowState<'db>, moved: &mut MovedPlaces<'db>, origin: crate::analysis::semantic::SemOrigin<'db>, expr: &NExpr<'db>, @@ -922,7 +1756,7 @@ impl<'db> Borrowck<'db> { fn record_operand_move( &self, - state: &State, + state: &BorrowState<'db>, moved: &mut MovedPlaces<'db>, operand: NOperand, origin: crate::analysis::semantic::SemOrigin<'db>, @@ -930,9 +1764,7 @@ impl<'db> Borrowck<'db> { let origin = operand_origin(operand, origin); if operand.mode == ReadMode::Move && self.local_has_runtime_move_semantics(operand.local) { let site = self.move_site(operand, origin); - for place in self.canon().canonicalize_value_base(state, operand.local) { - moved.insert(place, site.clone()); - } + self.record_move_region(moved, self.canon().value_region(state, operand.local), site); } Ok(()) } @@ -966,14 +1798,16 @@ impl<'db> Borrowck<'db> { fn check_moved_overlap( &self, moved: &MovedPlaces<'db>, - accessed: &FxHashSet>, + accessed: &RegionSet<'db>, + authorized: &AuthoritySet, origin: crate::analysis::semantic::SemOrigin<'db>, message: &str, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { if let Some((_, site)) = moved.iter().find(|(moved, _)| { - accessed - .iter() - .any(|accessed| places_overlap(moved, accessed)) + accessed.clauses().any(|accessed| { + moved.may_overlap(&accessed).is_some() + && !self.loan_authorizes_access(authorized, &accessed) + }) }) { let mut diag = self.move_conflict_diag(origin, message.to_string()); self.push_secondary_origin(&mut diag, site.origin, site.note.clone()); @@ -985,14 +1819,15 @@ impl<'db> Borrowck<'db> { fn check_moved_parent( &self, moved: &MovedPlaces<'db>, - written: &FxHashSet>, + written: &RegionSet<'db>, + authorized: &AuthoritySet, origin: crate::analysis::semantic::SemOrigin<'db>, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { if let Some((_, site)) = moved.iter().find(|(moved, _)| { - written.iter().any(|written| { - written.root == moved.root - && moved.proj.is_prefix_of(&written.proj) - && moved.proj != written.proj + written.clauses().any(|written| { + moved.provably_covers(&written) + && !written.provably_covers(moved) + && !self.loan_authorizes_access(authorized, &written) }) }) { let mut diag = @@ -1003,6 +1838,26 @@ impl<'db> Borrowck<'db> { Ok(()) } + fn loan_authorizes_access(&self, authorized: &AuthoritySet, accessed: &RegionSet<'db>) -> bool { + authorized.iter().any(|authority| { + self.loans[authority.reference().id.0 as usize] + .instantiate(authority.reference()) + .with_guard(authority.guard()) + .provably_covers(accessed) + }) + } + + fn record_move_region( + &self, + moved: &mut MovedPlaces<'db>, + region: RegionSet<'db>, + site: MoveSite<'db>, + ) { + for clause in region.clauses() { + moved.insert(clause, site.clone()); + } + } + fn local_root(&self, local: crate::analysis::semantic::SLocalId) -> Option { self.body.local(local)?.lowering.root() } @@ -1057,12 +1912,27 @@ impl<'db> Borrowck<'db> { let mut diag = self.diag(SemanticBorrowDiagKind::BorrowConflict, origin, message); self.push_secondary_origin( &mut diag, - self.loans[loan.0 as usize].origin, + self.loan_origin(loan), "borrow created here".to_string(), ); diag } + fn loan_origin(&self, mut loan: LoanId) -> SemOrigin<'db> { + let mut seen = FxHashSet::default(); + while seen.insert(loan) { + let data = &self.loans[loan.0 as usize]; + let Some(parent) = data.parents().iter().next() else { + return data.origin(); + }; + if data.parents().iter().nth(1).is_some() { + return data.origin(); + } + loan = parent.reference().id; + } + self.loans[loan.0 as usize].origin() + } + fn move_conflict_diag( &self, origin: crate::analysis::semantic::SemOrigin<'db>, @@ -1120,7 +1990,11 @@ impl<'db> Borrowck<'db> { } fn overlapping_loans_msg(&self, loan: LoanId, new_kind: BorrowKind) -> String { - match (new_kind, self.loans[loan.0 as usize].kind) { + match ( + new_kind, + self.loan_conflict_kind(loan) + .expect("dormant loans do not produce conflicts"), + ) { (BorrowKind::Mut, BorrowKind::Mut) => { "cannot mutably borrow this place while a mut borrow is active".to_string() } @@ -1147,16 +2021,204 @@ fn semantic_borrow_summary_cycle_initial<'db>( instance: SemanticInstance<'db>, ) -> SemanticBorrowSummaryResult<'db> { SemanticBorrowSummaryResult::Ok( - instance_returns_borrow(db, instance).then(|| BorrowSummaryId::new(db, Vec::new())), + instance_returns_borrowing_value(db, instance) + .then(|| BorrowSummaryId::new(db, empty_signature_borrow_summary(db, instance))), ) } -fn instance_returns_borrow<'db>( +fn instance_returns_borrowing_value<'db>( db: &'db dyn HirAnalysisDb, instance: SemanticInstance<'db>, ) -> bool { - let key = instance.key(db); - key.owner(db).body(db).is_some() && key.typed_body(db).result_ty().as_borrow(db).is_some() + !capability_slots( + db, + capability_shape(db, instance.normalized_result_ty(db)), + true, + ) + .is_empty() +} + +fn summary_path_for_leaf( + path: &SlotPath, + families: &mut BTreeMap, +) -> (SummaryPath, IndexSubst) { + let mut subst = IndexSubst::new(); + let projection = path + .as_slice() + .iter() + .map(|step| match step { + SlotProjection::Field(field) => SummaryProjection::Field(field.index()), + SlotProjection::VariantField { variant, field } => SummaryProjection::VariantField { + variant: *variant, + field: *field, + }, + SlotProjection::Index(IndexExpr::Const(index)) => { + SummaryProjection::Index(IndexExpr::Const(*index)) + } + SlotProjection::Index(index) => { + let next = ResultIndexId( + u32::try_from(families.len()).expect("borrow result family space exhausted"), + ); + let family = *families.entry(*index).or_insert(next); + subst.insert(*index, IndexExpr::ResultParam(family)); + SummaryProjection::Index(IndexExpr::ResultParam(family)) + } + }) + .collect::>(); + (SummaryPath::from_steps(projection), subst) +} + +fn summary_path_for_slot(path: &SlotPath) -> SummaryPath { + SummaryPath::from_steps(path.as_slice().iter().map(|projection| match projection { + SlotProjection::Field(field) => SummaryProjection::Field(field.index()), + SlotProjection::VariantField { variant, field } => SummaryProjection::VariantField { + variant: *variant, + field: *field, + }, + SlotProjection::Index(index) => SummaryProjection::Index(*index), + })) +} + +fn summary_path_for_slot_template( + path: &SlotPath, + mut index: impl FnMut(super::guard::IndexParamId) -> IndexExpr, +) -> SummaryPath { + SummaryPath::from_steps(path.as_slice().iter().map(|projection| match projection { + SlotProjection::Field(field) => SummaryProjection::Field(field.index()), + SlotProjection::VariantField { variant, field } => SummaryProjection::VariantField { + variant: *variant, + field: *field, + }, + SlotProjection::Index(param) => SummaryProjection::Index(index(*param)), + })) +} + +fn layout_path_for_slot_template(path: &SlotPath) -> Vec { + path.as_slice() + .iter() + .map(|projection| match projection { + SlotProjection::Field(field) => LayoutBackingProjection::Field(field.index()), + SlotProjection::VariantField { variant, field } => { + LayoutBackingProjection::VariantField { + variant: *variant, + field: *field, + } + } + SlotProjection::Index(_) => LayoutBackingProjection::Index(None), + }) + .collect() +} + +fn summary_path_for_region_projection(path: &[RegionProjection]) -> SummaryPath { + SummaryPath::from_steps(path.iter().map(|projection| match projection { + RegionProjection::Field(field) => SummaryProjection::Field(*field), + RegionProjection::VariantField { variant, field } => SummaryProjection::VariantField { + variant: *variant, + field: *field, + }, + RegionProjection::Index(index) => SummaryProjection::Index(*index), + })) +} + +fn empty_signature_borrow_summary<'db>( + db: &'db dyn HirAnalysisDb, + instance: SemanticInstance<'db>, +) -> BorrowSummary { + let shape = capability_shape(db, instance.normalized_result_ty(db)); + BorrowSummary::new( + capability_slots(db, shape, true) + .into_iter() + .map(|slot| { + BorrowSummaryLeaf::new( + slot.kind, + summary_path_for_slot_template(&slot.path, |param| { + IndexExpr::ResultParam(ResultIndexId(param.0)) + }), + Vec::new(), + ) + }) + .collect(), + ) +} + +fn conservative_signature_borrow_summary<'db>( + db: &'db dyn HirAnalysisDb, + instance: SemanticInstance<'db>, +) -> BorrowSummary { + let results = capability_slots( + db, + capability_shape(db, instance.normalized_result_ty(db)), + true, + ); + let inputs = match instance.key(db).owner(db) { + BodyOwner::Func(func) => func + .params(db) + .filter_map(|param| { + u32::try_from(param.index()).ok().map(|idx| { + let ty = instance.normalized_ty(db, param.ty(db)); + let slots = capability_slots(db, capability_shape(db, ty), false); + (idx, ty, param.is_mut(db), slots) + }) + }) + .collect::>(), + _ => Vec::new(), + }; + let mut next_existential = 0_u32; + let mut summary = Vec::new(); + for result in results { + let result_path = summary_path_for_slot_template(&result.path, |param| { + IndexExpr::ResultParam(ResultIndexId(param.0)) + }); + let mut sources = Vec::new(); + for (idx, ty, is_mut, input_results) in &inputs { + if signature_input_is_unresolved(db, *ty) { + sources.push(BorrowSourceClause { + guard: super::guard::Guard::always(), + source: BorrowSource::AnyAccessible { + param: *idx, + class: match result.kind { + BorrowKind::Ref => super::summary::AccessClass::Shared, + BorrowKind::Mut => super::summary::AccessClass::Mutable, + }, + }, + }); + continue; + } + if result.kind == BorrowKind::Ref || *is_mut { + sources.push(BorrowSourceClause { + guard: super::guard::Guard::always(), + source: BorrowSource::ParamPlace { + param: *idx, + path: SummaryPath::new(), + }, + }); + } + for input in input_results { + if result.kind == BorrowKind::Ref || input.kind == BorrowKind::Mut { + let slot = summary_path_for_slot_template(&input.path, |_| { + let existential = ExistentialId(next_existential); + next_existential = next_existential + .checked_add(1) + .expect("summary existential space exhausted"); + IndexExpr::Existential(existential) + }); + sources.push(BorrowSourceClause { + guard: super::guard::Guard::always(), + source: BorrowSource::ParamCapability { param: *idx, slot }, + }); + } + } + } + summary.push(BorrowSummaryLeaf::new(result.kind, result_path, sources)); + } + BorrowSummary::new(summary) +} + +fn signature_input_is_unresolved(db: &dyn HirAnalysisDb, input_ty: TyId<'_>) -> bool { + input_ty.has_param(db) + || input_ty.has_var(db) + || input_ty.has_projection(db) + || input_ty.has_invalid(db) } fn semantic_borrow_summary_cycle_recover<'db>( diff --git a/crates/hir/src/analysis/semantic/borrowck/guard.rs b/crates/hir/src/analysis/semantic/borrowck/guard.rs new file mode 100644 index 0000000000..3c2ccde4d9 --- /dev/null +++ b/crates/hir/src/analysis/semantic/borrowck/guard.rs @@ -0,0 +1,603 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use crate::analysis::semantic::{SLocalId, VariantIndex}; + +use super::shape::{SlotPath, SlotProjection}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct IndexParamId(pub u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ResultIndexId(pub u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ValueIndexId(pub u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ExistentialId(pub u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum IndexExpr { + Const(usize), + Runtime(SLocalId), + ValueParam(ValueIndexId), + LoanParam(IndexParamId), + ResultParam(ResultIndexId), + InputParam(u32), + Existential(ExistentialId), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum ValueScope { + Relative, + Local(SLocalId), + Argument(u32), + Summary, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ChoiceKey { + scope: ValueScope, + occurrence: SlotPath, +} + +impl ChoiceKey { + pub(crate) fn relative(occurrence: SlotPath) -> Self { + Self { + scope: ValueScope::Relative, + occurrence, + } + } + + fn substitute(&self, subst: &IndexSubst) -> Self { + Self { + scope: self.scope, + occurrence: substitute_slot_path(&self.occurrence, subst), + } + } + + pub(crate) fn scoped(&self, scope: ValueScope) -> Self { + Self { + scope: match self.scope { + ValueScope::Relative => scope, + scope => scope, + }, + occurrence: self.occurrence.clone(), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct IndexSubst { + entries: BTreeMap, +} + +impl IndexSubst { + pub(crate) fn new() -> Self { + Self::default() + } + + pub(crate) fn from_pair(from: IndexExpr, to: IndexExpr) -> Self { + let mut subst = Self::new(); + subst.insert(from, to); + subst + } + + pub(crate) fn insert(&mut self, from: IndexExpr, to: IndexExpr) { + if from == to { + self.entries.remove(&from); + } else { + self.entries.insert(from, to); + } + } + + pub(crate) fn apply(&self, expr: IndexExpr) -> IndexExpr { + let mut current = expr; + let mut seen = BTreeSet::new(); + while let Some(next) = self.entries.get(¤t).copied() { + if !seen.insert(current) { + return seen.into_iter().min().unwrap_or(current); + } + current = next; + } + current + } + + pub(crate) fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} + +#[derive(Clone, Debug, Default)] +struct IndexAtoms { + equalities: Vec<(IndexExpr, IndexExpr)>, + disequalities: Vec<(IndexExpr, IndexExpr)>, + bounds: Vec<(IndexExpr, usize)>, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct IndexConstraints { + classes: Box<[Box<[IndexExpr]>]>, + disequalities: Box<[(IndexExpr, IndexExpr)]>, + bounds: Box<[(IndexExpr, usize)]>, +} + +impl IndexConstraints { + fn from_atoms(atoms: IndexAtoms) -> Option { + let mut terms = BTreeSet::new(); + for (lhs, rhs) in atoms.equalities.iter().chain(atoms.disequalities.iter()) { + terms.insert(*lhs); + terms.insert(*rhs); + } + terms.extend(atoms.bounds.iter().map(|(expr, _)| *expr)); + let terms = terms.into_iter().collect::>(); + let indices = terms + .iter() + .copied() + .enumerate() + .map(|(idx, expr)| (expr, idx)) + .collect::>(); + let mut parents = (0..terms.len()).collect::>(); + + for (lhs, rhs) in &atoms.equalities { + let lhs = find_root(&mut parents, indices[lhs]); + let rhs = find_root(&mut parents, indices[rhs]); + if lhs != rhs { + let (root, child) = if lhs < rhs { (lhs, rhs) } else { (rhs, lhs) }; + parents[child] = root; + } + } + + let mut grouped = BTreeMap::>::new(); + for (idx, expr) in terms.iter().copied().enumerate() { + let root = find_root(&mut parents, idx); + grouped.entry(root).or_default().push(expr); + } + let mut representative = BTreeMap::new(); + let mut classes = Vec::new(); + for mut members in grouped.into_values() { + members.sort_unstable(); + let constants = members + .iter() + .filter_map(|expr| match expr { + IndexExpr::Const(value) => Some(*value), + _ => None, + }) + .collect::>(); + if constants.len() > 1 { + return None; + } + let key = constants + .first() + .copied() + .map(IndexExpr::Const) + .unwrap_or(members[0]); + for member in &members { + representative.insert(*member, key); + } + if members.len() > 1 { + classes.push(members.into_boxed_slice()); + } + } + classes.sort_unstable_by_key(|class| class_key(class)); + + let mut disequalities = BTreeSet::new(); + for (lhs, rhs) in atoms.disequalities { + let lhs = representative.get(&lhs).copied().unwrap_or(lhs); + let rhs = representative.get(&rhs).copied().unwrap_or(rhs); + if lhs == rhs { + return None; + } + if matches!((lhs, rhs), (IndexExpr::Const(_), IndexExpr::Const(_))) { + continue; + } + disequalities.insert(ordered_pair(lhs, rhs)); + } + + let mut bounds = BTreeMap::::new(); + for (expr, len) in atoms.bounds { + let expr = representative.get(&expr).copied().unwrap_or(expr); + if let IndexExpr::Const(value) = expr { + if value >= len { + return None; + } + continue; + } + bounds + .entry(expr) + .and_modify(|bound| *bound = (*bound).min(len)) + .or_insert(len); + } + + if bounds.iter().any(|(expr, len)| { + disequalities + .iter() + .filter_map(|(lhs, rhs)| match (*lhs, *rhs) { + (candidate, IndexExpr::Const(value)) | (IndexExpr::Const(value), candidate) + if candidate == *expr && value < *len => + { + Some(value) + } + _ => None, + }) + .count() + >= *len + }) { + return None; + } + + Some(Self { + classes: classes.into_boxed_slice(), + disequalities: disequalities.into_iter().collect(), + bounds: bounds.into_iter().collect(), + }) + } + + fn atoms(&self) -> IndexAtoms { + let equalities = + self.classes + .iter() + .flat_map(|class| { + class.first().copied().into_iter().flat_map(|first| { + class.iter().skip(1).copied().map(move |term| (first, term)) + }) + }) + .collect(); + IndexAtoms { + equalities, + disequalities: self.disequalities.to_vec(), + bounds: self.bounds.to_vec(), + } + } + + fn and(&self, other: &Self) -> Option { + let mut atoms = self.atoms(); + let other = other.atoms(); + atoms.equalities.extend(other.equalities); + atoms.disequalities.extend(other.disequalities); + atoms.bounds.extend(other.bounds); + Self::from_atoms(atoms) + } + + fn substitute(&self, subst: &IndexSubst) -> Option { + if subst.is_empty() { + return Some(self.clone()); + } + let mut atoms = self.atoms(); + for (lhs, rhs) in &mut atoms.equalities { + *lhs = subst.apply(*lhs); + *rhs = subst.apply(*rhs); + } + for (lhs, rhs) in &mut atoms.disequalities { + *lhs = subst.apply(*lhs); + *rhs = subst.apply(*rhs); + } + for (expr, _) in &mut atoms.bounds { + *expr = subst.apply(*expr); + } + Self::from_atoms(atoms) + } + + fn implies(&self, other: &Self) -> bool { + other.classes.iter().all(|class| { + class.first().is_none_or(|first| { + class + .iter() + .skip(1) + .all(|term| self.proves_equal(*first, *term)) + }) + }) && other + .disequalities + .iter() + .all(|(lhs, rhs)| self.proves_disequal(*lhs, *rhs)) + && other + .bounds + .iter() + .all(|(expr, len)| match self.canonical_term(*expr) { + IndexExpr::Const(value) => value < *len, + expr => self + .bounds + .iter() + .find_map(|(candidate, bound)| (*candidate == expr).then_some(*bound)) + .is_some_and(|bound| bound <= *len), + }) + } + + fn proves_equal(&self, lhs: IndexExpr, rhs: IndexExpr) -> bool { + lhs == rhs || self.canonical_term(lhs) == self.canonical_term(rhs) + } + + fn proves_disequal(&self, lhs: IndexExpr, rhs: IndexExpr) -> bool { + let lhs = self.canonical_term(lhs); + let rhs = self.canonical_term(rhs); + matches!((lhs, rhs), (IndexExpr::Const(lhs), IndexExpr::Const(rhs)) if lhs != rhs) + || self + .disequalities + .binary_search(&ordered_pair(lhs, rhs)) + .is_ok() + } + + fn canonical_term(&self, expr: IndexExpr) -> IndexExpr { + self.classes + .iter() + .find(|class| class.binary_search(&expr).is_ok()) + .map_or(expr, |class| class_key(class)) + } +} + +fn find_root(parents: &mut [usize], mut index: usize) -> usize { + while parents[index] != index { + let parent = parents[index]; + parents[index] = parents[parent]; + index = parents[index]; + } + index +} + +fn class_key(class: &[IndexExpr]) -> IndexExpr { + class + .iter() + .find(|expr| matches!(expr, IndexExpr::Const(_))) + .copied() + .unwrap_or(class[0]) +} + +fn ordered_pair(lhs: IndexExpr, rhs: IndexExpr) -> (IndexExpr, IndexExpr) { + if lhs <= rhs { (lhs, rhs) } else { (rhs, lhs) } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Guard { + index: IndexConstraints, + variants: Box<[(ChoiceKey, VariantIndex)]>, +} + +impl Guard { + pub(crate) fn always() -> Self { + Self::default() + } + + pub(crate) fn equal(lhs: IndexExpr, rhs: IndexExpr) -> Option { + Self::always().with_equality(lhs, rhs) + } + + pub(crate) fn not_equal(lhs: IndexExpr, rhs: IndexExpr) -> Option { + Self::always().with_disequality(lhs, rhs) + } + + pub(crate) fn bounded(expr: IndexExpr, len: usize) -> Option { + Self::always().with_bound(expr, len) + } + + pub(crate) fn and(&self, other: &Self) -> Option { + let index = self.index.and(&other.index)?; + let mut variants = self.variants.iter().cloned().collect::>(); + for (choice, variant) in &other.variants { + if variants + .insert(choice.clone(), *variant) + .is_some_and(|existing| existing != *variant) + { + return None; + } + } + Some(Self { + index, + variants: variants.into_iter().collect(), + }) + } + + pub(crate) fn with_equality(&self, lhs: IndexExpr, rhs: IndexExpr) -> Option { + self.and(&Self::from_atoms(IndexAtoms { + equalities: vec![(lhs, rhs)], + ..IndexAtoms::default() + })?) + } + + pub(crate) fn with_disequality(&self, lhs: IndexExpr, rhs: IndexExpr) -> Option { + self.and(&Self::from_atoms(IndexAtoms { + disequalities: vec![(lhs, rhs)], + ..IndexAtoms::default() + })?) + } + + pub(crate) fn with_bound(&self, expr: IndexExpr, len: usize) -> Option { + self.and(&Self::from_atoms(IndexAtoms { + bounds: vec![(expr, len)], + ..IndexAtoms::default() + })?) + } + + pub(crate) fn with_variant(&self, choice: ChoiceKey, variant: VariantIndex) -> Option { + self.and(&Self { + index: IndexConstraints::default(), + variants: vec![(choice, variant)].into_boxed_slice(), + }) + } + + pub(crate) fn substitute(&self, subst: &IndexSubst) -> Option { + let index = self.index.substitute(subst)?; + let variants = self + .variants + .iter() + .map(|(choice, variant)| (choice.substitute(subst), *variant)) + .collect::>(); + Some(Self { + index, + variants: variants.into_iter().collect(), + }) + } + + pub(crate) fn scoped(&self, scope: ValueScope) -> Self { + Self { + index: self.index.clone(), + variants: self + .variants + .iter() + .map(|(choice, variant)| (choice.scoped(scope), *variant)) + .collect(), + } + } + + pub(crate) fn implies(&self, other: &Self) -> bool { + self.index.implies(&other.index) + && other + .variants + .iter() + .all(|expected| self.variants.binary_search(expected).is_ok()) + } + + pub(crate) fn satisfiable(&self) -> bool { + true + } + + pub(crate) fn proves_equal(&self, lhs: IndexExpr, rhs: IndexExpr) -> bool { + self.index.proves_equal(lhs, rhs) + } + + pub(crate) fn existential_ids(&self) -> BTreeSet { + let mut existentials = BTreeSet::new(); + let atoms = self.index.atoms(); + for expr in atoms + .equalities + .iter() + .chain(&atoms.disequalities) + .flat_map(|(lhs, rhs)| [lhs, rhs]) + .chain(atoms.bounds.iter().map(|(expr, _)| expr)) + { + if let IndexExpr::Existential(id) = expr { + existentials.insert(*id); + } + } + for (choice, _) in &self.variants { + for projection in choice.occurrence.as_slice() { + if let SlotProjection::Index(IndexExpr::Existential(id)) = projection { + existentials.insert(*id); + } + } + } + existentials + } + + pub(crate) fn index_exprs(&self) -> BTreeSet { + let atoms = self.index.atoms(); + let mut expressions = atoms + .equalities + .iter() + .chain(&atoms.disequalities) + .flat_map(|(lhs, rhs)| [*lhs, *rhs]) + .chain(atoms.bounds.iter().map(|(expr, _)| *expr)) + .collect::>(); + expressions.extend(self.variants.iter().flat_map(|(choice, _)| { + choice + .occurrence + .as_slice() + .iter() + .filter_map(|projection| match projection { + SlotProjection::Index(index) => Some(*index), + SlotProjection::Field(_) | SlotProjection::VariantField { .. } => None, + }) + })); + expressions + } + + #[cfg(test)] + pub(crate) fn alpha_normalize_existentials(&self) -> Self { + let mut subst = IndexSubst::new(); + for (next, old) in self.existential_ids().into_iter().enumerate() { + subst.insert( + IndexExpr::Existential(old), + IndexExpr::Existential(ExistentialId(next as u32)), + ); + } + self.substitute(&subst) + .expect("alpha-renaming preserves guard satisfiability") + } + + fn from_atoms(atoms: IndexAtoms) -> Option { + Some(Self { + index: IndexConstraints::from_atoms(atoms)?, + variants: Box::new([]), + }) + } +} + +fn substitute_slot_path(path: &SlotPath, subst: &IndexSubst) -> SlotPath { + SlotPath::from_steps(path.as_slice().iter().map(|projection| match projection { + SlotProjection::Field(field) => SlotProjection::Field(*field), + SlotProjection::VariantField { variant, field } => SlotProjection::VariantField { + variant: *variant, + field: *field, + }, + SlotProjection::Index(index) => SlotProjection::Index(subst.apply(*index)), + })) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn runtime(index: u32) -> IndexExpr { + IndexExpr::Runtime(SLocalId::from_u32(index)) + } + + #[test] + fn equality_and_disequality_detect_contradictions() { + let guard = Guard::equal(runtime(0), IndexExpr::Const(1)).expect("valid equality"); + + assert!( + guard + .with_disequality(runtime(0), IndexExpr::Const(1)) + .is_none() + ); + assert!(Guard::equal(IndexExpr::Const(1), IndexExpr::Const(2)).is_none()); + assert!(Guard::not_equal(IndexExpr::Const(1), IndexExpr::Const(2)).is_some()); + } + + #[test] + fn bounds_follow_equalities_and_substitutions() { + let guard = Guard::equal(runtime(0), runtime(1)) + .expect("valid equality") + .with_bound(runtime(0), 2) + .expect("valid bound"); + let expected = Guard::bounded(runtime(1), 3).expect("valid bound"); + assert!(guard.implies(&expected)); + + let subst = IndexSubst::from_pair(runtime(0), IndexExpr::Const(3)); + assert!(guard.substitute(&subst).is_none()); + } + + #[test] + fn enum_choices_are_scoped_and_exclusive() { + let choice = ChoiceKey::relative(SlotPath::new()); + let guard = Guard::always() + .with_variant(choice.clone(), VariantIndex(0)) + .expect("first variant"); + + assert!(guard.with_variant(choice, VariantIndex(1)).is_none()); + assert!( + guard + .scoped(ValueScope::Argument(0)) + .and(&guard.scoped(ValueScope::Argument(1))) + .is_some() + ); + } + + #[test] + fn existential_alpha_renaming_is_stable() { + let left = Guard::equal( + IndexExpr::Existential(ExistentialId(9)), + IndexExpr::Const(1), + ) + .expect("valid guard"); + let right = Guard::equal( + IndexExpr::Existential(ExistentialId(3)), + IndexExpr::Const(1), + ) + .expect("valid guard"); + + assert_eq!( + left.alpha_normalize_existentials(), + right.alpha_normalize_existentials() + ); + } +} diff --git a/crates/hir/src/analysis/semantic/borrowck/ir.rs b/crates/hir/src/analysis/semantic/borrowck/ir.rs index 6b2e9aa505..ceba3b27ea 100644 --- a/crates/hir/src/analysis/semantic/borrowck/ir.rs +++ b/crates/hir/src/analysis/semantic/borrowck/ir.rs @@ -4,12 +4,14 @@ use salsa::Update; use crate::{ analysis::{ HirAnalysisDb, + place::projectable_place_ty, semantic::{ - FieldIndex, LayoutBackingProjection, Mutability, SConst, SLocalId, SStmtId, SemOrigin, - SemanticBody, SemanticCalleeRef, SemanticCodeRegionRef, SemanticCodeRegionTarget, - SemanticLocalKind, SemanticProjectionPath, VariantIndex, + BorrowActivation, FieldIndex, LayoutBackingProjection, Mutability, SConst, SLocalId, + SStmtId, SemOrigin, SemanticBody, SemanticCalleeRef, SemanticCodeRegionRef, + SemanticCodeRegionTarget, SemanticLocalKind, SemanticProjectionPath, VariantIndex, }, ty::{ + adt_def::{AdtRef, instantiate_adt_field_shape}, provider::ProviderAddressSpace, ty_check::{BodyOwner, EffectPassMode, LocalBinding}, ty_def::{BorrowKind, TyId}, @@ -20,6 +22,8 @@ use crate::{ semantic::ProviderBinding, }; +use super::summary::BorrowSummary; + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct NBorrowRootId(u32); entity_impl!(NBorrowRootId); @@ -66,6 +70,11 @@ impl<'db> NormalizedSemanticBody<'db> { }, } } + + pub fn place_ty(&self, db: &'db dyn HirAnalysisDb, place: &NSPlace<'db>) -> Option> { + let root_ty = self.place_root_ty(&place.root)?; + semantic_projection_ty(db, root_ty, &place.path).map(|(ty, _)| ty) + } } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -167,6 +176,184 @@ pub struct NLayoutBackingSource<'db> { pub source: NSPlace<'db>, } +fn layout_backing_projection_matches( + pattern: LayoutBackingProjection, + candidate: LayoutBackingProjection, +) -> bool { + pattern == candidate + || matches!( + (pattern, candidate), + ( + LayoutBackingProjection::Index(None) | LayoutBackingProjection::IndexFamily(_), + LayoutBackingProjection::Index(_) | LayoutBackingProjection::IndexFamily(_) + ) + ) +} + +fn layout_backing_path_is_prefix( + prefix: &[LayoutBackingProjection], + path: &[LayoutBackingProjection], +) -> bool { + prefix.len() <= path.len() + && prefix + .iter() + .copied() + .zip(path.iter().copied()) + .all(|(pattern, candidate)| layout_backing_projection_matches(pattern, candidate)) +} + +fn semantic_layout_query<'db>( + path: &SemanticProjectionPath<'db>, +) -> Option<(Vec, SemanticProjectionPath<'db>)> { + let mut target = Vec::new(); + let mut filtered = SemanticProjectionPath::new(); + for projection in path.iter() { + let step = match projection { + Projection::Field(field) => { + LayoutBackingProjection::Field(FieldIndex(u16::try_from(*field).ok()?)) + } + Projection::VariantField { + variant, field_idx, .. + } => LayoutBackingProjection::VariantField { + variant: *variant, + field: FieldIndex(u16::try_from(*field_idx).ok()?), + }, + Projection::Index(IndexSource::Constant(index)) => { + LayoutBackingProjection::Index(Some(*index)) + } + Projection::Index(IndexSource::Dynamic(_)) => LayoutBackingProjection::Index(None), + Projection::Deref => continue, + Projection::Discriminant => return None, + }; + target.push(step); + filtered.push(projection.clone()); + } + Some((target, filtered)) +} + +pub(super) fn layout_path_for_semantic_projection<'db>( + path: &SemanticProjectionPath<'db>, +) -> Option> { + semantic_layout_query(path).map(|(target, _)| target) +} + +pub(super) fn semantic_projection_for_layout_path<'db>( + db: &'db dyn HirAnalysisDb, + mut ty: TyId<'db>, + path: &[LayoutBackingProjection], +) -> Option> { + let mut out = SemanticProjectionPath::new(); + for step in path { + ty = projectable_place_ty(db, ty); + match *step { + LayoutBackingProjection::Field(field) => { + ty = *ty.field_types(db).get(field.0 as usize)?; + out.push(Projection::Field(field.0 as usize)); + } + LayoutBackingProjection::VariantField { variant, field } => { + let adt = ty.adt_def(db)?; + if !matches!(adt.adt_ref(db), AdtRef::Enum(_)) { + return None; + } + let field_ty = instantiate_adt_field_shape( + db, + adt, + variant.0 as usize, + field.0 as usize, + ty.generic_args(db), + ); + out.push(Projection::VariantField { + variant, + enum_ty: ty, + field_idx: field.0 as usize, + }); + ty = field_ty; + } + LayoutBackingProjection::Index(Some(index)) => { + if !ty.is_array(db) || ty.array_len(db).is_some_and(|len| index >= len) { + return None; + } + ty = *ty.generic_args(db).first()?; + out.push(Projection::Index(IndexSource::Constant(index))); + } + LayoutBackingProjection::Index(None) | LayoutBackingProjection::IndexFamily(_) => { + return None; + } + } + } + Some(out) +} + +pub(crate) fn semantic_projection_ty<'db>( + db: &'db dyn HirAnalysisDb, + mut ty: TyId<'db>, + path: &SemanticProjectionPath<'db>, +) -> Option<(TyId<'db>, bool)> { + let mut traverses_capability = false; + for projection in path.iter() { + if !matches!(projection, Projection::Deref) { + while let Some((_, inner)) = ty.as_capability(db) { + traverses_capability = true; + ty = inner; + } + } + ty = match projection { + Projection::Field(field) => *ty.field_types(db).get(*field)?, + Projection::VariantField { + variant, field_idx, .. + } => { + let adt = ty.adt_def(db)?; + instantiate_adt_field_shape( + db, + adt, + variant.0 as usize, + *field_idx, + ty.generic_args(db), + ) + } + Projection::Index(_) => { + if !ty.is_array(db) { + return None; + } + *ty.generic_args(db).first()? + } + Projection::Deref => { + let (_, inner) = ty.as_capability(db)?; + traverses_capability = true; + inner + } + Projection::Discriminant => return None, + }; + } + traverses_capability |= ty.as_capability(db).is_some(); + Some((ty, traverses_capability)) +} + +pub(super) fn resolved_layout_backing_places<'db>( + sources: &[NLayoutBackingSource<'db>], + requested: &SemanticProjectionPath<'db>, +) -> Vec> { + let Some((target, path)) = semantic_layout_query(requested) else { + return Vec::new(); + }; + let mut resolved = Vec::new(); + for source in sources { + if !layout_backing_path_is_prefix(&source.target, &target) { + continue; + } + let mut suffix = SemanticProjectionPath::new(); + for projection in path.iter().skip(source.target.len()) { + suffix.push(projection.clone()); + } + let mut place = source.source.clone(); + place.path = place.path.concat(&suffix); + if !resolved.contains(&place) { + resolved.push(place); + } + } + resolved +} + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum NBorrowRoot<'db> { Param { @@ -359,6 +546,7 @@ pub enum NExpr<'db> { place: NSPlace<'db>, kind: BorrowKind, provider: Option, + activation: BorrowActivation, }, Const(SConst<'db>), Unary { @@ -564,24 +752,11 @@ pub enum SemanticNormalizeError<'db> { }, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum BorrowInputRef { - Param(u32), -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct BorrowTransform<'db> { - pub input: BorrowInputRef, - pub proj: NSProjectionPath<'db>, -} - -pub type BorrowSummary<'db> = Vec>; - #[salsa::interned] #[derive(Debug)] pub struct BorrowSummaryId<'db> { #[return_ref] - pub items: Vec>, + pub summary: BorrowSummary, } #[derive(Clone, Debug, PartialEq, Eq, Hash, Update)] diff --git a/crates/hir/src/analysis/semantic/borrowck/loan.rs b/crates/hir/src/analysis/semantic/borrowck/loan.rs new file mode 100644 index 0000000000..d0420328ad --- /dev/null +++ b/crates/hir/src/analysis/semantic/borrowck/loan.rs @@ -0,0 +1,338 @@ +use crate::analysis::{ + semantic::{BorrowActivation, SemOrigin}, + ty::ty_def::BorrowKind, +}; + +use super::{ + guard::{Guard, IndexExpr, IndexParamId, IndexSubst, ResultIndexId}, + region::RegionSet, + shape::SlotPath, + summary::{SummaryPath, SummaryProjection}, + value::IndexPayload, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(super) struct LoanId(pub(super) u32); + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ParentClause { + guard: Guard, + reference: LoanRef, +} + +impl ParentClause { + pub(crate) fn new(guard: Guard, reference: LoanRef) -> Self { + Self { guard, reference } + } + + pub(crate) fn guard(&self) -> &Guard { + &self.guard + } + + pub(crate) fn reference(&self) -> &LoanRef { + &self.reference + } + + fn substitute(&self, subst: &IndexSubst) -> Option { + Some(Self { + guard: self.guard.substitute(subst)?, + reference: self.reference.substitute(subst), + }) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct ParentSet { + clauses: Vec, +} + +impl ParentSet { + pub(crate) fn from_guarded_references( + references: impl IntoIterator, + ) -> Self { + Self::from_clauses( + references + .into_iter() + .map(|(guard, reference)| ParentClause::new(guard, reference)), + ) + } + + fn from_clauses(clauses: impl IntoIterator) -> Self { + let mut clauses = clauses.into_iter().collect::>(); + clauses.sort_unstable(); + clauses.dedup(); + Self { clauses } + } + + pub(crate) fn iter(&self) -> impl Iterator { + self.clauses.iter() + } + + pub(crate) fn with_guard(&self, guard: &Guard) -> Self { + Self::from_clauses(self.clauses.iter().filter_map(|clause| { + Some(ParentClause { + guard: clause.guard.and(guard)?, + reference: clause.reference.clone(), + }) + })) + } + + pub(crate) fn substitute(&self, subst: &IndexSubst) -> Self { + Self::from_clauses( + self.clauses + .iter() + .filter_map(|clause| clause.substitute(subst)), + ) + } + + pub(crate) fn union(&mut self, other: Self) -> bool { + let before = self.clauses.len(); + self.clauses.extend(other.clauses); + self.clauses.sort_unstable(); + self.clauses.dedup(); + self.clauses.len() != before + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct AuthoritySet(ParentSet); + +impl AuthoritySet { + pub(crate) fn from_parents(parents: ParentSet) -> Self { + Self(parents) + } + + pub(crate) fn iter(&self) -> impl Iterator { + self.0.iter() + } + + pub(crate) fn union(&mut self, other: Self) { + self.0.union(other.0); + } + + pub(crate) fn matches(&self, reference: &LoanRef, holder_guard: &Guard) -> bool { + self.iter().any(|authority| { + holder_guard + .and(authority.guard()) + .and_then(|guard| reference.unify(authority.reference(), &guard)) + .is_some() + }) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct LoanDef<'db> { + kind: BorrowKind, + activation: BorrowActivation, + binders: Vec, + region: RegionSet<'db>, + parents: ParentSet, + origin: SemOrigin<'db>, +} + +impl<'db> LoanDef<'db> { + pub(crate) fn plain( + kind: BorrowKind, + activation: BorrowActivation, + origin: SemOrigin<'db>, + ) -> Self { + Self::new(kind, activation, Vec::new(), origin) + } + + pub(crate) fn for_slot( + kind: BorrowKind, + path: &SlotPath, + activation: BorrowActivation, + origin: SemOrigin<'db>, + ) -> Self { + Self::new(kind, activation, slot_binders(path), origin) + } + + pub(crate) fn for_summary( + kind: BorrowKind, + path: &SummaryPath, + activation: BorrowActivation, + origin: SemOrigin<'db>, + ) -> Self { + let mut binders = result_params(path) + .map(|result| IndexParamId(result.0)) + .collect::>(); + binders.sort_unstable(); + binders.dedup(); + Self::new(kind, activation, binders, origin) + } + + fn new( + kind: BorrowKind, + activation: BorrowActivation, + binders: Vec, + origin: SemOrigin<'db>, + ) -> Self { + Self { + kind, + activation, + binders, + region: RegionSet::empty(), + parents: ParentSet::default(), + origin, + } + } + + pub(crate) fn kind(&self) -> BorrowKind { + self.kind + } + + pub(crate) fn activation(&self) -> BorrowActivation { + self.activation + } + + pub(crate) fn origin(&self) -> SemOrigin<'db> { + self.origin + } + + pub(crate) fn parents(&self) -> &ParentSet { + &self.parents + } + + pub(crate) fn extend(&mut self, region: RegionSet<'db>, parents: ParentSet) -> bool { + let subst = self.result_parameter_subst(); + let region = region.substitute(&subst); + let parents = parents.substitute(&subst); + let joined = self.region.union(®ion); + let changed = joined != self.region; + self.region = joined; + self.parents.union(parents) || changed + } + + pub(crate) fn instantiate(&self, reference: &LoanRef) -> RegionSet<'db> { + self.region.substitute(&self.instantiation_subst(reference)) + } + + pub(crate) fn instantiate_parents( + &self, + reference: &LoanRef, + holder_guard: &Guard, + ) -> ParentSet { + self.parents + .substitute(&self.instantiation_subst(reference)) + .with_guard(holder_guard) + } + + fn instantiation_subst(&self, reference: &LoanRef) -> IndexSubst { + let mut subst = IndexSubst::new(); + for binder in &self.binders { + if let Some(value) = reference.binding_for_param(*binder) { + subst.insert(IndexExpr::LoanParam(*binder), value); + } + } + subst + } + + fn result_parameter_subst(&self) -> IndexSubst { + let mut subst = IndexSubst::new(); + for binder in &self.binders { + subst.insert( + IndexExpr::ResultParam(ResultIndexId(binder.0)), + IndexExpr::LoanParam(*binder), + ); + } + subst + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct LoanRef { + pub(crate) id: LoanId, + args: Vec<(IndexParamId, IndexExpr)>, +} + +impl LoanRef { + pub(crate) fn new(id: LoanId) -> Self { + Self { + id, + args: Vec::new(), + } + } + + pub(crate) fn for_slot(id: LoanId, path: &SlotPath) -> Self { + Self { + id, + args: slot_binders(path) + .into_iter() + .map(|param| (param, IndexExpr::LoanParam(param))) + .collect(), + } + } + + pub(crate) fn for_summary(id: LoanId, path: &SummaryPath) -> Self { + Self { + id, + args: result_params(path) + .map(|result| (IndexParamId(result.0), IndexExpr::ResultParam(result))) + .collect(), + } + } + + fn binding_for_param(&self, param: IndexParamId) -> Option { + self.args + .iter() + .find_map(|(candidate, value)| (*candidate == param).then_some(*value)) + } + + pub(crate) fn substitute(&self, subst: &IndexSubst) -> Self { + self.substitute_indices(subst) + } + + pub(crate) fn unify(&self, other: &Self, guard: &Guard) -> Option { + if self.id != other.id || self.args.len() != other.args.len() { + return None; + } + self.args.iter().zip(&other.args).try_fold( + guard.clone(), + |guard, ((lhs_param, lhs), (rhs_param, rhs))| { + (lhs_param == rhs_param) + .then(|| guard.with_equality(*lhs, *rhs)) + .flatten() + }, + ) + } +} + +impl IndexPayload for LoanRef { + fn substitute_indices(&self, subst: &IndexSubst) -> Self { + Self { + id: self.id, + args: self + .args + .iter() + .map(|(param, value)| (*param, subst.apply(*value))) + .collect(), + } + } +} + +fn slot_binders(path: &SlotPath) -> Vec { + let mut binders = path + .as_slice() + .iter() + .filter_map(|projection| match projection { + super::shape::SlotProjection::Index(index) => Some(*index), + super::shape::SlotProjection::Field(_) + | super::shape::SlotProjection::VariantField { .. } => None, + }) + .collect::>(); + binders.sort_unstable(); + binders.dedup(); + binders +} + +fn result_params(path: &SummaryPath) -> impl Iterator + '_ { + path.as_slice() + .iter() + .filter_map(|projection| match projection { + SummaryProjection::Index(IndexExpr::ResultParam(result)) => Some(*result), + SummaryProjection::Field(_) + | SummaryProjection::VariantField { .. } + | SummaryProjection::Index(_) => None, + }) +} diff --git a/crates/hir/src/analysis/semantic/borrowck/mod.rs b/crates/hir/src/analysis/semantic/borrowck/mod.rs index dede41a8ad..f779c591e5 100644 --- a/crates/hir/src/analysis/semantic/borrowck/mod.rs +++ b/crates/hir/src/analysis/semantic/borrowck/mod.rs @@ -1,12 +1,20 @@ +mod access; mod analyses; mod callsite; mod canon; mod check; mod diagnostics; mod facts; +mod guard; mod ir; +mod loan; mod noesc; mod normalize; +mod region; +mod shape; +mod summary; +mod transfer; +mod value; mod verify; pub(crate) use callsite::provisional_call_site_provider_refinements; @@ -16,7 +24,9 @@ pub use check::{ }; pub(crate) use diagnostics::{checker_name, resolve_local_source_span, span_for_origin_from_body}; pub use facts::*; +pub use guard::{ExistentialId, Guard, IndexExpr, IndexParamId, ResultIndexId, ValueIndexId}; pub use ir::*; pub use noesc::{check_semantic_noesc, check_semantic_noesc_voucher}; pub use normalize::{normalize_semantic_body, normalize_semantic_body_for_layout_evidence}; +pub use summary::*; pub use verify::verify_normalized_semantic_body; diff --git a/crates/hir/src/analysis/semantic/borrowck/noesc.rs b/crates/hir/src/analysis/semantic/borrowck/noesc.rs index 2eef58ac40..acd0b713ee 100644 --- a/crates/hir/src/analysis/semantic/borrowck/noesc.rs +++ b/crates/hir/src/analysis/semantic/borrowck/noesc.rs @@ -1,6 +1,5 @@ use common::diagnostics::CompleteDiagnostic; use cranelift_entity::EntityRef; -use rustc_hash::FxHashSet; use crate::analysis::{ HirAnalysisDb, @@ -10,13 +9,16 @@ use crate::analysis::{ }; use super::{ - canon::{BorrowRoot, CanonPlace, State, address_space_for_borrow_root}, + canon::address_space_for_region_root, check::Borrowck, diagnostics::{normalized_body_internal_diag, operand_origin}, ir::{ BorrowDiagnosticId, NExpr, NOperand, NSStmt, NSStmtKind, SemanticBorrowCheckResult, SemanticBorrowDiagKind, SemanticBorrowDiagnostic, SemanticBorrowDiagnosticSpan, }, + region::{RegionRoot, RegionSet}, + shape::capability_shape, + transfer::BorrowState, }; pub fn check_semantic_noesc<'db>( @@ -64,7 +66,7 @@ impl<'db> NoEsc<'db> { self.borrowck.entry_state[crate::analysis::semantic::SBlockId::new(bb_idx)].clone(); for stmt in &block.stmts { self.check_stmt(&state, stmt)?; - self.borrowck.canon().apply_stmt_state(&mut state, stmt); + self.borrowck.apply_stmt_state(&mut state, stmt); } } Ok(()) @@ -72,7 +74,7 @@ impl<'db> NoEsc<'db> { fn check_stmt( &self, - state: &State, + state: &BorrowState<'db>, stmt: &NSStmt<'db>, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { match &stmt.kind { @@ -87,15 +89,12 @@ impl<'db> NoEsc<'db> { fn check_store( &self, - state: &State, + state: &BorrowState<'db>, origin: SemOrigin<'db>, dst: &super::ir::NSPlace<'db>, src: NOperand, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { - let targets = self - .borrowck - .canon() - .canonicalize_place(state, dst, origin)?; + let targets = self.borrowck.canon().resolve_place(state, dst, origin)?; let spaces = self.address_spaces_for_targets(&targets, origin)?; if spaces.contains(&ProviderAddressSpace::Calldata) { return Err(self.noesc_diag(origin, "cannot write to calldata".to_string())); @@ -128,27 +127,22 @@ impl<'db> NoEsc<'db> { fn check_call_args( &self, - state: &State, + state: &BorrowState<'db>, origin: SemOrigin<'db>, callee: SemanticCalleeRef<'db>, args: &[NOperand], ) -> Result<(), SemanticBorrowDiagnostic<'db>> { for arg in args.iter().copied().skip(self.receiver_arg_count(callee)) { - let ty = self.operand_ty(arg, origin)?; - if ty.as_borrow(self.borrowck.db).is_none() { + let arg_origin = operand_origin(arg, origin); + let ty = self.operand_ty(arg, arg_origin)?; + if !capability_shape(self.borrowck.db, ty).contains_borrow(self.borrowck.db) { continue; } - let targets = self.borrowck.canon().borrow_local_targets(state, arg.local); - let spaces = self.address_spaces_for_targets(&targets, operand_origin(arg, origin))?; - let Some(space) = spaces - .iter() - .copied() - .find(|space| *space != ProviderAddressSpace::Memory) - else { + let Some(space) = self.non_memory_operand_space(state, arg, arg_origin)? else { continue; }; return Err(self.noesc_diag( - operand_origin(arg, origin), + arg_origin, format!( "cannot pass `{}` from {} as function argument", ty.pretty_print(self.borrowck.db), @@ -159,6 +153,22 @@ impl<'db> NoEsc<'db> { Ok(()) } + fn non_memory_operand_space( + &self, + state: &BorrowState<'db>, + operand: NOperand, + origin: SemOrigin<'db>, + ) -> Result, SemanticBorrowDiagnostic<'db>> { + let targets = self + .borrowck + .canon() + .borrow_local_region(state, operand.local); + let spaces = self.address_spaces_for_targets(&targets, origin)?; + Ok(spaces + .into_iter() + .find(|space| *space != ProviderAddressSpace::Memory)) + } + fn receiver_arg_count(&self, callee: SemanticCalleeRef<'db>) -> usize { match callee.key.owner(self.borrowck.db) { BodyOwner::Func(func) if func.receiver_ty(self.borrowck.db).is_some() => 1, @@ -188,12 +198,12 @@ impl<'db> NoEsc<'db> { fn address_spaces_for_targets( &self, - targets: &FxHashSet>, + targets: &RegionSet<'db>, origin: SemOrigin<'db>, ) -> Result, SemanticBorrowDiagnostic<'db>> { - let mut spaces = Vec::with_capacity(targets.len()); - for target in targets { - let space = self.address_space_for_root(&target.root, origin)?; + let mut spaces = Vec::new(); + for (_, target) in targets.guarded_places() { + let space = self.address_space_for_root(target.root(), origin)?; if !spaces.contains(&space) { spaces.push(space); } @@ -204,10 +214,10 @@ impl<'db> NoEsc<'db> { fn address_space_for_root( &self, - root: &BorrowRoot<'db>, + root: &RegionRoot<'db>, origin: SemOrigin<'db>, ) -> Result> { - address_space_for_borrow_root( + address_space_for_region_root( self.borrowck.db, self.borrowck.instance, &self.borrowck.body, diff --git a/crates/hir/src/analysis/semantic/borrowck/normalize.rs b/crates/hir/src/analysis/semantic/borrowck/normalize.rs index 0e96a9c26c..dc1283fa5a 100644 --- a/crates/hir/src/analysis/semantic/borrowck/normalize.rs +++ b/crates/hir/src/analysis/semantic/borrowck/normalize.rs @@ -994,10 +994,12 @@ impl<'db> NormalizeCtxt<'db> { place, kind, provider, + activation, } => NExpr::Borrow { place: self.normalize_place(place)?, kind: *kind, provider: *provider, + activation: *activation, }, SExpr::GetEnumTag { value } => NExpr::GetEnumTag { value: self.normalize_copy_operand(*value, origin), diff --git a/crates/hir/src/analysis/semantic/borrowck/region.rs b/crates/hir/src/analysis/semantic/borrowck/region.rs new file mode 100644 index 0000000000..b0a38339d8 --- /dev/null +++ b/crates/hir/src/analysis/semantic/borrowck/region.rs @@ -0,0 +1,614 @@ +use crate::{ + analysis::semantic::{FieldIndex, SLocalId, VariantIndex}, + semantic::ProviderBinding, +}; + +use super::{ + guard::{ExistentialId, Guard, IndexExpr, IndexSubst}, + shape::{SlotPath, SlotProjection}, +}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum RegionRoot<'db> { + ParamPlace(u32), + ParamCapability { + param: u32, + slot: SlotPath, + }, + Local(SLocalId), + Provider(ProviderBinding<'db>), +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum RegionProjection { + Field(FieldIndex), + VariantField { + variant: VariantIndex, + field: FieldIndex, + }, + Index(IndexExpr), +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct SymbolicPlace<'db> { + root: RegionRoot<'db>, + projection: Vec, +} + +impl<'db> SymbolicPlace<'db> { + pub(crate) fn new( + root: RegionRoot<'db>, + projection: impl IntoIterator, + ) -> Self { + Self { + root, + projection: projection.into_iter().collect(), + } + } + + pub(crate) fn root(&self) -> &RegionRoot<'db> { + &self.root + } + + pub(crate) fn projection(&self) -> &[RegionProjection] { + &self.projection + } + + fn project(&self, projection: &[RegionProjection]) -> Self { + Self { + root: self.root.clone(), + projection: self.projection.iter().chain(projection).cloned().collect(), + } + } + + pub(crate) fn substitute(&self, subst: &IndexSubst) -> Self { + Self { + root: match &self.root { + RegionRoot::ParamCapability { param, slot } => RegionRoot::ParamCapability { + param: *param, + slot: substitute_slot_path(slot, subst), + }, + root => root.clone(), + }, + projection: self + .projection + .iter() + .map(|projection| match projection { + RegionProjection::Field(field) => RegionProjection::Field(*field), + RegionProjection::VariantField { variant, field } => { + RegionProjection::VariantField { + variant: *variant, + field: *field, + } + } + RegionProjection::Index(index) => RegionProjection::Index(subst.apply(*index)), + }) + .collect(), + } + } + + pub(crate) fn index_exprs(&self) -> Vec { + let root = match &self.root { + RegionRoot::ParamCapability { slot, .. } => slot + .as_slice() + .iter() + .filter_map(|projection| match projection { + SlotProjection::Index(index) => Some(*index), + SlotProjection::Field(_) | SlotProjection::VariantField { .. } => None, + }) + .collect::>(), + RegionRoot::ParamPlace(_) | RegionRoot::Local(_) | RegionRoot::Provider(_) => { + Vec::new() + } + }; + root.into_iter() + .chain(self.projection.iter().filter_map(|projection| { + if let RegionProjection::Index(index) = projection { + Some(*index) + } else { + None + } + })) + .collect() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct RegionClause<'db> { + guard: Guard, + place: SymbolicPlace<'db>, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub(crate) struct RegionSet<'db> { + clauses: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct OverlapWitness<'db> { + pub(crate) left: SymbolicPlace<'db>, + pub(crate) right: SymbolicPlace<'db>, + pub(crate) model: Guard, +} + +impl<'db> RegionSet<'db> { + pub(crate) fn empty() -> Self { + Self::default() + } + + pub(crate) fn singleton(place: SymbolicPlace<'db>) -> Self { + Self::from_clause(Guard::always(), place) + } + + pub(crate) fn from_clause(guard: Guard, place: SymbolicPlace<'db>) -> Self { + Self::normalize(vec![RegionClause { guard, place }]) + } + + pub(crate) fn union(&self, other: &Self) -> Self { + Self::normalize(self.clauses.iter().chain(&other.clauses).cloned().collect()) + } + + pub(crate) fn with_guard(&self, guard: &Guard) -> Self { + Self::normalize( + self.clauses + .iter() + .filter_map(|clause| { + Some(RegionClause { + guard: clause.guard.and(guard)?, + place: clause.place.clone(), + }) + }) + .collect(), + ) + } + + pub(crate) fn substitute(&self, subst: &IndexSubst) -> Self { + Self::normalize( + self.clauses + .iter() + .filter_map(|clause| { + Some(RegionClause { + guard: clause.guard.substitute(subst)?, + place: clause.place.substitute(subst), + }) + }) + .collect(), + ) + } + + pub(crate) fn project(&self, projection: &[RegionProjection]) -> Self { + Self::normalize( + self.clauses + .iter() + .map(|clause| RegionClause { + guard: clause.guard.clone(), + place: clause.place.project(projection), + }) + .collect(), + ) + } + + pub(crate) fn may_overlap(&self, other: &Self) -> Option> { + for lhs in &self.clauses { + for rhs in &other.clauses { + let rhs = freshen_against(rhs, lhs); + let Some(mut model) = lhs.guard.and(&rhs.guard) else { + continue; + }; + if !roots_match(&lhs.place.root, &rhs.place.root) + || !projections_may_overlap( + &lhs.place.projection, + &rhs.place.projection, + &mut model, + ) + { + continue; + } + return Some(OverlapWitness { + left: lhs.place.clone(), + right: rhs.place.clone(), + model, + }); + } + } + None + } + + pub(crate) fn intersection(&self, other: &Self) -> Self { + let mut clauses = Vec::new(); + for lhs in &self.clauses { + for rhs in &other.clauses { + let rhs = freshen_against(rhs, lhs); + let Some(mut guard) = lhs.guard.and(&rhs.guard) else { + continue; + }; + if !roots_match(&lhs.place.root, &rhs.place.root) + || !projections_may_overlap( + &lhs.place.projection, + &rhs.place.projection, + &mut guard, + ) + { + continue; + } + clauses.push(RegionClause { + guard, + place: if lhs.place.projection.len() >= rhs.place.projection.len() { + lhs.place.clone() + } else { + rhs.place.clone() + }, + }); + } + } + Self::normalize(clauses) + } + + pub(crate) fn provably_covers(&self, other: &Self) -> bool { + other.clauses.iter().all(|target| { + self.clauses.iter().any(|container| { + roots_match(&container.place.root, &target.place.root) + && projection_covers( + &container.place.projection, + &target.place.projection, + &target.guard, + ) + && target.guard.implies(&container.guard) + }) + }) + } + + pub(crate) fn is_empty(&self) -> bool { + self.clauses.is_empty() + } + + pub(crate) fn guarded_places(&self) -> impl Iterator)> { + self.clauses + .iter() + .map(|clause| (&clause.guard, &clause.place)) + } + + pub(crate) fn clauses(&self) -> impl Iterator + '_ { + self.clauses.iter().cloned().map(|clause| Self { + clauses: vec![clause], + }) + } + + pub(crate) fn has_root(&self, root: &RegionRoot<'db>) -> bool { + self.clauses.iter().any(|clause| &clause.place.root == root) + } + + fn normalize(clauses: Vec>) -> Self { + let mut clauses = clauses + .into_iter() + .filter(|clause| clause.guard.satisfiable()) + .map(alpha_normalize_clause) + .collect::>(); + clauses.sort_by(|lhs, rhs| { + root_sort_key(&lhs.place.root) + .cmp(&root_sort_key(&rhs.place.root)) + .then_with(|| lhs.place.projection.cmp(&rhs.place.projection)) + .then_with(|| lhs.guard.cmp(&rhs.guard)) + }); + clauses.dedup(); + + let mut normalized: Vec> = Vec::new(); + for clause in clauses { + if normalized.iter().any(|existing| { + existing.place == clause.place && clause.guard.implies(&existing.guard) + }) { + continue; + } + normalized.retain(|existing| { + existing.place != clause.place || !existing.guard.implies(&clause.guard) + }); + normalized.push(clause); + } + Self { + clauses: normalized, + } + } +} + +fn freshen_against<'db>( + clause: &RegionClause<'db>, + other: &RegionClause<'db>, +) -> RegionClause<'db> { + let offset = existential_ids(other) + .into_iter() + .map(|id| id.0) + .max() + .and_then(|id| id.checked_add(1)) + .unwrap_or(0); + let mut subst = IndexSubst::new(); + for id in existential_ids(clause) { + subst.insert( + IndexExpr::Existential(id), + IndexExpr::Existential(ExistentialId( + offset.checked_add(id.0).expect("existential id overflow"), + )), + ); + } + RegionClause { + guard: clause + .guard + .substitute(&subst) + .expect("alpha-renaming preserves satisfiability"), + place: clause.place.substitute(&subst), + } +} + +fn existential_ids(clause: &RegionClause<'_>) -> Vec { + let mut ordered = Vec::new(); + if let RegionRoot::ParamCapability { slot, .. } = &clause.place.root { + collect_slot_existentials(slot, &mut ordered); + } + for projection in &clause.place.projection { + if let RegionProjection::Index(IndexExpr::Existential(id)) = projection + && !ordered.contains(id) + { + ordered.push(*id); + } + } + for id in clause.guard.existential_ids() { + if !ordered.contains(&id) { + ordered.push(id); + } + } + ordered +} + +fn roots_match(lhs: &RegionRoot<'_>, rhs: &RegionRoot<'_>) -> bool { + lhs == rhs +} + +fn projections_may_overlap( + lhs: &[RegionProjection], + rhs: &[RegionProjection], + guard: &mut Guard, +) -> bool { + for (lhs, rhs) in lhs.iter().zip(rhs) { + match (lhs, rhs) { + (RegionProjection::Field(lhs), RegionProjection::Field(rhs)) => { + if lhs != rhs { + return false; + } + } + ( + RegionProjection::VariantField { + variant: lhs_variant, + field: lhs_field, + }, + RegionProjection::VariantField { + variant: rhs_variant, + field: rhs_field, + }, + ) => { + if lhs_variant != rhs_variant || lhs_field != rhs_field { + return false; + } + } + (RegionProjection::Index(lhs), RegionProjection::Index(rhs)) => { + let Some(combined) = guard.with_equality(*lhs, *rhs) else { + return false; + }; + *guard = combined; + } + _ => return false, + } + } + true +} + +fn projection_covers( + container: &[RegionProjection], + target: &[RegionProjection], + guard: &Guard, +) -> bool { + container.len() <= target.len() + && container + .iter() + .zip(target) + .all(|(container, target)| match (container, target) { + (RegionProjection::Field(lhs), RegionProjection::Field(rhs)) => lhs == rhs, + ( + RegionProjection::VariantField { + variant: lhs_variant, + field: lhs_field, + }, + RegionProjection::VariantField { + variant: rhs_variant, + field: rhs_field, + }, + ) => lhs_variant == rhs_variant && lhs_field == rhs_field, + (RegionProjection::Index(lhs), RegionProjection::Index(rhs)) => { + guard.proves_equal(*lhs, *rhs) + } + _ => false, + }) +} + +fn alpha_normalize_clause<'db>(clause: RegionClause<'db>) -> RegionClause<'db> { + let mut subst = IndexSubst::new(); + for (next, old) in existential_ids(&clause).into_iter().enumerate() { + subst.insert( + IndexExpr::Existential(old), + IndexExpr::Existential(ExistentialId(next as u32)), + ); + } + RegionClause { + guard: clause + .guard + .substitute(&subst) + .expect("alpha-renaming preserves satisfiability"), + place: clause.place.substitute(&subst), + } +} + +fn collect_slot_existentials(path: &SlotPath, out: &mut Vec) { + for projection in path.as_slice() { + if let SlotProjection::Index(IndexExpr::Existential(id)) = projection + && !out.contains(id) + { + out.push(*id); + } + } +} + +fn substitute_slot_path(path: &SlotPath, subst: &IndexSubst) -> SlotPath { + SlotPath::from_steps(path.as_slice().iter().map(|projection| match projection { + SlotProjection::Field(field) => SlotProjection::Field(*field), + SlotProjection::VariantField { variant, field } => SlotProjection::VariantField { + variant: *variant, + field: *field, + }, + SlotProjection::Index(index) => SlotProjection::Index(subst.apply(*index)), + })) +} + +fn root_sort_key(root: &RegionRoot<'_>) -> (u8, u32) { + match root { + RegionRoot::ParamPlace(param) => (0, *param), + RegionRoot::ParamCapability { param, .. } => (1, *param), + RegionRoot::Local(local) => (2, local.as_u32()), + RegionRoot::Provider(provider) => (3, provider.provider_idx), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn local( + index: u32, + projection: impl IntoIterator, + ) -> RegionSet<'static> { + RegionSet::singleton(SymbolicPlace::new( + RegionRoot::Local(SLocalId::from_u32(index)), + projection, + )) + } + + #[test] + fn distinct_constant_indices_are_disjoint() { + let left = local(0, [RegionProjection::Index(IndexExpr::Const(0))]); + let right = local(0, [RegionProjection::Index(IndexExpr::Const(1))]); + + assert!(left.may_overlap(&right).is_none()); + } + + #[test] + fn dynamic_index_overlap_produces_an_equality_model() { + let dynamic = IndexExpr::Runtime(SLocalId::from_u32(1)); + let left = local(0, [RegionProjection::Index(dynamic)]); + let right = local(0, [RegionProjection::Index(IndexExpr::Const(1))]); + let witness = left.may_overlap(&right).expect("indices may be equal"); + + assert!(witness.model.proves_equal(dynamic, IndexExpr::Const(1))); + } + + #[test] + fn incompatible_guards_prove_regions_disjoint() { + let index = IndexExpr::Runtime(SLocalId::from_u32(1)); + let place = SymbolicPlace::new( + RegionRoot::Local(SLocalId::from_u32(0)), + [RegionProjection::Index(index)], + ); + let left = RegionSet::from_clause( + Guard::equal(index, IndexExpr::Const(0)).expect("valid guard"), + place.clone(), + ); + let right = RegionSet::from_clause( + Guard::equal(index, IndexExpr::Const(1)).expect("valid guard"), + place, + ); + + assert!(left.may_overlap(&right).is_none()); + } + + #[test] + fn coverage_requires_proof_of_the_index_relation() { + let index = IndexExpr::Runtime(SLocalId::from_u32(1)); + let symbolic = local(0, [RegionProjection::Index(index)]); + let exact = local(0, [RegionProjection::Index(IndexExpr::Const(0))]); + let constrained = + exact.with_guard(&Guard::equal(index, IndexExpr::Const(0)).expect("valid equality")); + + assert!(!symbolic.provably_covers(&exact)); + assert!(symbolic.provably_covers(&constrained)); + } + + #[test] + fn guarded_intersection_can_be_covered_by_partial_suspension() { + let index = IndexExpr::LoanParam(super::super::guard::IndexParamId(0)); + let base = RegionSet::from_clause( + Guard::bounded(index, 2).expect("nonempty array"), + SymbolicPlace::new(RegionRoot::Local(SLocalId::from_u32(0)), []), + ); + let access = local(0, []); + let suspended = + base.with_guard(&Guard::equal(index, IndexExpr::Const(0)).expect("valid member")); + let overlap = base.intersection(&access); + + assert!(!overlap.is_empty()); + assert!(!suspended.provably_covers(&overlap)); + assert!(suspended.provably_covers( + &overlap.with_guard(&Guard::equal(index, IndexExpr::Const(0)).expect("valid member")) + )); + } + + #[test] + fn distinct_enum_variants_are_disjoint() { + let left = local( + 0, + [RegionProjection::VariantField { + variant: VariantIndex(0), + field: FieldIndex(0), + }], + ); + let right = local( + 0, + [RegionProjection::VariantField { + variant: VariantIndex(1), + field: FieldIndex(0), + }], + ); + + assert!(left.intersection(&right).is_empty()); + assert!(left.may_overlap(&right).is_none()); + } + + #[test] + fn existential_normalization_uses_place_occurrence_order() { + let left = local( + 0, + [RegionProjection::Index(IndexExpr::Existential( + ExistentialId(9), + ))], + ); + let right = local( + 0, + [RegionProjection::Index(IndexExpr::Existential( + ExistentialId(2), + ))], + ); + + assert_eq!(left, right); + } + + #[test] + fn separate_clauses_do_not_share_existential_constraints() { + let existential = IndexExpr::Existential(ExistentialId(0)); + let place = SymbolicPlace::new(RegionRoot::Local(SLocalId::from_u32(0)), []); + let left = RegionSet::from_clause( + Guard::equal(existential, IndexExpr::Const(0)).expect("valid guard"), + place.clone(), + ); + let right = RegionSet::from_clause( + Guard::equal(existential, IndexExpr::Const(1)).expect("valid guard"), + place, + ); + + assert!(left.may_overlap(&right).is_some()); + } +} diff --git a/crates/hir/src/analysis/semantic/borrowck/shape.rs b/crates/hir/src/analysis/semantic/borrowck/shape.rs new file mode 100644 index 0000000000..295bd3c528 --- /dev/null +++ b/crates/hir/src/analysis/semantic/borrowck/shape.rs @@ -0,0 +1,396 @@ +use crate::analysis::{ + HirAnalysisDb, + semantic::{FieldIndex, VariantIndex}, + ty::{ + adt_def::{AdtRef, instantiate_adt_field_shape}, + ty_def::{BorrowKind, TyId}, + }, +}; + +use super::guard::IndexParamId; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum CapabilityLeafKind { + Borrow(BorrowKind), + View, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum FieldKey { + Tuple(FieldIndex), + Struct(FieldIndex), + Variant(FieldIndex), +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum SlotProjection { + Field(FieldKey), + VariantField { + variant: VariantIndex, + field: FieldIndex, + }, + Index(I), +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct SlotPath(Vec>); + +impl SlotPath { + pub(crate) fn new() -> Self { + Self(Vec::new()) + } + + pub(crate) fn from_steps(steps: impl IntoIterator>) -> Self { + Self(steps.into_iter().collect()) + } + + pub(crate) fn push(&mut self, projection: SlotProjection) { + self.0.push(projection); + } + + pub(crate) fn pop(&mut self) -> Option> { + self.0.pop() + } + + pub(crate) fn as_slice(&self) -> &[SlotProjection] { + &self.0 + } + + pub(crate) fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl SlotPath { + pub(crate) fn map_indices(&self, mut map: impl FnMut(&I) -> J) -> SlotPath { + SlotPath::from_steps(self.0.iter().map(|projection| match projection { + SlotProjection::Field(field) => SlotProjection::Field(*field), + SlotProjection::VariantField { variant, field } => SlotProjection::VariantField { + variant: *variant, + field: *field, + }, + SlotProjection::Index(index) => SlotProjection::Index(map(index)), + })) + } +} + +impl FieldKey { + pub(crate) fn index(self) -> FieldIndex { + match self { + Self::Tuple(index) | Self::Struct(index) | Self::Variant(index) => index, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct CapabilityShape<'db> { + pub(crate) direct: Option, + pub(crate) children: ShapeChildren<'db>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum ShapeChildren<'db> { + None, + Product { + fields: Box<[(FieldKey, ShapeId<'db>)]>, + }, + Sum { + variants: Box<[(VariantIndex, ShapeId<'db>)]>, + }, + Array { + len: usize, + elem: ShapeId<'db>, + }, +} + +#[salsa::interned] +#[derive(Debug)] +pub(crate) struct ShapeId<'db> { + #[return_ref] + pub(crate) data: CapabilityShape<'db>, +} + +impl<'db> ShapeId<'db> { + pub(crate) fn contains_borrow(self, db: &'db dyn HirAnalysisDb) -> bool { + self.contains(db, |leaf| matches!(leaf, CapabilityLeafKind::Borrow(_))) + } + + pub(crate) fn contains_capability(self, db: &'db dyn HirAnalysisDb) -> bool { + self.contains(db, |_| true) + } + + fn contains( + self, + db: &'db dyn HirAnalysisDb, + predicate: impl Copy + Fn(CapabilityLeafKind) -> bool, + ) -> bool { + let shape = self.data(db); + shape.direct.is_some_and(predicate) + || match &shape.children { + ShapeChildren::None => false, + ShapeChildren::Product { fields } => fields + .iter() + .any(|(_, child)| child.contains(db, predicate)), + ShapeChildren::Sum { variants } => variants + .iter() + .any(|(_, child)| child.contains(db, predicate)), + ShapeChildren::Array { elem, .. } => elem.contains(db, predicate), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct CapabilitySlot { + pub(crate) kind: BorrowKind, + pub(crate) path: SlotPath, +} + +pub(crate) fn capability_slots<'db>( + db: &'db dyn HirAnalysisDb, + shape: ShapeId<'db>, + include_views: bool, +) -> Vec { + fn collect<'db>( + db: &'db dyn HirAnalysisDb, + shape: ShapeId<'db>, + include_views: bool, + path: &mut SlotPath, + next_binder: &mut u32, + out: &mut Vec, + ) { + match shape.data(db).direct { + Some(CapabilityLeafKind::Borrow(kind)) => out.push(CapabilitySlot { + kind, + path: path.clone(), + }), + Some(CapabilityLeafKind::View) if include_views => out.push(CapabilitySlot { + kind: BorrowKind::Ref, + path: path.clone(), + }), + Some(CapabilityLeafKind::View) | None => {} + } + + match &shape.data(db).children { + ShapeChildren::None => {} + ShapeChildren::Product { fields } => { + for (field, child) in fields { + path.push(SlotProjection::Field(*field)); + collect(db, *child, include_views, path, next_binder, out); + path.pop(); + } + } + ShapeChildren::Sum { variants } => { + for (variant, child) in variants { + let ShapeChildren::Product { fields } = &child.data(db).children else { + continue; + }; + for (field, field_shape) in fields { + path.push(SlotProjection::VariantField { + variant: *variant, + field: field.index(), + }); + collect(db, *field_shape, include_views, path, next_binder, out); + path.pop(); + } + } + } + ShapeChildren::Array { elem, .. } + if if include_views { + elem.contains_capability(db) + } else { + elem.contains_borrow(db) + } => + { + let binder = IndexParamId(*next_binder); + *next_binder = next_binder + .checked_add(1) + .expect("capability-slot binder space exhausted"); + path.push(SlotProjection::Index(binder)); + collect(db, *elem, include_views, path, next_binder, out); + path.pop(); + } + ShapeChildren::Array { .. } => {} + } + } + + let mut slots = Vec::new(); + collect( + db, + shape, + include_views, + &mut SlotPath::new(), + &mut 0, + &mut slots, + ); + slots +} + +pub(crate) fn capability_shape<'db>(db: &'db dyn HirAnalysisDb, ty: TyId<'db>) -> ShapeId<'db> { + build_shape(db, ty, &mut Vec::new()) +} + +fn build_shape<'db>( + db: &'db dyn HirAnalysisDb, + ty: TyId<'db>, + visiting: &mut Vec>, +) -> ShapeId<'db> { + if let Some((kind, _)) = ty.as_borrow(db) { + return intern_shape( + db, + Some(CapabilityLeafKind::Borrow(kind)), + ShapeChildren::None, + ); + } + + if let Some(inner) = ty.as_view(db) { + if inner.as_capability(db).is_some() { + return build_shape(db, inner, visiting); + } + let inner = build_shape(db, inner, visiting); + return intern_shape( + db, + Some(CapabilityLeafKind::View), + inner.data(db).children.clone(), + ); + } + + if visiting.contains(&ty) { + return empty_shape(db); + } + visiting.push(ty); + + let children = if ty.is_array(db) { + match (ty.array_len(db), ty.generic_args(db).first().copied()) { + (Some(0) | None, _) | (_, None) => ShapeChildren::None, + (Some(len), Some(elem)) => ShapeChildren::Array { + len, + elem: build_shape(db, elem, visiting), + }, + } + } else if ty.is_tuple(db) { + ShapeChildren::Product { + fields: product_fields(db, ty.field_types(db), visiting, FieldKey::Tuple), + } + } else if let Some(adt) = ty.adt_def(db) { + match adt.adt_ref(db) { + AdtRef::Struct(_) => ShapeChildren::Product { + fields: product_fields(db, ty.field_types(db), visiting, FieldKey::Struct), + }, + AdtRef::Enum(_) => { + let mut variants = Vec::new(); + for (variant_idx, variant) in adt.fields(db).iter().enumerate() { + let Some(variant_idx) = u16::try_from(variant_idx).ok().map(VariantIndex) + else { + continue; + }; + let fields = (0..variant.num_types()) + .filter_map(|field_idx| { + let field = u16::try_from(field_idx).ok().map(FieldIndex)?; + let field_ty = instantiate_adt_field_shape( + db, + adt, + variant_idx.0 as usize, + field_idx, + ty.generic_args(db), + ); + Some(( + FieldKey::Variant(field), + build_shape(db, field_ty, visiting), + )) + }) + .collect::>() + .into_boxed_slice(); + variants.push(( + variant_idx, + intern_shape(db, None, ShapeChildren::Product { fields }), + )); + } + ShapeChildren::Sum { + variants: variants.into_boxed_slice(), + } + } + } + } else { + ShapeChildren::None + }; + + visiting.pop(); + intern_shape(db, None, children) +} + +fn product_fields<'db>( + db: &'db dyn HirAnalysisDb, + fields: Vec>, + visiting: &mut Vec>, + key: impl Fn(FieldIndex) -> FieldKey, +) -> Box<[(FieldKey, ShapeId<'db>)]> { + fields + .into_iter() + .enumerate() + .filter_map(|(idx, ty)| { + let index = u16::try_from(idx).ok().map(FieldIndex)?; + Some((key(index), build_shape(db, ty, visiting))) + }) + .collect::>() + .into_boxed_slice() +} + +fn empty_shape<'db>(db: &'db dyn HirAnalysisDb) -> ShapeId<'db> { + intern_shape(db, None, ShapeChildren::None) +} + +fn intern_shape<'db>( + db: &'db dyn HirAnalysisDb, + direct: Option, + children: ShapeChildren<'db>, +) -> ShapeId<'db> { + ShapeId::new(db, CapabilityShape { direct, children }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_db::HirAnalysisTestDb; + + #[test] + fn shape_nodes_are_structurally_interned() { + let db = HirAnalysisTestDb::default(); + let first = intern_shape( + &db, + Some(CapabilityLeafKind::Borrow(BorrowKind::Mut)), + ShapeChildren::None, + ); + let second = intern_shape( + &db, + Some(CapabilityLeafKind::Borrow(BorrowKind::Mut)), + ShapeChildren::None, + ); + + assert_eq!(first, second); + assert!(first.contains_borrow(&db)); + } + + #[test] + fn array_shape_size_does_not_depend_on_declared_length() { + let db = HirAnalysisTestDb::default(); + let elem = intern_shape( + &db, + Some(CapabilityLeafKind::Borrow(BorrowKind::Ref)), + ShapeChildren::None, + ); + let array = intern_shape( + &db, + None, + ShapeChildren::Array { + len: 1_000_000, + elem, + }, + ); + + assert!(array.contains_borrow(&db)); + assert!(matches!( + array.data(&db).children, + ShapeChildren::Array { len: 1_000_000, .. } + )); + } +} diff --git a/crates/hir/src/analysis/semantic/borrowck/summary.rs b/crates/hir/src/analysis/semantic/borrowck/summary.rs new file mode 100644 index 0000000000..3a3e7b605d --- /dev/null +++ b/crates/hir/src/analysis/semantic/borrowck/summary.rs @@ -0,0 +1,399 @@ +use crate::analysis::{ + HirAnalysisDb, + place::projectable_place_ty, + semantic::{FieldIndex, VariantIndex}, + ty::{ + adt_def::{AdtRef, instantiate_adt_field_shape}, + ty_def::{BorrowKind, TyId}, + }, +}; + +use super::{ + guard::{ExistentialId, Guard, IndexExpr, IndexSubst}, + shape::{CapabilityLeafKind, ShapeChildren, ShapeId, capability_shape}, +}; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct BorrowSummary { + leaves: Vec, +} + +impl BorrowSummary { + pub(crate) fn new(mut leaves: Vec) -> Self { + leaves.sort_unstable(); + leaves.dedup(); + Self { leaves } + } + + pub fn leaves(&self) -> &[BorrowSummaryLeaf] { + &self.leaves + } + + pub fn is_empty(&self) -> bool { + self.leaves.is_empty() + } + + pub fn len(&self) -> usize { + self.leaves.len() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BorrowSummaryLeaf { + pub kind: BorrowKind, + pub path: SummaryPath, + pub sources: Vec, +} + +impl BorrowSummaryLeaf { + pub(crate) fn new( + kind: BorrowKind, + path: SummaryPath, + sources: Vec, + ) -> Self { + let mut sources = sources + .into_iter() + .map(BorrowSourceClause::alpha_normalize_existentials) + .collect::>(); + sources.sort_unstable(); + sources.dedup(); + Self { + kind, + path, + sources, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BorrowSourceClause { + pub guard: Guard, + pub source: BorrowSource, +} + +impl BorrowSourceClause { + fn alpha_normalize_existentials(self) -> Self { + let mut ordered = self.source.index_exprs(); + for expression in self.guard.index_exprs() { + if !ordered.contains(&expression) { + ordered.push(expression); + } + } + let mut unique = Vec::new(); + ordered.retain(|expression| { + matches!(expression, IndexExpr::Existential(_)) && !unique.contains(expression) && { + unique.push(*expression); + true + } + }); + let mut subst = IndexSubst::new(); + let mut next = 0; + for expression in ordered { + if let IndexExpr::Existential(id) = expression { + subst.insert( + IndexExpr::Existential(id), + IndexExpr::Existential(ExistentialId(next)), + ); + next += 1; + } + } + Self { + guard: self + .guard + .substitute(&subst) + .expect("alpha-renaming preserves satisfiability"), + source: self.source.substitute(&subst), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BorrowSource { + ParamPlace { param: u32, path: SummaryPath }, + ParamCapability { param: u32, slot: SummaryPath }, + AnyAccessible { param: u32, class: AccessClass }, +} + +impl BorrowSource { + pub fn param(&self) -> u32 { + match self { + Self::ParamPlace { param, .. } + | Self::ParamCapability { param, .. } + | Self::AnyAccessible { param, .. } => *param, + } + } + + fn index_exprs(&self) -> Vec { + match self { + Self::ParamPlace { path, .. } | Self::ParamCapability { slot: path, .. } => { + path.index_exprs() + } + Self::AnyAccessible { .. } => Vec::new(), + } + } + + fn substitute(&self, subst: &IndexSubst) -> Self { + match self { + Self::ParamPlace { param, path } => Self::ParamPlace { + param: *param, + path: path.substitute(subst), + }, + Self::ParamCapability { param, slot } => Self::ParamCapability { + param: *param, + slot: slot.substitute(subst), + }, + Self::AnyAccessible { param, class } => Self::AnyAccessible { + param: *param, + class: *class, + }, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AccessClass { + Shared, + Mutable, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SummaryPath(Vec); + +impl SummaryPath { + pub fn new() -> Self { + Self::default() + } + + pub fn from_steps(steps: impl IntoIterator) -> Self { + Self(steps.into_iter().collect()) + } + + pub fn as_slice(&self) -> &[SummaryProjection] { + &self.0 + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub(crate) fn index_exprs(&self) -> Vec { + self.0 + .iter() + .filter_map(|projection| match projection { + SummaryProjection::Index(index) => Some(*index), + SummaryProjection::Field(_) | SummaryProjection::VariantField { .. } => None, + }) + .collect() + } + + pub(crate) fn substitute(&self, subst: &IndexSubst) -> Self { + Self::from_steps(self.0.iter().map(|projection| match projection { + SummaryProjection::Field(field) => SummaryProjection::Field(*field), + SummaryProjection::VariantField { variant, field } => SummaryProjection::VariantField { + variant: *variant, + field: *field, + }, + SummaryProjection::Index(index) => SummaryProjection::Index(subst.apply(*index)), + })) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SummaryProjection { + Field(FieldIndex), + VariantField { + variant: VariantIndex, + field: FieldIndex, + }, + Index(IndexExpr), +} + +pub(crate) fn validate_borrow_summary<'db>( + db: &'db dyn HirAnalysisDb, + result_ty: TyId<'db>, + argument_tys: &[TyId<'db>], + summary: &BorrowSummary, +) -> Result<(), String> { + let result_shape = capability_shape(db, result_ty); + for leaf in summary.leaves() { + if shape_for_summary_path(db, result_shape, &leaf.path) + .filter(|shape| { + matches!( + shape.data(db).direct, + Some(CapabilityLeafKind::Borrow(kind)) if kind == leaf.kind + ) + }) + .is_none() + { + return Err(format!( + "callee borrow summary contains invalid result slot {:?}", + leaf.path + )); + } + let result_params = leaf + .path + .index_exprs() + .into_iter() + .filter_map(|expression| match expression { + IndexExpr::ResultParam(param) => Some(param), + _ => None, + }) + .collect::>(); + if leaf.path.index_exprs().into_iter().any(|expression| { + !matches!(expression, IndexExpr::Const(_) | IndexExpr::ResultParam(_)) + }) { + return Err("callee borrow summary has an unbound result index".to_string()); + } + for clause in &leaf.sources { + let Some(argument_ty) = argument_tys.get(clause.source.param() as usize).copied() + else { + return Err(format!( + "callee borrow summary references missing input {}", + clause.source.param() + )); + }; + let source_indices = match &clause.source { + BorrowSource::ParamPlace { path, .. } + | BorrowSource::ParamCapability { slot: path, .. } => path.index_exprs(), + BorrowSource::AnyAccessible { .. } => Vec::new(), + }; + if clause + .guard + .index_exprs() + .into_iter() + .chain(source_indices) + .any(|expression| match expression { + IndexExpr::Const(_) | IndexExpr::Existential(_) => false, + IndexExpr::ResultParam(param) => !result_params.contains(¶m), + IndexExpr::InputParam(param) => param as usize >= argument_tys.len(), + IndexExpr::Runtime(_) | IndexExpr::ValueParam(_) | IndexExpr::LoanParam(_) => { + true + } + }) + { + return Err("callee borrow summary has an out-of-scope source index".to_string()); + } + let valid_source = match &clause.source { + BorrowSource::ParamPlace { path, .. } => { + summary_place_path_ty(db, argument_ty, path).is_some() + } + BorrowSource::ParamCapability { slot, .. } => { + shape_for_summary_path(db, capability_shape(db, argument_ty), slot).is_some_and( + |shape| { + matches!( + shape.data(db).direct, + Some(CapabilityLeafKind::Borrow(kind)) + if leaf.kind == BorrowKind::Ref || kind == BorrowKind::Mut + ) + }, + ) + } + BorrowSource::AnyAccessible { class, .. } => { + leaf.kind == BorrowKind::Ref || *class == AccessClass::Mutable + } + }; + if !valid_source { + return Err("callee borrow summary contains an invalid source slot".to_string()); + } + } + } + Ok(()) +} + +pub(crate) fn shape_for_summary_path<'db>( + db: &'db dyn HirAnalysisDb, + mut shape: ShapeId<'db>, + path: &SummaryPath, +) -> Option> { + for projection in path.as_slice() { + shape = + match (projection, &shape.data(db).children) { + (SummaryProjection::Field(field), ShapeChildren::Product { fields }) => fields + .iter() + .find_map(|(key, shape)| (key.index() == *field).then_some(*shape))?, + ( + SummaryProjection::VariantField { variant, field }, + ShapeChildren::Sum { variants }, + ) => { + let variant_shape = variants.iter().find_map(|(candidate, shape)| { + (*candidate == *variant).then_some(*shape) + })?; + let ShapeChildren::Product { fields } = &variant_shape.data(db).children else { + return None; + }; + fields + .iter() + .find_map(|(key, shape)| (key.index() == *field).then_some(*shape))? + } + (SummaryProjection::Index(_), ShapeChildren::Array { elem, .. }) => *elem, + _ => return None, + }; + } + Some(shape) +} + +fn summary_place_path_ty<'db>( + db: &'db dyn HirAnalysisDb, + mut ty: TyId<'db>, + path: &SummaryPath, +) -> Option> { + for projection in path.as_slice() { + ty = projectable_place_ty(db, ty); + ty = match projection { + SummaryProjection::Field(field) => *ty.field_types(db).get(field.0 as usize)?, + SummaryProjection::VariantField { variant, field } => { + let adt = ty.adt_def(db)?; + if !matches!(adt.adt_ref(db), AdtRef::Enum(_)) { + return None; + } + instantiate_adt_field_shape( + db, + adt, + variant.0 as usize, + field.0 as usize, + ty.generic_args(db), + ) + } + SummaryProjection::Index(index) => { + if !ty.is_array(db) + || matches!(index, IndexExpr::Const(index) if ty.array_len(db).is_some_and(|len| *index >= len)) + { + return None; + } + *ty.generic_args(db).first()? + } + }; + } + Some(ty) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn source_existentials_are_clause_local_and_alpha_normalized() { + let clause = |id| BorrowSourceClause { + guard: Guard::equal(IndexExpr::Existential(id), IndexExpr::Const(1)) + .expect("valid guard"), + source: BorrowSource::ParamCapability { + param: 0, + slot: SummaryPath::from_steps([SummaryProjection::Index(IndexExpr::Existential( + id, + ))]), + }, + }; + let left = BorrowSummaryLeaf::new( + BorrowKind::Mut, + SummaryPath::new(), + vec![clause(ExistentialId(3))], + ); + let right = BorrowSummaryLeaf::new( + BorrowKind::Mut, + SummaryPath::new(), + vec![clause(ExistentialId(19))], + ); + + assert_eq!(left, right); + } +} diff --git a/crates/hir/src/analysis/semantic/borrowck/transfer.rs b/crates/hir/src/analysis/semantic/borrowck/transfer.rs new file mode 100644 index 0000000000..af4281b254 --- /dev/null +++ b/crates/hir/src/analysis/semantic/borrowck/transfer.rs @@ -0,0 +1,602 @@ +use std::{cell::RefCell, fmt, rc::Rc}; + +use cranelift_entity::SecondaryMap; +use dataflow::JoinSemiLattice; +use rustc_hash::FxHashMap; + +use crate::{ + analysis::{ + HirAnalysisDb, + semantic::{LayoutBackingProjection, SLocalId}, + ty::ty_is_noesc, + }, + projection::{IndexSource, Projection}, +}; + +use super::{ + guard::{ExistentialId, IndexExpr, ValueScope}, + ir::{ + NBorrowRoot, NBorrowRootId, NExpr, NSPlace, NSPlaceRoot, NSProjectionPath, NSStmt, + NSStmtKind, NormalizedSemanticBody, layout_path_for_semantic_projection, + semantic_projection_ty, + }, + loan::{LoanId, LoanRef}, + shape::{FieldKey, ShapeChildren, ShapeId, SlotPath, SlotProjection, capability_shape}, + summary::{SummaryPath, SummaryProjection}, + value::{GuardedLeaf, ValueId, ValueInterner}, +}; + +pub(crate) type BorrowStateValueId<'db> = ValueId<'db, LoanRef>; +pub(crate) type BorrowValueInterner<'db> = ValueInterner<'db, LoanRef>; +pub(crate) type SharedBorrowValueInterner<'db> = Rc>>; + +pub(crate) fn shared_value_interner<'db>( + db: &'db dyn HirAnalysisDb, +) -> SharedBorrowValueInterner<'db> { + Rc::new(RefCell::new(ValueInterner::new(db))) +} + +pub(super) struct BorrowTransferCx<'a, 'db> { + db: &'db dyn HirAnalysisDb, + body: &'a NormalizedSemanticBody<'db>, + loan_for_local: &'a FxHashMap, + constant_indices: &'a SecondaryMap>, +} + +impl<'a, 'db> BorrowTransferCx<'a, 'db> { + pub(super) fn new( + db: &'db dyn HirAnalysisDb, + body: &'a NormalizedSemanticBody<'db>, + loan_for_local: &'a FxHashMap, + constant_indices: &'a SecondaryMap>, + ) -> Self { + Self { + db, + body, + loan_for_local, + constant_indices, + } + } + + pub(super) fn apply_stmt( + &self, + state: &mut BorrowState<'db>, + stmt: &NSStmt<'db>, + call_result_loans: Option<&[(SummaryPath, LoanId)]>, + ) { + match &stmt.kind { + NSStmtKind::Assign { dst, expr } => { + let Some(dst_shape) = self.local_shape(*dst) else { + state.clear(*dst); + return; + }; + let value = match expr { + NExpr::Use(src) => self.own_loan_value(state, *dst).unwrap_or_else(|| { + self.propagated_value(state, *dst, state.value(src.local)) + }), + NExpr::Borrow { .. } => self + .own_loan_value(state, *dst) + .unwrap_or_else(|| state.empty(dst_shape)), + NExpr::Call { .. } => { + if let Some(own) = self.own_loan_value(state, *dst) { + own + } else if let Some(call_result_loans) = call_result_loans { + summary_loan_value( + state.interner(), + dst_shape, + call_result_loans.iter().map(|(path, loan)| { + (path.clone(), LoanRef::for_summary(*loan, path)) + }), + ) + .unwrap_or_else(|| state.empty(dst_shape)) + } else { + state.empty(dst_shape) + } + } + NExpr::AggregateMake { fields, .. } => { + match &dst_shape.data(self.db).children { + ShapeChildren::Product { + fields: shape_fields, + } => { + let fields = shape_fields + .iter() + .enumerate() + .map(|(idx, (key, shape))| { + let value = fields + .get(idx) + .and_then(|field| state.value(field.local)) + .unwrap_or_else(|| state.empty(*shape)); + (*key, value) + }) + .collect::>(); + state.product(dst_shape, fields) + } + ShapeChildren::Array { elem, .. } => { + let fields = fields + .iter() + .enumerate() + .map(|(idx, field)| { + ( + idx, + state + .value(field.local) + .unwrap_or_else(|| state.empty(*elem)), + ) + }) + .collect::>(); + state.array_exact(dst_shape, fields) + } + ShapeChildren::None | ShapeChildren::Sum { .. } => { + state.empty(dst_shape) + } + } + } + NExpr::EnumMake { + variant, fields, .. + } => { + let ShapeChildren::Sum { variants } = &dst_shape.data(self.db).children + else { + return state.assign(*dst, state.empty(dst_shape)); + }; + let Some(variant_shape) = variants.iter().find_map(|(candidate, shape)| { + (*candidate == *variant).then_some(*shape) + }) else { + return state.assign(*dst, state.empty(dst_shape)); + }; + let ShapeChildren::Product { + fields: shape_fields, + } = &variant_shape.data(self.db).children + else { + return state.assign(*dst, state.empty(dst_shape)); + }; + let fields = shape_fields + .iter() + .enumerate() + .map(|(idx, (key, shape))| { + let value = fields + .get(idx) + .and_then(|field| state.value(field.local)) + .unwrap_or_else(|| state.empty(*shape)); + (*key, value) + }) + .collect::>(); + let variant_value = state.product(variant_shape, fields); + state.sum_variant(dst_shape, *variant, variant_value) + } + NExpr::ArrayRepeat { value, .. } => { + let ShapeChildren::Array { elem, .. } = dst_shape.data(self.db).children + else { + return state.assign(*dst, state.empty(dst_shape)); + }; + let value = state + .value(value.local) + .unwrap_or_else(|| state.empty(elem)); + state.array_repeat(dst_shape, value) + } + NExpr::ExtractEnumField { + value, + variant, + field, + } => { + let projected = self.local_shape(value.local).and_then(|shape| { + let projection = [LayoutBackingProjection::VariantField { + variant: *variant, + field: *field, + }]; + let path = slot_path_for_layout(self.db, shape, &projection)?; + state.project(value.local, &path, ValueScope::Local(value.local)) + }); + self.propagated_value(state, *dst, projected) + } + NExpr::ReadPlace { place, .. } => { + self.own_loan_value(state, *dst).unwrap_or_else(|| { + let projected = self.place_base_local(place).and_then(|base| { + let shape = self.local_shape(base)?; + let projection = self.layout_path(&place.path)?; + let path = slot_path_for_layout(self.db, shape, &projection)?; + state.project(base, &path, ValueScope::Local(base)) + }); + self.propagated_value(state, *dst, projected) + }) + } + _ => state.empty(dst_shape), + }; + state.assign(*dst, value); + } + NSStmtKind::Store { dst, src } => { + if let NSPlaceRoot::Root(root) = dst.root + && let Some(base) = self.root_base_local(root) + { + let path = self.materialize_constant_indices(&dst.path); + if self + .body + .place_root_ty(&dst.root) + .and_then(|ty| semantic_projection_ty(self.db, ty, &path)) + .is_some_and(|(_, traverses_capability)| traverses_capability) + { + return; + } + let (Some(base_shape), Some(projection), Some(src_shape)) = ( + self.local_shape(base), + layout_path_for_semantic_projection(&path), + self.local_shape(src.local), + ) else { + return; + }; + let Some(path) = slot_path_for_layout(self.db, base_shape, &projection) else { + return; + }; + let replacement = state + .value(src.local) + .unwrap_or_else(|| state.empty(src_shape)); + state.replace(base, base_shape, &path, replacement); + } + } + } + } + + fn propagated_value( + &self, + state: &BorrowState<'db>, + dst: SLocalId, + value: Option>, + ) -> BorrowStateValueId<'db> { + let shape = self + .local_shape(dst) + .expect("normalized destination local must have a capability shape"); + if self.body.local(dst).is_some_and(|local| { + local.ty.as_capability(self.db).is_some() || ty_is_noesc(self.db, local.ty) + }) && let Some(value) = value + && state.interner().borrow().shape(value) == shape + { + value + } else { + state.empty(shape) + } + } + + fn own_loan_value( + &self, + state: &BorrowState<'db>, + local: SLocalId, + ) -> Option> { + let shape = self.local_shape(local)?; + self.loan_for_local + .get(&local) + .copied() + .map(|loan| state.direct_loan(shape, loan)) + } + + fn local_shape(&self, local: SLocalId) -> Option> { + self.body + .local(local) + .map(|local| capability_shape(self.db, local.ty)) + } + + fn root_base_local(&self, root: NBorrowRootId) -> Option { + match self.body.root(root)? { + NBorrowRoot::Param { local, .. } | NBorrowRoot::LocalSlot { local } => Some(*local), + NBorrowRoot::Provider { .. } => None, + } + } + + fn place_base_local(&self, place: &NSPlace<'db>) -> Option { + match place.root { + NSPlaceRoot::CarrierDerefLocal(local) => Some(local), + NSPlaceRoot::Root(root) => self.root_base_local(root), + } + } + + fn materialize_constant_indices(&self, path: &NSProjectionPath<'db>) -> NSProjectionPath<'db> { + let mut out = NSProjectionPath::new(); + for projection in path.iter() { + out.push(match projection { + Projection::Index(IndexSource::Dynamic(index)) + if let Some(index) = self.constant_indices[*index] => + { + Projection::Index(IndexSource::Constant(index)) + } + projection => projection.clone(), + }); + } + out + } + + fn layout_path(&self, path: &NSProjectionPath<'db>) -> Option> { + layout_path_for_semantic_projection(&self.materialize_constant_indices(path)) + } +} + +#[derive(Clone)] +pub(crate) struct BorrowState<'db> { + values: FxHashMap>, + interner: SharedBorrowValueInterner<'db>, +} + +impl fmt::Debug for BorrowState<'_> { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("BorrowState") + .field("values", &self.values) + .finish_non_exhaustive() + } +} + +impl PartialEq for BorrowState<'_> { + fn eq(&self, other: &Self) -> bool { + debug_assert!(Rc::ptr_eq(&self.interner, &other.interner)); + self.values == other.values + } +} + +impl Eq for BorrowState<'_> {} + +impl<'db> BorrowState<'db> { + pub(crate) fn new(interner: SharedBorrowValueInterner<'db>) -> Self { + Self { + values: FxHashMap::default(), + interner, + } + } + + pub(crate) fn value(&self, local: SLocalId) -> Option> { + self.values.get(&local).copied() + } + + pub(crate) fn interner(&self) -> &SharedBorrowValueInterner<'db> { + &self.interner + } + + pub(crate) fn empty(&self, shape: ShapeId<'db>) -> BorrowStateValueId<'db> { + self.interner.borrow_mut().empty(shape) + } + + pub(crate) fn direct_loan(&self, shape: ShapeId<'db>, loan: LoanId) -> BorrowStateValueId<'db> { + direct_loan_value(&self.interner, shape, loan) + } + + pub(crate) fn product( + &self, + shape: ShapeId<'db>, + fields: impl IntoIterator)>, + ) -> BorrowStateValueId<'db> { + self.interner.borrow_mut().product(shape, fields) + } + + pub(crate) fn sum_variant( + &self, + shape: ShapeId<'db>, + variant: crate::analysis::semantic::VariantIndex, + value: BorrowStateValueId<'db>, + ) -> BorrowStateValueId<'db> { + self.interner + .borrow_mut() + .sum_variant(shape, variant, value) + } + + pub(crate) fn array_repeat( + &self, + shape: ShapeId<'db>, + value: BorrowStateValueId<'db>, + ) -> BorrowStateValueId<'db> { + self.interner.borrow_mut().array_repeat(shape, value) + } + + pub(crate) fn array_exact( + &self, + shape: ShapeId<'db>, + values: impl IntoIterator)>, + ) -> BorrowStateValueId<'db> { + self.interner.borrow_mut().array_exact(shape, values) + } + + pub(crate) fn assign(&mut self, local: SLocalId, value: BorrowStateValueId<'db>) { + if self.interner.borrow().is_empty(value) { + self.values.remove(&local); + } else { + self.values.insert(local, value); + } + } + + pub(crate) fn clear(&mut self, local: SLocalId) { + self.values.remove(&local); + } + + pub(crate) fn leaves_in( + &self, + local: SLocalId, + scope: ValueScope, + ) -> Vec> { + self.value(local).map_or_else(Vec::new, |value| { + self.interner.borrow().enumerate_leaves(value, scope) + }) + } + + pub(crate) fn leaves( + &self, + value: BorrowStateValueId<'db>, + scope: ValueScope, + ) -> Vec> { + self.interner.borrow().enumerate_leaves(value, scope) + } + + pub(crate) fn project( + &self, + local: SLocalId, + path: &SlotPath, + scope: ValueScope, + ) -> Option> { + let value = self.value(local)?; + Some(self.interner.borrow_mut().project(value, path, scope)) + } + + pub(crate) fn replace( + &mut self, + local: SLocalId, + shape: ShapeId<'db>, + path: &SlotPath, + replacement: BorrowStateValueId<'db>, + ) { + let value = self.value(local).unwrap_or_else(|| self.empty(shape)); + let value = self.interner.borrow_mut().replace(value, path, replacement); + self.assign(local, value); + } + + pub(crate) fn locals(&self) -> impl Iterator + '_ { + self.values.keys().copied() + } +} + +impl JoinSemiLattice for BorrowState<'_> { + fn join_into(&mut self, other: &Self) -> bool { + debug_assert!(Rc::ptr_eq(&self.interner, &other.interner)); + let mut changed = false; + for (local, other_value) in &other.values { + let joined = self + .values + .get(local) + .copied() + .map_or(*other_value, |value| { + self.interner.borrow_mut().join(value, *other_value) + }); + if self.values.insert(*local, joined) != Some(joined) { + changed = true; + } + } + changed + } +} + +pub(crate) fn direct_loan_value<'db>( + interner: &SharedBorrowValueInterner<'db>, + shape: ShapeId<'db>, + loan: LoanId, +) -> BorrowStateValueId<'db> { + let mut interner = interner.borrow_mut(); + let empty = interner.empty(shape); + interner.with_direct(empty, LoanRef::new(loan)) +} + +pub(crate) fn summary_loan_value<'db>( + interner: &SharedBorrowValueInterner<'db>, + shape: ShapeId<'db>, + leaves: impl IntoIterator, +) -> Option> { + let db = interner.borrow().db(); + let leaves = leaves + .into_iter() + .map(|(path, loan)| Some((slot_path_for_summary(db, shape, &path)?, loan))) + .collect::>>()?; + slot_loan_value(interner, shape, leaves) +} + +pub(crate) fn slot_loan_value<'db>( + interner: &SharedBorrowValueInterner<'db>, + shape: ShapeId<'db>, + leaves: impl IntoIterator, LoanRef)>, +) -> Option> { + let leaves = leaves + .into_iter() + .map(|(path, payload)| GuardedLeaf { + path, + guard: super::guard::Guard::always(), + payload_guard: super::guard::Guard::always(), + payload, + }) + .collect::>(); + interner.borrow_mut().reconstruct(shape, &leaves) +} + +pub(crate) fn slot_path_for_summary<'db>( + db: &'db dyn HirAnalysisDb, + shape: ShapeId<'db>, + path: &SummaryPath, +) -> Option> { + let mut shape = shape; + let mut steps = Vec::with_capacity(path.as_slice().len()); + for projection in path.as_slice() { + let (step, child) = match (projection, &shape.data(db).children) { + (SummaryProjection::Field(field), ShapeChildren::Product { fields }) => { + let (key, child) = fields.iter().find(|(key, _)| key.index() == *field)?; + (SlotProjection::Field(*key), *child) + } + ( + SummaryProjection::VariantField { variant, field }, + ShapeChildren::Sum { variants }, + ) => { + let variant_shape = variants + .iter() + .find_map(|(candidate, shape)| (*candidate == *variant).then_some(*shape))?; + let ShapeChildren::Product { fields } = &variant_shape.data(db).children else { + return None; + }; + let child = fields + .iter() + .find_map(|(key, shape)| (key.index() == *field).then_some(*shape))?; + ( + SlotProjection::VariantField { + variant: *variant, + field: *field, + }, + child, + ) + } + (SummaryProjection::Index(index), ShapeChildren::Array { elem, .. }) => { + (SlotProjection::Index(*index), *elem) + } + _ => return None, + }; + steps.push(step); + shape = child; + } + Some(SlotPath::from_steps(steps)) +} + +pub(crate) fn slot_path_for_layout<'db>( + db: &'db dyn HirAnalysisDb, + shape: ShapeId<'db>, + path: &[LayoutBackingProjection], +) -> Option> { + let mut shape = shape; + let mut steps = Vec::with_capacity(path.len()); + for (depth, projection) in path.iter().copied().enumerate() { + let (step, child) = match (projection, &shape.data(db).children) { + (LayoutBackingProjection::Field(field), ShapeChildren::Product { fields }) => { + let (key, child) = fields.iter().find(|(key, _)| key.index() == field)?; + (SlotProjection::Field(*key), *child) + } + ( + LayoutBackingProjection::VariantField { variant, field }, + ShapeChildren::Sum { variants }, + ) => { + let variant_shape = variants + .iter() + .find_map(|(candidate, shape)| (*candidate == variant).then_some(*shape))?; + let ShapeChildren::Product { fields } = &variant_shape.data(db).children else { + return None; + }; + let child = fields + .iter() + .find_map(|(key, shape)| (key.index() == field).then_some(*shape))?; + (SlotProjection::VariantField { variant, field }, child) + } + (LayoutBackingProjection::Index(index), ShapeChildren::Array { elem, .. }) => ( + SlotProjection::Index(index.map_or_else( + || IndexExpr::Existential(ExistentialId(depth as u32)), + IndexExpr::Const, + )), + *elem, + ), + (LayoutBackingProjection::IndexFamily(family), ShapeChildren::Array { elem, .. }) => ( + SlotProjection::Index(IndexExpr::ResultParam(super::guard::ResultIndexId( + family as u32, + ))), + *elem, + ), + _ => return None, + }; + steps.push(step); + shape = child; + } + Some(SlotPath::from_steps(steps)) +} diff --git a/crates/hir/src/analysis/semantic/borrowck/value.rs b/crates/hir/src/analysis/semantic/borrowck/value.rs new file mode 100644 index 0000000000..790e15520f --- /dev/null +++ b/crates/hir/src/analysis/semantic/borrowck/value.rs @@ -0,0 +1,1300 @@ +use std::{collections::BTreeMap, hash::Hash, marker::PhantomData}; + +use rustc_hash::FxHashMap; + +use crate::analysis::{HirAnalysisDb, semantic::VariantIndex}; + +use super::{ + guard::{ChoiceKey, Guard, IndexExpr, IndexSubst, ValueIndexId, ValueScope}, + shape::{FieldKey, ShapeChildren, ShapeId, SlotPath, SlotProjection}, +}; + +pub(crate) trait IndexPayload: Clone + Eq + Ord + Hash { + fn substitute_indices(&self, subst: &IndexSubst) -> Self; +} + +#[derive(Debug)] +pub(crate) struct ValueId<'db, P> { + raw: u32, + marker: PhantomData P>, +} + +impl

Clone for ValueId<'_, P> { + fn clone(&self) -> Self { + *self + } +} + +impl

Copy for ValueId<'_, P> {} + +impl

PartialEq for ValueId<'_, P> { + fn eq(&self, other: &Self) -> bool { + self.raw == other.raw + } +} + +impl

Eq for ValueId<'_, P> {} + +impl

PartialOrd for ValueId<'_, P> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl

Ord for ValueId<'_, P> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.raw.cmp(&other.raw) + } +} + +impl

Hash for ValueId<'_, P> { + fn hash(&self, state: &mut H) { + self.raw.hash(state); + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct Guarded

{ + guard: Guard, + payload: P, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct GuardedSet

{ + entries: Vec>, +} + +impl

Default for GuardedSet

{ + fn default() -> Self { + Self { + entries: Vec::new(), + } + } +} + +impl GuardedSet

{ + fn singleton(payload: P) -> Self { + Self { + entries: vec![Guarded { + guard: Guard::always(), + payload, + }], + } + } + + fn union(&self, other: &Self) -> Self { + let mut entries = self.entries.clone(); + entries.extend(other.entries.iter().cloned()); + Self::normalize(entries) + } + + fn with_guard(&self, guard: &Guard) -> Self { + Self::normalize( + self.entries + .iter() + .filter_map(|entry| { + Some(Guarded { + guard: entry.guard.and(guard)?, + payload: entry.payload.clone(), + }) + }) + .collect(), + ) + } + + fn substitute(&self, subst: &IndexSubst) -> Self { + Self::normalize( + self.entries + .iter() + .filter_map(|entry| { + Some(Guarded { + guard: entry.guard.substitute(subst)?, + payload: entry.payload.substitute_indices(subst), + }) + }) + .collect(), + ) + } + + fn scoped(&self, scope: ValueScope) -> Self { + Self::normalize( + self.entries + .iter() + .map(|entry| Guarded { + guard: entry.guard.scoped(scope), + payload: entry.payload.clone(), + }) + .collect(), + ) + } + + fn normalize(mut entries: Vec>) -> Self { + entries.sort_unstable_by(|lhs, rhs| { + lhs.payload + .cmp(&rhs.payload) + .then_with(|| lhs.guard.cmp(&rhs.guard)) + }); + entries.dedup(); + + let mut normalized: Vec> = Vec::new(); + for entry in entries { + if normalized.iter().any(|existing| { + existing.payload == entry.payload && entry.guard.implies(&existing.guard) + }) { + continue; + } + normalized.retain(|existing| { + existing.payload != entry.payload || !existing.guard.implies(&entry.guard) + }); + normalized.push(entry); + } + normalized.sort_unstable_by(|lhs, rhs| { + lhs.payload + .cmp(&rhs.payload) + .then_with(|| lhs.guard.cmp(&rhs.guard)) + }); + Self { + entries: normalized, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct StructuredValue<'db, P> { + shape: ShapeId<'db>, + direct: GuardedSet

, + children: ValueChildren<'db, P>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum ValueChildren<'db, P> { + None, + Product { + fields: Box<[(FieldKey, ValueId<'db, P>)]>, + }, + Sum { + variants: Box<[(VariantIndex, ValueId<'db, P>)]>, + }, + Array { + binder: ValueIndexId, + default: ValueId<'db, P>, + exact: BTreeMap>, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct GuardedLeaf

{ + pub(crate) path: SlotPath, + pub(crate) guard: Guard, + pub(crate) payload_guard: Guard, + pub(crate) payload: P, +} + +pub(crate) struct ValueInterner<'db, P> { + db: &'db dyn HirAnalysisDb, + nodes: Vec>, + interned: FxHashMap, ValueId<'db, P>>, + empty: FxHashMap, ValueId<'db, P>>, + next_binder: u32, +} + +impl<'db, P: IndexPayload> ValueInterner<'db, P> { + pub(crate) fn new(db: &'db dyn HirAnalysisDb) -> Self { + Self { + db, + nodes: Vec::new(), + interned: FxHashMap::default(), + empty: FxHashMap::default(), + next_binder: 0, + } + } + + pub(crate) fn db(&self) -> &'db dyn HirAnalysisDb { + self.db + } + + pub(crate) fn empty(&mut self, shape: ShapeId<'db>) -> ValueId<'db, P> { + if let Some(value) = self.empty.get(&shape) { + return *value; + } + let children = match &shape.data(self.db).children { + ShapeChildren::None => ValueChildren::None, + ShapeChildren::Product { fields } => ValueChildren::Product { + fields: fields + .iter() + .map(|(field, shape)| (*field, self.empty(*shape))) + .collect(), + }, + ShapeChildren::Sum { variants } => ValueChildren::Sum { + variants: variants + .iter() + .map(|(variant, shape)| (*variant, self.empty(*shape))) + .collect(), + }, + ShapeChildren::Array { elem, .. } => ValueChildren::Array { + binder: self.fresh_binder(), + default: self.empty(*elem), + exact: BTreeMap::new(), + }, + }; + let value = self.intern(StructuredValue { + shape, + direct: GuardedSet::default(), + children, + }); + self.empty.insert(shape, value); + value + } + + pub(crate) fn with_direct(&mut self, value: ValueId<'db, P>, payload: P) -> ValueId<'db, P> { + let mut node = self.node(value).clone(); + node.direct = GuardedSet::singleton(payload); + self.intern(node) + } + + pub(crate) fn product( + &mut self, + shape: ShapeId<'db>, + fields: impl IntoIterator)>, + ) -> ValueId<'db, P> { + let fields = fields.into_iter().collect::>().into_boxed_slice(); + debug_assert!(matches!( + &shape.data(self.db).children, + ShapeChildren::Product { fields: expected } + if expected.iter().map(|(field, _)| field).eq(fields.iter().map(|(field, _)| field)) + )); + self.intern(StructuredValue { + shape, + direct: GuardedSet::default(), + children: ValueChildren::Product { fields }, + }) + } + + pub(crate) fn sum_variant( + &mut self, + shape: ShapeId<'db>, + variant: VariantIndex, + value: ValueId<'db, P>, + ) -> ValueId<'db, P> { + debug_assert!(matches!( + &shape.data(self.db).children, + ShapeChildren::Sum { variants } + if variants.iter().any(|(candidate, _)| *candidate == variant) + )); + self.intern(StructuredValue { + shape, + direct: GuardedSet::default(), + children: ValueChildren::Sum { + variants: vec![(variant, value)].into_boxed_slice(), + }, + }) + } + + pub(crate) fn array_repeat( + &mut self, + shape: ShapeId<'db>, + default: ValueId<'db, P>, + ) -> ValueId<'db, P> { + debug_assert!(matches!( + &shape.data(self.db).children, + ShapeChildren::Array { elem, .. } if *elem == self.node(default).shape + )); + let binder = self.fresh_binder(); + self.intern(StructuredValue { + shape, + direct: GuardedSet::default(), + children: ValueChildren::Array { + binder, + default, + exact: BTreeMap::new(), + }, + }) + } + + pub(crate) fn array_exact( + &mut self, + shape: ShapeId<'db>, + exact: impl IntoIterator)>, + ) -> ValueId<'db, P> { + let ShapeChildren::Array { len, elem } = &shape.data(self.db).children else { + panic!("array value requires an array shape"); + }; + let exact = exact + .into_iter() + .filter(|(index, value)| *index < *len && self.node(*value).shape == *elem) + .collect(); + let default = self.empty(*elem); + let binder = self.fresh_binder(); + self.intern(StructuredValue { + shape, + direct: GuardedSet::default(), + children: ValueChildren::Array { + binder, + default, + exact, + }, + }) + } + + pub(crate) fn join(&mut self, lhs: ValueId<'db, P>, rhs: ValueId<'db, P>) -> ValueId<'db, P> { + if lhs == rhs { + return lhs; + } + let lhs_node = self.node(lhs).clone(); + let mut rhs_node = self.node(rhs).clone(); + assert_eq!(lhs_node.shape, rhs_node.shape, "value join shape mismatch"); + let direct = lhs_node.direct.union(&rhs_node.direct); + let children = match (lhs_node.children, &mut rhs_node.children) { + (ValueChildren::None, ValueChildren::None) => ValueChildren::None, + (ValueChildren::Product { fields: lhs }, ValueChildren::Product { fields: rhs }) => { + ValueChildren::Product { + fields: lhs + .iter() + .zip(rhs.iter()) + .map(|((lhs_key, lhs), (rhs_key, rhs))| { + assert_eq!(lhs_key, rhs_key, "product join field mismatch"); + (*lhs_key, self.join(*lhs, *rhs)) + }) + .collect(), + } + } + (ValueChildren::Sum { variants: lhs }, ValueChildren::Sum { variants: rhs }) => { + let lhs = lhs.into_iter().collect::>(); + let rhs = rhs.iter().copied().collect::>(); + let variants = lhs + .keys() + .chain(rhs.keys()) + .copied() + .collect::>() + .into_iter() + .filter_map(|variant| match (lhs.get(&variant), rhs.get(&variant)) { + (Some(lhs), Some(rhs)) => Some((variant, self.join(*lhs, *rhs))), + (Some(value), None) | (None, Some(value)) => Some((variant, *value)), + (None, None) => None, + }) + .collect(); + ValueChildren::Sum { variants } + } + ( + ValueChildren::Array { + binder: lhs_binder, + default: lhs_default, + exact: lhs_exact, + }, + ValueChildren::Array { + binder: rhs_binder, + default: rhs_default, + exact: rhs_exact, + }, + ) => { + let subst = IndexSubst::from_pair( + IndexExpr::ValueParam(*rhs_binder), + IndexExpr::ValueParam(lhs_binder), + ); + let rhs_default = self.substitute(*rhs_default, &subst); + let rhs_exact = rhs_exact + .iter() + .map(|(index, value)| (*index, self.substitute(*value, &subst))) + .collect::>(); + let default = self.join(lhs_default, rhs_default); + let keys = lhs_exact + .keys() + .chain(rhs_exact.keys()) + .copied() + .collect::>(); + let mut exact = BTreeMap::new(); + for index in keys { + let lhs = lhs_exact + .get(&index) + .copied() + .unwrap_or_else(|| self.specialize_default(lhs_default, lhs_binder, index)); + let rhs = rhs_exact + .get(&index) + .copied() + .unwrap_or_else(|| self.specialize_default(rhs_default, lhs_binder, index)); + let joined = self.join(lhs, rhs); + if joined != self.specialize_default(default, lhs_binder, index) { + exact.insert(index, joined); + } + } + ValueChildren::Array { + binder: lhs_binder, + default, + exact, + } + } + _ => panic!("value join structure mismatch"), + }; + self.intern(StructuredValue { + shape: lhs_node.shape, + direct, + children, + }) + } + + pub(crate) fn project( + &mut self, + value: ValueId<'db, P>, + path: &SlotPath, + scope: ValueScope, + ) -> ValueId<'db, P> { + self.project_from(value, path.as_slice(), scope, &mut SlotPath::new()) + } + + fn project_from( + &mut self, + value: ValueId<'db, P>, + path: &[SlotProjection], + scope: ValueScope, + traversed: &mut SlotPath, + ) -> ValueId<'db, P> { + let Some((projection, suffix)) = path.split_first() else { + return value; + }; + let node = self.node(value).clone(); + let selected = match (projection, node.children) { + (SlotProjection::Field(field), ValueChildren::Product { fields }) => fields + .iter() + .find_map(|(candidate, value)| (*candidate == *field).then_some(*value)) + .expect("projected field must exist"), + (SlotProjection::VariantField { variant, field }, ValueChildren::Sum { variants }) => { + let variant_value = variants + .iter() + .find_map(|(candidate, value)| (*candidate == *variant).then_some(*value)) + .unwrap_or_else(|| { + let ShapeChildren::Sum { variants } = &node.shape.data(self.db).children + else { + unreachable!() + }; + let shape = variants + .iter() + .find_map(|(candidate, shape)| { + (*candidate == *variant).then_some(*shape) + }) + .expect("projected variant must exist"); + self.empty(shape) + }); + let ValueChildren::Product { fields } = &self.node(variant_value).children else { + panic!("enum variant value must be a product"); + }; + let selected = fields + .iter() + .find_map(|(candidate, value)| (candidate.index() == *field).then_some(*value)) + .expect("projected variant field must exist"); + let guard = Guard::always() + .with_variant( + ChoiceKey::relative(traversed.clone()).scoped(scope), + *variant, + ) + .expect("one variant selection is satisfiable"); + self.with_guard(selected, &guard) + } + ( + SlotProjection::Index(index), + ValueChildren::Array { + binder, + default, + exact, + }, + ) => self.project_array(node.shape, binder, default, &exact, *index), + _ => panic!("value projection does not match shape"), + }; + traversed.push(projection.clone()); + let projected = self.project_from(selected, suffix, scope, traversed); + traversed.pop(); + projected + } + + fn project_array( + &mut self, + shape: ShapeId<'db>, + binder: ValueIndexId, + default: ValueId<'db, P>, + exact: &BTreeMap>, + index: IndexExpr, + ) -> ValueId<'db, P> { + let ShapeChildren::Array { len, .. } = shape.data(self.db).children else { + unreachable!() + }; + if let IndexExpr::Const(index) = index { + assert!(index < len, "array projection is in bounds"); + return exact + .get(&index) + .copied() + .unwrap_or_else(|| self.specialize_default(default, binder, index)); + } + + let mut alternatives = Vec::new(); + for (exact_index, value) in exact { + if let Some(guard) = Guard::equal(index, IndexExpr::Const(*exact_index)) { + alternatives.push(self.with_guard(*value, &guard)); + } + } + let subst = IndexSubst::from_pair(IndexExpr::ValueParam(binder), index); + if let Some(fallback_guard) = exact.keys().try_fold( + Guard::bounded(index, len).expect("symbolic bound is satisfiable"), + |guard, exact_index| guard.with_disequality(index, IndexExpr::Const(*exact_index)), + ) { + let fallback = self.substitute(default, &subst); + alternatives.push(self.with_guard(fallback, &fallback_guard)); + } + alternatives + .into_iter() + .reduce(|lhs, rhs| self.join(lhs, rhs)) + .expect("array projection has a fallback") + } + + pub(crate) fn replace( + &mut self, + value: ValueId<'db, P>, + path: &SlotPath, + replacement: ValueId<'db, P>, + ) -> ValueId<'db, P> { + self.replace_from(value, path.as_slice(), replacement) + } + + fn replace_from( + &mut self, + value: ValueId<'db, P>, + path: &[SlotProjection], + replacement: ValueId<'db, P>, + ) -> ValueId<'db, P> { + let Some((projection, suffix)) = path.split_first() else { + assert_eq!( + self.node(value).shape, + self.node(replacement).shape, + "replacement shape mismatch" + ); + return replacement; + }; + let mut node = self.node(value).clone(); + node.children = match (projection, node.children) { + (SlotProjection::Field(field), ValueChildren::Product { mut fields }) => { + let (_, selected) = fields + .iter_mut() + .find(|(candidate, _)| candidate == field) + .expect("replaced field must exist"); + *selected = self.replace_from(*selected, suffix, replacement); + ValueChildren::Product { fields } + } + (SlotProjection::VariantField { variant, field }, ValueChildren::Sum { variants }) => { + let shape_variants = match &node.shape.data(self.db).children { + ShapeChildren::Sum { variants } => variants, + _ => unreachable!(), + }; + let mut variants = variants.into_vec(); + let position = variants + .iter() + .position(|(candidate, _)| candidate == variant); + let variant_value = position.map_or_else( + || { + let shape = shape_variants + .iter() + .find_map(|(candidate, shape)| (candidate == variant).then_some(*shape)) + .expect("replaced variant must exist"); + self.empty(shape) + }, + |position| variants[position].1, + ); + let mut variant_node = self.node(variant_value).clone(); + let ValueChildren::Product { mut fields } = variant_node.children else { + panic!("enum variant value must be a product"); + }; + let (_, selected) = fields + .iter_mut() + .find(|(candidate, _)| candidate.index() == *field) + .expect("replaced variant field must exist"); + *selected = self.replace_from(*selected, suffix, replacement); + variant_node.children = ValueChildren::Product { fields }; + let variant_value = self.intern(variant_node); + if let Some(position) = position { + variants[position].1 = variant_value; + } else { + variants.push((*variant, variant_value)); + variants.sort_unstable_by_key(|(variant, _)| *variant); + } + ValueChildren::Sum { + variants: variants.into_boxed_slice(), + } + } + (SlotProjection::Index(index), children @ ValueChildren::Array { .. }) => { + self.replace_array(node.shape, children, *index, suffix, replacement) + } + _ => panic!("value replacement does not match shape"), + }; + self.intern(node) + } + + fn replace_array( + &mut self, + shape: ShapeId<'db>, + children: ValueChildren<'db, P>, + index: IndexExpr, + suffix: &[SlotProjection], + replacement: ValueId<'db, P>, + ) -> ValueChildren<'db, P> { + let ValueChildren::Array { + binder, + default, + mut exact, + } = children + else { + unreachable!() + }; + let ShapeChildren::Array { len, .. } = shape.data(self.db).children else { + unreachable!() + }; + if let IndexExpr::Const(index) = index { + if index >= len { + return ValueChildren::Array { + binder, + default, + exact, + }; + } + let current = exact + .get(&index) + .copied() + .unwrap_or_else(|| self.specialize_default(default, binder, index)); + let replacement = self.replace_from(current, suffix, replacement); + let specialized_default = self.specialize_default(default, binder, index); + if replacement == specialized_default { + exact.remove(&index); + } else { + exact.insert(index, replacement); + } + return ValueChildren::Array { + binder, + default, + exact, + }; + } + + let default_replacement = self.replace_from(default, suffix, replacement); + let old_guard = Guard::not_equal(IndexExpr::ValueParam(binder), index) + .expect("symbolic array indices may differ"); + let new_guard = Guard::equal(IndexExpr::ValueParam(binder), index) + .expect("symbolic array indices may be equal"); + let default = { + let old = self.with_guard(default, &old_guard); + let new = self.with_guard(default_replacement, &new_guard); + self.join(old, new) + }; + for (exact_index, value) in &mut exact { + let current = *value; + let updated = self.replace_from(current, suffix, replacement); + let old_guard = Guard::not_equal(index, IndexExpr::Const(*exact_index)) + .expect("symbolic and exact index may differ"); + let new_guard = Guard::equal(index, IndexExpr::Const(*exact_index)) + .expect("symbolic and exact index may match"); + let old = self.with_guard(current, &old_guard); + let new = self.with_guard(updated, &new_guard); + *value = self.join(old, new); + } + ValueChildren::Array { + binder, + default, + exact, + } + } + + pub(crate) fn with_guard(&mut self, value: ValueId<'db, P>, guard: &Guard) -> ValueId<'db, P> { + let node = self.node(value).clone(); + let children = match node.children { + ValueChildren::None => ValueChildren::None, + ValueChildren::Product { fields } => ValueChildren::Product { + fields: fields + .iter() + .map(|(field, value)| (*field, self.with_guard(*value, guard))) + .collect(), + }, + ValueChildren::Sum { variants } => ValueChildren::Sum { + variants: variants + .iter() + .map(|(variant, value)| (*variant, self.with_guard(*value, guard))) + .collect(), + }, + ValueChildren::Array { + binder, + default, + exact, + } => ValueChildren::Array { + binder, + default: self.with_guard(default, guard), + exact: exact + .into_iter() + .map(|(index, value)| (index, self.with_guard(value, guard))) + .collect(), + }, + }; + self.intern(StructuredValue { + shape: node.shape, + direct: node.direct.with_guard(guard), + children, + }) + } + + pub(crate) fn substitute( + &mut self, + value: ValueId<'db, P>, + subst: &IndexSubst, + ) -> ValueId<'db, P> { + if subst.is_empty() { + return value; + } + let node = self.node(value).clone(); + let children = match node.children { + ValueChildren::None => ValueChildren::None, + ValueChildren::Product { fields } => ValueChildren::Product { + fields: fields + .iter() + .map(|(field, value)| (*field, self.substitute(*value, subst))) + .collect(), + }, + ValueChildren::Sum { variants } => ValueChildren::Sum { + variants: variants + .iter() + .map(|(variant, value)| (*variant, self.substitute(*value, subst))) + .collect(), + }, + ValueChildren::Array { + binder, + default, + exact, + } => ValueChildren::Array { + binder, + default: self.substitute(default, subst), + exact: exact + .into_iter() + .map(|(index, value)| (index, self.substitute(value, subst))) + .collect(), + }, + }; + self.intern(StructuredValue { + shape: node.shape, + direct: node.direct.substitute(subst), + children, + }) + } + + pub(crate) fn enumerate_leaves( + &self, + value: ValueId<'db, P>, + scope: ValueScope, + ) -> Vec> { + let mut leaves = Vec::new(); + self.enumerate_from( + value, + scope, + &mut SlotPath::new(), + &Guard::always(), + &mut leaves, + ); + leaves.sort_unstable_by(|lhs, rhs| { + lhs.path + .cmp(&rhs.path) + .then_with(|| lhs.payload.cmp(&rhs.payload)) + .then_with(|| lhs.guard.cmp(&rhs.guard)) + }); + leaves + } + + pub(crate) fn reconstruct( + &mut self, + shape: ShapeId<'db>, + leaves: &[GuardedLeaf

], + ) -> Option> { + self.build_from_leaves(shape, leaves, &IndexSubst::new()) + } + + fn build_from_leaves( + &mut self, + shape: ShapeId<'db>, + leaves: &[GuardedLeaf

], + subst: &IndexSubst, + ) -> Option> { + let direct = GuardedSet::normalize( + leaves + .iter() + .filter(|leaf| leaf.path.is_empty()) + .filter_map(|leaf| { + Some(Guarded { + guard: leaf.payload_guard.substitute(subst)?, + payload: leaf.payload.substitute_indices(subst), + }) + }) + .collect(), + ); + let children = match &shape.data(self.db).children { + ShapeChildren::None => ValueChildren::None, + ShapeChildren::Product { fields } => ValueChildren::Product { + fields: fields + .iter() + .map(|(field, child_shape)| { + let children = strip_matching_leaves(leaves, |projection| { + matches!(projection, SlotProjection::Field(candidate) if candidate == field) + }); + Some((*field, self.build_from_leaves(*child_shape, &children, subst)?)) + }) + .collect::>>()?, + }, + ShapeChildren::Sum { variants } => ValueChildren::Sum { + variants: variants + .iter() + .map(|(variant, variant_shape)| { + let ShapeChildren::Product { fields } = + &variant_shape.data(self.db).children + else { + return None; + }; + let variant_leaves = leaves + .iter() + .filter_map(|leaf| { + let (projection, suffix) = leaf.path.as_slice().split_first()?; + let SlotProjection::VariantField { + variant: candidate, + field, + } = projection + else { + return None; + }; + (*candidate == *variant).then(|| { + let key = fields + .iter() + .find_map(|(key, _)| { + (key.index() == *field).then_some(*key) + }) + .expect("summary variant field must match its shape"); + let mut path = SlotPath::new(); + path.push(SlotProjection::Field(key)); + for projection in suffix { + path.push(projection.clone()); + } + GuardedLeaf { + path, + guard: leaf.guard.clone(), + payload_guard: leaf.payload_guard.clone(), + payload: leaf.payload.clone(), + } + }) + }) + .collect::>(); + Some(( + *variant, + self.build_from_leaves(*variant_shape, &variant_leaves, subst)?, + )) + }) + .collect::>>()?, + }, + ShapeChildren::Array { elem, .. } => { + let symbolic = leaves + .iter() + .filter_map(|leaf| match leaf.path.as_slice().first() { + Some(SlotProjection::Index(index)) + if !matches!(index, IndexExpr::Const(_)) => + { + Some(*index) + } + _ => None, + }) + .collect::>(); + if symbolic.len() > 1 { + return None; + } + let binder = self.fresh_binder(); + let default = if let Some(index) = symbolic.first().copied() { + let children = strip_matching_leaves(leaves, |projection| { + matches!(projection, SlotProjection::Index(candidate) if *candidate == index) + }); + let mut default_subst = subst.clone(); + default_subst.insert(index, IndexExpr::ValueParam(binder)); + self.build_from_leaves(*elem, &children, &default_subst)? + } else { + self.empty(*elem) + }; + let exact = leaves + .iter() + .filter_map(|leaf| match leaf.path.as_slice().first() { + Some(SlotProjection::Index(IndexExpr::Const(index))) => Some(*index), + _ => None, + }) + .collect::>() + .into_iter() + .map(|index| { + let children = strip_matching_leaves(leaves, |projection| { + matches!(projection, SlotProjection::Index(IndexExpr::Const(candidate)) if *candidate == index) + }); + Some((index, self.build_from_leaves(*elem, &children, subst)?)) + }) + .collect::>>()?; + ValueChildren::Array { + binder, + default, + exact, + } + } + }; + Some(self.intern(StructuredValue { + shape, + direct, + children, + })) + } + + fn enumerate_from( + &self, + value: ValueId<'db, P>, + scope: ValueScope, + path: &mut SlotPath, + inherited_guard: &Guard, + out: &mut Vec>, + ) { + let node = self.node(value); + for entry in &node.direct.scoped(scope).entries { + if let Some(guard) = inherited_guard.and(&entry.guard) { + out.push(GuardedLeaf { + path: path.clone(), + guard, + payload_guard: entry.guard.clone(), + payload: entry.payload.clone(), + }); + } + } + match &node.children { + ValueChildren::None => {} + ValueChildren::Product { fields } => { + for (field, value) in fields { + path.push(SlotProjection::Field(*field)); + self.enumerate_from(*value, scope, path, inherited_guard, out); + path.pop(); + } + } + ValueChildren::Sum { variants } => { + let choice = ChoiceKey::relative(path.clone()).scoped(scope); + for (variant, value) in variants { + let Some(guard) = inherited_guard.with_variant(choice.clone(), *variant) else { + continue; + }; + let ValueChildren::Product { fields } = &self.node(*value).children else { + continue; + }; + for (field, value) in fields { + path.push(SlotProjection::VariantField { + variant: *variant, + field: field.index(), + }); + self.enumerate_from(*value, scope, path, &guard, out); + path.pop(); + } + } + } + ValueChildren::Array { + binder, + default, + exact, + } => { + let ShapeChildren::Array { len, .. } = node.shape.data(self.db).children else { + unreachable!() + }; + let index = IndexExpr::ValueParam(*binder); + let guard = exact.keys().try_fold( + inherited_guard + .with_bound(index, len) + .expect("array binder bound is satisfiable"), + |guard, exact_index| { + guard.with_disequality(index, IndexExpr::Const(*exact_index)) + }, + ); + if let Some(guard) = guard { + path.push(SlotProjection::Index(index)); + self.enumerate_from(*default, scope, path, &guard, out); + path.pop(); + } + for (exact_index, value) in exact { + path.push(SlotProjection::Index(IndexExpr::Const(*exact_index))); + self.enumerate_from(*value, scope, path, inherited_guard, out); + path.pop(); + } + } + } + } + + #[cfg(test)] + pub(crate) fn node_count(&self) -> usize { + self.nodes.len() + } + + pub(crate) fn is_empty(&self, value: ValueId<'db, P>) -> bool { + self.enumerate_leaves(value, ValueScope::Relative) + .is_empty() + } + + pub(crate) fn shape(&self, value: ValueId<'db, P>) -> ShapeId<'db> { + self.node(value).shape + } + + fn specialize_default( + &mut self, + default: ValueId<'db, P>, + binder: ValueIndexId, + index: usize, + ) -> ValueId<'db, P> { + self.substitute( + default, + &IndexSubst::from_pair(IndexExpr::ValueParam(binder), IndexExpr::Const(index)), + ) + } + + fn fresh_binder(&mut self) -> ValueIndexId { + let binder = ValueIndexId(self.next_binder); + self.next_binder = self + .next_binder + .checked_add(1) + .expect("structural value binder space exhausted"); + binder + } + + fn node(&self, value: ValueId<'db, P>) -> &StructuredValue<'db, P> { + &self.nodes[value.raw as usize] + } + + fn intern(&mut self, value: StructuredValue<'db, P>) -> ValueId<'db, P> { + if let Some(id) = self.interned.get(&value) { + return *id; + } + let id = ValueId { + raw: u32::try_from(self.nodes.len()).expect("structural value id space exhausted"), + marker: PhantomData, + }; + self.nodes.push(value.clone()); + self.interned.insert(value, id); + id + } +} + +fn strip_matching_leaves( + leaves: &[GuardedLeaf

], + predicate: impl Fn(&SlotProjection) -> bool, +) -> Vec> { + leaves + .iter() + .filter_map(|leaf| { + let (projection, suffix) = leaf.path.as_slice().split_first()?; + predicate(projection).then(|| GuardedLeaf { + path: SlotPath::from_steps(suffix.iter().cloned()), + guard: leaf.guard.clone(), + payload_guard: leaf.payload_guard.clone(), + payload: leaf.payload.clone(), + }) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use super::*; + use crate::{analysis::ty::ty_def::BorrowKind, test_db::HirAnalysisTestDb}; + + use super::super::shape::{CapabilityLeafKind, CapabilityShape}; + + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + struct Payload(u8); + + impl IndexPayload for Payload { + fn substitute_indices(&self, _subst: &IndexSubst) -> Self { + *self + } + } + + fn leaf_shape<'db>(db: &'db HirAnalysisTestDb) -> ShapeId<'db> { + ShapeId::new( + db, + CapabilityShape { + direct: Some(CapabilityLeafKind::Borrow(BorrowKind::Mut)), + children: ShapeChildren::None, + }, + ) + } + + fn array_shape<'db>( + db: &'db HirAnalysisTestDb, + elem: ShapeId<'db>, + len: usize, + ) -> ShapeId<'db> { + ShapeId::new( + db, + CapabilityShape { + direct: None, + children: ShapeChildren::Array { len, elem }, + }, + ) + } + + fn index_path(index: IndexExpr) -> SlotPath { + SlotPath::from_steps([SlotProjection::Index(index)]) + } + + #[test] + fn exact_array_replacement_shadows_only_that_member() { + let db = HirAnalysisTestDb::default(); + let leaf = leaf_shape(&db); + let array = array_shape(&db, leaf, 1_000_000); + let mut values = ValueInterner::new(&db); + let empty = values.empty(leaf); + let old = values.with_direct(empty, Payload(0)); + let replacement = values.with_direct(empty, Payload(1)); + let original = values.array_repeat(array, old); + let replaced = values.replace(original, &index_path(IndexExpr::Const(0)), replacement); + + let scope = ValueScope::Local(crate::analysis::semantic::SLocalId::from_u32(0)); + let first = values.project(replaced, &index_path(IndexExpr::Const(0)), scope); + assert_eq!( + values + .enumerate_leaves(first, scope) + .into_iter() + .map(|leaf| leaf.payload) + .collect::>(), + vec![Payload(1)] + ); + let second = values.project(replaced, &index_path(IndexExpr::Const(1)), scope); + assert_eq!( + values + .enumerate_leaves(second, scope) + .into_iter() + .map(|leaf| leaf.payload) + .collect::>(), + vec![Payload(0)] + ); + assert!(values.node_count() < 32); + } + + #[test] + fn array_join_restores_branch_local_old_member() { + let db = HirAnalysisTestDb::default(); + let leaf = leaf_shape(&db); + let array = array_shape(&db, leaf, 2); + let mut values = ValueInterner::new(&db); + let empty = values.empty(leaf); + let old = values.with_direct(empty, Payload(0)); + let replacement = values.with_direct(empty, Payload(1)); + let original = values.array_repeat(array, old); + let replaced = values.replace(original, &index_path(IndexExpr::Const(0)), replacement); + let joined = values.join(original, replaced); + let selected = values.project( + joined, + &index_path(IndexExpr::Const(0)), + ValueScope::Local(crate::analysis::semantic::SLocalId::from_u32(0)), + ); + let payloads = values + .enumerate_leaves( + selected, + ValueScope::Local(crate::analysis::semantic::SLocalId::from_u32(0)), + ) + .into_iter() + .map(|leaf| leaf.payload) + .collect::>(); + + assert_eq!(payloads, vec![Payload(0), Payload(1)]); + } + + #[test] + fn out_of_bounds_array_replacement_is_unreachable() { + let db = HirAnalysisTestDb::default(); + let leaf = leaf_shape(&db); + let array = array_shape(&db, leaf, 2); + let mut values = ValueInterner::new(&db); + let empty = values.empty(leaf); + let old = values.with_direct(empty, Payload(0)); + let original = values.array_repeat(array, old); + let replacement = values.with_direct(empty, Payload(1)); + + assert_eq!( + values.replace( + original, + &index_path(IndexExpr::Const(usize::MAX)), + replacement, + ), + original + ); + } + + #[test] + fn structurally_equal_values_are_interned() { + let db = HirAnalysisTestDb::default(); + let leaf = leaf_shape(&db); + let mut values = ValueInterner::new(&db); + let empty = values.empty(leaf); + + assert_eq!( + values.with_direct(empty, Payload(7)), + values.with_direct(empty, Payload(7)) + ); + } + + #[test] + fn dynamic_array_projection_matches_each_concrete_index() { + let db = HirAnalysisTestDb::default(); + let leaf = leaf_shape(&db); + let array = array_shape(&db, leaf, 3); + let mut values = ValueInterner::new(&db); + let empty = values.empty(leaf); + let old = values.with_direct(empty, Payload(0)); + let replacement = values.with_direct(empty, Payload(1)); + let original = values.array_repeat(array, old); + let replaced = values.replace(original, &index_path(IndexExpr::Const(0)), replacement); + let index = IndexExpr::Runtime(crate::analysis::semantic::SLocalId::from_u32(1)); + let projected = values.project( + replaced, + &index_path(index), + ValueScope::Local(crate::analysis::semantic::SLocalId::from_u32(0)), + ); + let leaves = values.enumerate_leaves( + projected, + ValueScope::Local(crate::analysis::semantic::SLocalId::from_u32(0)), + ); + + for concrete in 0..3 { + let expected = if concrete == 0 { + Payload(1) + } else { + Payload(0) + }; + let actual = leaves + .iter() + .filter(|leaf| { + leaf.guard + .with_equality(index, IndexExpr::Const(concrete)) + .is_some() + }) + .map(|leaf| leaf.payload) + .collect::>(); + assert_eq!(actual, BTreeSet::from([expected])); + } + } + + #[test] + fn join_is_idempotent_commutative_and_associative() { + let db = HirAnalysisTestDb::default(); + let leaf = leaf_shape(&db); + let array = array_shape(&db, leaf, 3); + let mut values = ValueInterner::new(&db); + let empty = values.empty(leaf); + let original = values.with_direct(empty, Payload(0)); + let first = values.with_direct(empty, Payload(1)); + let second = values.with_direct(empty, Payload(2)); + let base = values.array_repeat(array, original); + let left = values.replace(base, &index_path(IndexExpr::Const(0)), first); + let right = values.replace(base, &index_path(IndexExpr::Const(1)), second); + + assert_eq!(values.join(left, left), left); + let left_right = values.join(left, right); + let right_left = values.join(right, left); + assert_eq!(left_right, right_left); + let base_left = values.join(base, left); + let left_associative = values.join(base_left, right); + let right_associative = values.join(base, left_right); + assert_eq!(left_associative, right_associative); + } +} diff --git a/crates/hir/src/analysis/semantic/borrowck/verify.rs b/crates/hir/src/analysis/semantic/borrowck/verify.rs index 9048b1729c..52a08550d9 100644 --- a/crates/hir/src/analysis/semantic/borrowck/verify.rs +++ b/crates/hir/src/analysis/semantic/borrowck/verify.rs @@ -3,7 +3,11 @@ use cranelift_entity::EntityRef; use crate::{ analysis::{ HirAnalysisDb, - semantic::{NOperand, NSLocal, SLocalId, SemOrigin, SemanticInstance, SemanticLocalKind}, + semantic::{ + BorrowActivation, NOperand, NSLocal, SLocalId, SemOrigin, SemanticInstance, + SemanticLocalKind, + }, + ty::{ty_check::BodyOwner, ty_def::BorrowKind}, }, projection::{IndexSource, Projection}, }; @@ -22,6 +26,24 @@ pub fn verify_normalized_semantic_body<'db>( instance: SemanticInstance<'db>, body: &NormalizedSemanticBody<'db>, ) -> Result<(), SemanticBorrowDiagnostic<'db>> { + let receiver_reservations = body + .blocks + .iter() + .flat_map(|block| &block.stmts) + .filter_map(|stmt| match &stmt.kind { + NSStmtKind::Assign { + expr: NExpr::Call { callee, args, .. }, + .. + } if matches!( + callee.key.owner(db), + BodyOwner::Func(func) if func.receiver_ty(db).is_some() + ) => + { + args.first().map(|receiver| receiver.local) + } + NSStmtKind::Assign { .. } | NSStmtKind::Store { .. } => None, + }) + .collect::>(); for (local_idx, local) in body.locals.iter().enumerate() { let local_id = SLocalId::from_u32(local_idx as u32); let verify_rooted_place = |place: &NSPlace<'db>, label: &str| { @@ -104,6 +126,22 @@ pub fn verify_normalized_semantic_body<'db>( match &stmt.kind { NSStmtKind::Assign { dst, expr } => { verify_local_exists(db, instance, body, stmt.origin, *dst)?; + if let NExpr::Borrow { + kind, + activation: BorrowActivation::AtCall, + .. + } = expr + && (*kind != BorrowKind::Mut || !receiver_reservations.contains(dst)) + { + return Err(normalized_body_internal_diag( + db, + instance, + body, + stmt.origin, + "call-activated borrow is not a mutable receiver reservation" + .to_string(), + )); + } verify_expr(db, instance, body, stmt.origin, expr)?; } NSStmtKind::Store { dst, src } => { diff --git a/crates/hir/src/analysis/semantic/ir.rs b/crates/hir/src/analysis/semantic/ir.rs index 34bcafb6d0..589b9edffa 100644 --- a/crates/hir/src/analysis/semantic/ir.rs +++ b/crates/hir/src/analysis/semantic/ir.rs @@ -119,7 +119,12 @@ pub enum PlaceProvenance<'db> { Derived(SPlace<'db>), } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Update)] +/// Identity of one symbolic fixed-array borrow-slot family. +/// +/// Family ids are local to a single borrow-result traversal. +pub type BorrowSlotFamilyId = usize; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Update)] pub enum LayoutBackingProjection { Field(FieldIndex), VariantField { @@ -127,6 +132,8 @@ pub enum LayoutBackingProjection { field: FieldIndex, }, Index(Option), + /// A symbolic member of an indexed borrow-slot family. + IndexFamily(BorrowSlotFamilyId), } /// Physical place provenance for layout-bearing value projections. @@ -384,6 +391,12 @@ pub enum SOperandOrigin { Synthetic, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Update)] +pub enum BorrowActivation { + Immediate, + AtCall, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Update)] pub struct SOperand { pub value: SValueId, @@ -470,6 +483,7 @@ pub enum SExpr<'db> { place: SPlace<'db>, kind: BorrowKind, provider: Option, + activation: BorrowActivation, }, GetEnumTag { value: SOperand, diff --git a/crates/hir/src/analysis/semantic/lower/body.rs b/crates/hir/src/analysis/semantic/lower/body.rs index 3cbd8a915a..e1d8f2a36e 100644 --- a/crates/hir/src/analysis/semantic/lower/body.rs +++ b/crates/hir/src/analysis/semantic/lower/body.rs @@ -10,12 +10,12 @@ use crate::{ analysis::{ HirAnalysisDb, semantic::{ - CallSiteId, FieldIndex, LayoutBackingPlace, LayoutBackingSource, Mutability, SBlock, - SBlockId, SConst, SExpr, SLocal, SLocalId, SOperand, SPlace, SStmt, SStmtId, SStmtKind, - STerminator, STerminatorKind, SValueId, SemConstValue, SemOrigin, SemanticBody, - SemanticCodeRegionTarget, SemanticLocalRole, VariantIndex, bool_const, bytes_const, - int_const, reify_runtime_const_for_ty, runtime_size_bytes, sem_const_from_ty, - unit_const, + BorrowActivation, CallSiteId, FieldIndex, LayoutBackingPlace, LayoutBackingSource, + Mutability, SBlock, SBlockId, SConst, SExpr, SLocal, SLocalId, SOperand, SPlace, SStmt, + SStmtId, SStmtKind, STerminator, STerminatorKind, SValueId, SemConstValue, SemOrigin, + SemanticBody, SemanticCodeRegionTarget, SemanticLocalRole, VariantIndex, bool_const, + bytes_const, int_const, reify_runtime_const_for_ty, runtime_size_bytes, + sem_const_from_ty, unit_const, }, ty::{ const_ty::{ @@ -560,6 +560,7 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { place, kind, provider: self.typed_body.expr_prop(self.db, expr).borrow_provider, + activation: BorrowActivation::Immediate, }, ) } @@ -1147,6 +1148,11 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { place, kind: plan.kind, provider: receiver_prop.borrow_provider, + activation: if plan.kind == BorrowKind::Mut { + BorrowActivation::AtCall + } else { + BorrowActivation::Immediate + }, }, ); } diff --git a/crates/hir/src/analysis/ty/mod.rs b/crates/hir/src/analysis/ty/mod.rs index f3bc16592c..1a96072976 100644 --- a/crates/hir/src/analysis/ty/mod.rs +++ b/crates/hir/src/analysis/ty/mod.rs @@ -200,7 +200,7 @@ fn copy_impl_self_may_match<'db>( impl_base == target_base } -pub fn ty_is_noesc<'db>(db: &'db dyn HirAnalysisDb, ty: TyId<'db>) -> bool { +fn ty_contains_noesc_capability<'db>(db: &'db dyn HirAnalysisDb, ty: TyId<'db>) -> bool { fn inner<'db>( db: &'db dyn HirAnalysisDb, ty: TyId<'db>, @@ -264,6 +264,10 @@ pub fn ty_is_noesc<'db>(db: &'db dyn HirAnalysisDb, ty: TyId<'db>) -> bool { } } +pub fn ty_is_noesc<'db>(db: &'db dyn HirAnalysisDb, ty: TyId<'db>) -> bool { + ty_contains_noesc_capability(db, ty) +} + /// An analysis pass for type definitions. pub struct AdtDefAnalysisPass {} diff --git a/crates/hir/src/analysis/ty/ty_def.rs b/crates/hir/src/analysis/ty/ty_def.rs index bc50d8e344..9757eaddca 100644 --- a/crates/hir/src/analysis/ty/ty_def.rs +++ b/crates/hir/src/analysis/ty/ty_def.rs @@ -1697,7 +1697,7 @@ pub enum PrimTy { BorrowRef, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum BorrowKind { Mut, Ref, diff --git a/crates/hir/tests/semantic_borrowck.rs b/crates/hir/tests/semantic_borrowck.rs index 96f71fddf5..1630f32948 100644 --- a/crates/hir/tests/semantic_borrowck.rs +++ b/crates/hir/tests/semantic_borrowck.rs @@ -5,11 +5,12 @@ use fe_hir::test_db::{HirAnalysisTestDb, format_diagnostics}; use fe_hir::{ analysis::{ semantic::{ - BorrowInputRef, BorrowTransform, NBorrowRoot, NExpr, NLocalOrigin, NSPlaceRoot, + BorrowSource, FieldIndex, IndexExpr, NBorrowRoot, NExpr, NLocalOrigin, NSPlaceRoot, NSStmtKind, NormalizedBindingLowering, ReadMode, SStmtKind, SemanticBorrowDiagKind, - SemanticInstance, SemanticLocalKind, check_semantic_borrows, check_semantic_noesc, - collect_semantic_borrow_diagnostic_vouchers, get_or_build_semantic_instance, - identity_semantic_instance_key, normalize_semantic_body, semantic_borrow_summary, + SemanticInstance, SemanticLocalKind, SummaryProjection, check_semantic_borrows, + check_semantic_noesc, collect_semantic_borrow_diagnostic_vouchers, + get_or_build_semantic_instance, identity_semantic_instance_key, + normalize_semantic_body, semantic_borrow_summary, }, ty::{ ProviderAddressSpace, @@ -18,7 +19,7 @@ use fe_hir::{ }, }, hir_def::{ItemKind, Partial}, - projection::{IndexSource, Projection, ProjectionPath}, + projection::{IndexSource, Projection}, }; fn borrow_diags(src: &str) -> String { @@ -300,15 +301,18 @@ impl Ledger { let summary = semantic_borrow_summary(&db, instance) .expect("borrow summary") .expect("borrow-returning function should produce a summary"); - assert_eq!(summary.len(), 2, "unexpected summary: {summary:#?}"); - assert!(summary.iter().any(|transform| { - matches!(transform.input, BorrowInputRef::Param(0)) - && transform.proj.iter().cloned().collect::>() == vec![Projection::Field(2)] - })); - assert!(summary.iter().any(|transform| { - matches!(transform.input, BorrowInputRef::Param(0)) - && transform.proj.iter().cloned().collect::>() == vec![Projection::Field(0)] - })); + assert_eq!(summary.len(), 1, "unexpected summary: {summary:#?}"); + let sources = &summary.leaves()[0].sources; + for field in [FieldIndex(2), FieldIndex(0)] { + assert!( + sources.iter().any(|clause| matches!( + &clause.source, + BorrowSource::ParamPlace { param: 0, path } + if path.as_slice() == [SummaryProjection::Field(field)] + )), + "unexpected sources: {sources:#?}" + ); + } check_semantic_borrows(&db, instance).expect("borrowck should accept branch-returned borrow"); } @@ -351,13 +355,12 @@ impl Holder { let summary = semantic_borrow_summary(&db, instance) .expect("borrow summary") .expect("forward should produce a borrow summary"); - assert_eq!( - summary, - vec![BorrowTransform { - input: BorrowInputRef::Param(1), - proj: ProjectionPath::default(), - }] - ); + assert_eq!(summary.len(), 1, "unexpected summary: {summary:#?}"); + assert!(summary.leaves()[0].path.is_empty()); + assert!(summary.leaves()[0].sources.iter().any(|clause| matches!( + &clause.source, + BorrowSource::ParamCapability { param: 1, slot } if slot.is_empty() + ))); check_semantic_borrows(&db, instance).expect("borrowck should accept forwarded borrows"); } @@ -2086,3 +2089,642 @@ fn write(mut tree: Tree, i: usize, h: u256) -> Tree { assert!(matches!(path[1], Projection::Index(_))); assert!(matches!(path[2], Projection::Field(0))); } + +#[test] +fn call_result_aggregate_retains_embedded_borrow_loans() { + let diags = borrow_diags( + r#" +struct Wrap { + handle: mut u256, + tag: u256, +} + +fn wrap(handle: mut u256) -> Wrap { + Wrap { handle, tag: 0 } +} + +fn bad() { + let mut value = 0 + let wrapped = wrap(handle: mut value) + let alias = mut value + alias = 1 + wrapped.handle = 2 +} +"#, + ); + + assert!(diags.contains("borrow conflict in `fn bad`"), "{diags}"); +} + +#[test] +fn aggregate_return_cannot_hide_borrow_of_local() { + let diags = borrow_diags( + r#" +struct Wrap { + handle: mut u256, +} + +fn bad() -> Wrap { + let mut value = 0 + Wrap { handle: mut value } +} +"#, + ); + + assert!( + diags.contains("invalid return borrow in `fn bad`"), + "{diags}" + ); + assert!( + diags.contains("cannot return a value that holds a borrow of local `value`"), + "{diags}" + ); +} + +#[test] +fn aggregate_call_arguments_check_embedded_borrow_aliases() { + let diags = borrow_diags( + r#" +struct Borrowed { + value: mut u256, +} + +fn write_both(mut left: own Borrowed, mut right: own Borrowed) { + left.value = 1 + right.value = 2 +} + +fn bad() { + let mut value = 0 + let borrowed = mut value + let left = Borrowed { value: borrowed } + let right = Borrowed { value: borrowed } + write_both(left: left, right: right) +} +"#, + ); + + assert!(diags.contains("borrow conflict in `fn bad`"), "{diags}"); + assert!( + diags.contains("call arguments require conflicting access"), + "{diags}" + ); +} + +#[test] +fn reading_one_returned_aggregate_borrow_does_not_retain_siblings() { + let diags = borrow_diags( + r#" +struct Pair { + left: mut u256, + right: mut u256, +} + +fn forward(_ pair: own Pair) -> Pair { + pair +} + +fn valid() { + let mut left = 0 + let mut right = 0 + let returned = forward(Pair { left: mut left, right: mut right }) + let selected = returned.left + let other = mut right + other = 1 + selected = 2 +} +"#, + ); + + assert!(diags.is_empty(), "{diags}"); +} + +#[test] +fn returned_array_family_preserves_constant_and_dynamic_aliasing() { + let diags = borrow_diags( + r#" +fn forward(_ values: own [mut u256; 2]) -> [mut u256; 2] { + values +} + +fn constant_sibling_is_disjoint() { + let mut left = 0 + let mut right = 0 + let returned = forward([mut left, mut right]) + let first = returned[0] + let other = mut right + other = 1 + first = 2 +} + +fn dynamic_index_overlaps(index: usize) { + let mut left = 0 + let mut right = 0 + let returned = forward([mut left, mut right]) + let selected = returned[index] + let other = mut right + other = 1 + selected = 2 +} +"#, + ); + + assert!( + !diags.contains("borrow conflict in `fn constant_sibling_is_disjoint`"), + "{diags}" + ); + assert!( + diags.contains("borrow conflict in `fn dynamic_index_overlaps`"), + "{diags}" + ); +} + +#[test] +fn exact_array_overwrites_partition_symbolic_families() { + let diags = borrow_diags( + r#" +struct Wrap { + handle: mut u256, +} + +fn forward(_ values: own [Wrap; 2]) -> [Wrap; 2] { + values +} + +fn replace_first( + mut _ values: own [Wrap; 2], + replacement: own Wrap, +) -> [Wrap; 2] { + values[0] = replacement + values +} + +fn local_replacement_releases_old_member() { + let mut old_left = 0 + let mut right = 0 + let mut replacement = 0 + let mut values = forward( + [Wrap { handle: mut old_left }, Wrap { handle: mut right }], + ) + values[0] = Wrap { handle: mut replacement } + let released = mut old_left + released = 1 + values[0].handle = 2 + values[1].handle = 3 +} + +fn sibling_remains_borrowed() { + let mut old_left = 0 + let mut right = 0 + let mut replacement = 0 + let mut values = forward( + [Wrap { handle: mut old_left }, Wrap { handle: mut right }], + ) + values[0] = Wrap { handle: mut replacement } + let alias = mut right + alias = 1 + values[1].handle = 2 +} + +fn helper_return_preserves_override() { + let mut old_left = 0 + let mut right = 0 + let mut replacement = 0 + let mut returned = replace_first( + [Wrap { handle: mut old_left }, Wrap { handle: mut right }], + replacement: Wrap { handle: mut replacement }, + ) + let released = mut old_left + released = 1 + returned[0].handle = 2 + returned[1].handle = 3 +} + +fn conditional_replacement_keeps_old(condition: bool) { + let mut old_left = 0 + let mut right = 0 + let mut replacement = 0 + let mut values = forward( + [Wrap { handle: mut old_left }, Wrap { handle: mut right }], + ) + if condition { + values[0] = Wrap { handle: mut replacement } + } + let alias = mut old_left + alias = 1 + values[0].handle = 2 +} + +fn replace_all_local_members(left: mut u256, right: mut u256) -> [Wrap; 2] { + let mut old_left = 0 + let mut old_right = 0 + let mut values = forward( + [Wrap { handle: mut old_left }, Wrap { handle: mut old_right }], + ) + values[0] = Wrap { handle: left } + values[1] = Wrap { handle: right } + values +} + +fn retain_one_local_member(left: mut u256) -> [Wrap; 2] { + let mut old_left = 0 + let mut old_right = 0 + let mut values = forward( + [Wrap { handle: mut old_left }, Wrap { handle: mut old_right }], + ) + values[0] = Wrap { handle: left } + values +} +"#, + ); + + assert!( + !diags.contains("borrow conflict in `fn local_replacement_releases_old_member`"), + "{diags}" + ); + assert!( + !diags.contains("borrow conflict in `fn helper_return_preserves_override`"), + "{diags}" + ); + assert!( + diags.contains("borrow conflict in `fn sibling_remains_borrowed`"), + "{diags}" + ); + assert!( + diags.contains("borrow conflict in `fn conditional_replacement_keeps_old`"), + "{diags}" + ); + assert!( + !diags.contains("invalid return borrow in `fn replace_all_local_members`"), + "{diags}" + ); + assert!( + diags.contains("invalid return borrow in `fn retain_one_local_member`"), + "{diags}" + ); +} + +#[test] +fn array_member_reborrow_suspends_only_that_parent_member() { + let diags = borrow_diags( + r#" +fn forward(_ values: own [mut u256; 2]) -> [mut u256; 2] { + values +} + +fn reborrow(value: mut u256) -> mut u256 { + value +} + +fn valid() { + let mut left = 0 + let mut right = 0 + let mut returned = forward([mut left, mut right]) + let first = reborrow(value: returned[0]) + returned[1] = 1 + first = 2 +} + +fn bad() { + let mut left = 0 + let mut right = 0 + let mut returned = forward([mut left, mut right]) + let first = reborrow(value: returned[0]) + let alias = mut right + alias = 1 + returned[1] = 2 + first = 3 +} + +fn transitive_bad() { + let mut left = 0 + let mut right = 0 + let returned = forward([mut left, mut right]) + let first = reborrow(value: returned[0]) + let first_again = reborrow(value: first) + let alias = mut right + alias = 1 + returned[1] = 2 + first_again = 3 +} +"#, + ); + + assert!(!diags.contains("borrow conflict in `fn valid`"), "{diags}"); + assert!(diags.contains("borrow conflict in `fn bad`"), "{diags}"); + assert!( + diags.contains("borrow conflict in `fn transitive_bad`"), + "{diags}" + ); +} + +#[test] +fn mutually_exclusive_enum_borrow_slots_do_not_conflict() { + let diags = borrow_diags( + r#" +enum Choice { + A(mut u256), + B(mut u256), +} + +struct Pair { + left: Choice, + right: Choice, +} + +fn consume(_ choice: own Choice) {} +fn consume_pair(_ pair: own Pair) {} + +fn valid(condition: bool) { + let mut value = 0 + let choice = if condition { + Choice::A(mut value) + } else { + Choice::B(mut value) + } + consume(choice) +} + +fn bad() { + let mut value = 0 + let borrowed = mut value + let pair = Pair { + left: Choice::A(borrowed), + right: Choice::B(borrowed), + } + consume_pair(pair) +} +"#, + ); + + assert!(!diags.contains("borrow conflict in `fn valid`"), "{diags}"); + assert!(diags.contains("borrow conflict in `fn bad`"), "{diags}"); +} + +#[test] +fn large_array_borrow_summary_uses_one_symbolic_family() { + let src = r#" +fn forward(_ values: own [mut u256; 1000000]) -> [mut u256; 1000000] { + values +} +"#; + let mut summary = None; + for_each_fixture_instance(src, |db, instance| { + if owner_name(db, instance.key(db).owner(db)) == "forward" { + summary = semantic_borrow_summary(db, instance) + .expect("large-array borrow summary") + .or(summary.take()); + } + }); + + let summary = summary.expect("forward summary"); + assert_eq!(summary.len(), 1, "unexpected summary: {summary:#?}"); + let [SummaryProjection::Index(IndexExpr::ResultParam(result))] = + summary.leaves()[0].path.as_slice() + else { + panic!("unexpected result path: {:#?}", summary.leaves()[0].path); + }; + assert!(summary.leaves()[0].sources.iter().any(|clause| matches!( + &clause.source, + BorrowSource::ParamCapability { param: 0, slot } + if matches!( + slot.as_slice(), + [SummaryProjection::Index(IndexExpr::ResultParam(source))] if source == result + ) + ))); +} + +#[test] +fn reading_a_forwarded_borrow_as_a_value_drops_loan_state() { + let diags = borrow_diags( + r#" +struct Holder { + tag: u256, +} + +impl Holder { + fn forward(mut self, _ value: mut u256) -> mut u256 { + value + } +} + +fn valid() -> u256 { + let mut holder = Holder { tag: 0 } + let mut local = 7 + let value = holder.forward(mut local) + value += 5 + value +} +"#, + ); + + assert!(diags.is_empty(), "{diags}"); +} + +#[test] +fn moving_a_projected_capability_authorizes_its_overlay_write() { + let diags = borrow_diags( + r#" +use std::evm::StorageMap + +enum Choice { + Left(StorageMap), + Right(StorageMap), +} + +impl Choice { + fn flip(mut self) { + match self { + Choice::Left(map) => self = Choice::Right(map), + Choice::Right(map) => self = Choice::Left(map), + } + } +} +"#, + ); + + assert!(diags.is_empty(), "{diags}"); +} + +#[test] +fn returned_array_literal_materializes_exact_slots() { + let src = r#" +fn pair(left: mut u256, right: mut u256) -> [mut u256; 2] { + [left, right] +} +"#; + let mut summary = None; + for_each_fixture_instance(src, |db, instance| { + if owner_name(db, instance.key(db).owner(db)) == "pair" { + summary = semantic_borrow_summary(db, instance) + .expect("array-literal borrow summary") + .or(summary.take()); + } + }); + let summary = summary.expect("pair summary"); + + assert_eq!(summary.len(), 2, "{summary:#?}"); + for (index, param) in [(0, 0), (1, 1)] { + assert!(summary.leaves().iter().any(|leaf| { + leaf.path.as_slice() == [SummaryProjection::Index(IndexExpr::Const(index))] + && leaf.sources.iter().any(|clause| { + matches!( + &clause.source, + BorrowSource::ParamCapability { param: candidate, slot } + if *candidate == param && slot.is_empty() + ) + }) + })); + } +} + +#[test] +fn mutable_receiver_reservation_activates_after_argument_evaluation() { + let diags = borrow_diags( + r#" +struct Cell { + value: u256, +} + +impl Cell { + fn read(self) -> u256 { + self.value + } + + fn write(mut self, value: u256) { + self.value = value + } +} + +fn valid() { + let mut cell = Cell { value: 1 } + cell.write(value: cell.read()) +} + +fn bad() -> u256 { + let mut cell = Cell { value: 1 } + let borrowed = ref cell.value + cell.write(value: cell.read()) + borrowed +} +"#, + ); + + assert!(!diags.contains("borrow conflict in `fn valid`"), "{diags}"); + assert!(diags.contains("borrow conflict in `fn bad`"), "{diags}"); +} + +#[test] +fn noesc_rejects_storage_borrow_inside_aggregate_call_argument() { + let diags = borrow_diags( + r#" +struct Store { + value: u256, +} + +struct Esc { + handle: mut u256, +} + +fn consume(_ value: Esc) {} + +pub contract NoEscStorageAggregateArg { + mut store: Store + + init() uses (mut store) { + let value = Esc { handle: mut store.value } + consume(value) + } +} +"#, + ); + + assert!( + diags.contains("noesc violation in `fn NoEscStorageAggregateArg::__init__`"), + "{diags}" + ); + assert!( + diags.contains("cannot pass `Esc` from storage as function argument"), + "{diags}" + ); +} + +#[test] +fn recursive_aggregate_borrow_summary_converges() { + let diags = borrow_diags( + r#" +struct Owner { + value: u256, +} + +struct Borrowed { + value: mut u256, +} + +fn borrow_value(mut owner: Owner, recurse: bool) -> Borrowed { + if recurse { + borrow_value(owner, recurse: false) + } else { + Borrowed { value: mut owner.value } + } +} + +fn bad() { + let mut owner = Owner { value: 0 } + let borrowed = borrow_value(owner, recurse: true) + let other = mut owner.value + other = 1 + borrowed.value = 2 +} +"#, + ); + + assert!(diags.contains("borrow conflict in `fn bad`"), "{diags}"); +} + +#[test] +fn opaque_aggregate_return_summary_is_conservative() { + let diags = borrow_diags( + r#" +struct Borrowed { + value: mut u256, +} + +trait BorrowValue { + fn borrow_value(mut self) -> Borrowed +} + +fn bad(mut value: T) { + let first = value.borrow_value() + let second = value.borrow_value() + first.value = 1 + second.value = 2 +} +"#, + ); + + assert!(diags.contains("borrow conflict in `fn bad`"), "{diags}"); +} + +#[test] +fn opaque_array_result_does_not_assume_pointwise_family_correlation() { + let diags = borrow_diags( + r#" +trait Permute { + fn permute(self, values: own [mut u256; 2]) -> [mut u256; 2] +} + +fn bad(permuter: T) { + let mut left = 0 + let mut right = 0 + let returned = permuter.permute([mut left, mut right]) + let selected = returned[0] + let alias = mut right + alias = 1 + selected = 2 +} +"#, + ); + + assert!(diags.contains("borrow conflict in `fn bad`"), "{diags}"); +} diff --git a/crates/uitest/fixtures/semantic_borrowck/noesc_storage_scalar_arg.fe b/crates/uitest/fixtures/semantic_borrowck/noesc_storage_scalar_arg.fe new file mode 100644 index 0000000000..f294837f58 --- /dev/null +++ b/crates/uitest/fixtures/semantic_borrowck/noesc_storage_scalar_arg.fe @@ -0,0 +1,13 @@ +struct Store { + value: u256, +} + +fn consume(_ value: u256) {} + +pub contract NoEscStorageScalarArg { + store: Store + + init() uses (store) { + consume(store.value) + } +} diff --git a/crates/uitest/fixtures/semantic_borrowck/noesc_storage_scalar_arg.snap b/crates/uitest/fixtures/semantic_borrowck/noesc_storage_scalar_arg.snap new file mode 100644 index 0000000000..d3c6376475 --- /dev/null +++ b/crates/uitest/fixtures/semantic_borrowck/noesc_storage_scalar_arg.snap @@ -0,0 +1,5 @@ +--- +source: crates/uitest/tests/semantic_borrowck.rs +expression: diags +input_file: fixtures/semantic_borrowck/noesc_storage_scalar_arg.fe +--- diff --git a/newsfragments/1534.bugfix.md b/newsfragments/1534.bugfix.md new file mode 100644 index 0000000000..6048e1ccad --- /dev/null +++ b/newsfragments/1534.bugfix.md @@ -0,0 +1 @@ +Fix borrow checking for aggregates containing references or mutable borrows passed across function calls.