1 //! Contains utilities for generating suggestions for borrowck errors related to unsatisfied 2 //! outlives constraints. 3 4 use rustc_data_structures::fx::FxIndexSet; 5 use rustc_errors::Diagnostic; 6 use rustc_middle::ty::RegionVid; 7 use smallvec::SmallVec; 8 use std::collections::BTreeMap; 9 10 use crate::MirBorrowckCtxt; 11 12 use super::{ErrorConstraintInfo, RegionName, RegionNameSource}; 13 14 /// The different things we could suggest. 15 enum SuggestedConstraint { 16 /// Outlives(a, [b, c, d, ...]) => 'a: 'b + 'c + 'd + ... 17 Outlives(RegionName, SmallVec<[RegionName; 2]>), 18 19 /// 'a = 'b 20 Equal(RegionName, RegionName), 21 22 /// 'a: 'static i.e. 'a = 'static and the user should just use 'static 23 Static(RegionName), 24 } 25 26 /// Collects information about outlives constraints that needed to be added for a given MIR node 27 /// corresponding to a function definition. 28 /// 29 /// Adds a help note suggesting adding a where clause with the needed constraints. 30 #[derive(Default)] 31 pub struct OutlivesSuggestionBuilder { 32 /// The list of outlives constraints that need to be added. Specifically, we map each free 33 /// region to all other regions that it must outlive. I will use the shorthand `fr: 34 /// outlived_frs`. Not all of these regions will already have names necessarily. Some could be 35 /// implicit free regions that we inferred. These will need to be given names in the final 36 /// suggestion message. 37 constraints_to_add: BTreeMap<RegionVid, Vec<RegionVid>>, 38 } 39 40 impl OutlivesSuggestionBuilder { 41 /// Returns `true` iff the `RegionNameSource` is a valid source for an outlives 42 /// suggestion. 43 // 44 // FIXME: Currently, we only report suggestions if the `RegionNameSource` is an early-bound 45 // region or a named region, avoiding using regions with synthetic names altogether. This 46 // allows us to avoid giving impossible suggestions (e.g. adding bounds to closure args). 47 // We can probably be less conservative, since some inferred free regions are namable (e.g. 48 // the user can explicitly name them. To do this, we would allow some regions whose names 49 // come from `MatchedAdtAndSegment`, being careful to filter out bad suggestions, such as 50 // naming the `'self` lifetime in methods, etc. region_name_is_suggestable(name: &RegionName) -> bool51 fn region_name_is_suggestable(name: &RegionName) -> bool { 52 match name.source { 53 RegionNameSource::NamedEarlyBoundRegion(..) 54 | RegionNameSource::NamedFreeRegion(..) 55 | RegionNameSource::Static => true, 56 57 // Don't give suggestions for upvars, closure return types, or other unnameable 58 // regions. 59 RegionNameSource::SynthesizedFreeEnvRegion(..) 60 | RegionNameSource::AnonRegionFromArgument(..) 61 | RegionNameSource::AnonRegionFromUpvar(..) 62 | RegionNameSource::AnonRegionFromOutput(..) 63 | RegionNameSource::AnonRegionFromYieldTy(..) 64 | RegionNameSource::AnonRegionFromAsyncFn(..) 65 | RegionNameSource::AnonRegionFromImplSignature(..) => { 66 debug!("Region {:?} is NOT suggestable", name); 67 false 68 } 69 } 70 } 71 72 /// Returns a name for the region if it is suggestable. See `region_name_is_suggestable`. region_vid_to_name( &self, mbcx: &MirBorrowckCtxt<'_, '_>, region: RegionVid, ) -> Option<RegionName>73 fn region_vid_to_name( 74 &self, 75 mbcx: &MirBorrowckCtxt<'_, '_>, 76 region: RegionVid, 77 ) -> Option<RegionName> { 78 mbcx.give_region_a_name(region).filter(Self::region_name_is_suggestable) 79 } 80 81 /// Compiles a list of all suggestions to be printed in the final big suggestion. compile_all_suggestions( &self, mbcx: &MirBorrowckCtxt<'_, '_>, ) -> SmallVec<[SuggestedConstraint; 2]>82 fn compile_all_suggestions( 83 &self, 84 mbcx: &MirBorrowckCtxt<'_, '_>, 85 ) -> SmallVec<[SuggestedConstraint; 2]> { 86 let mut suggested = SmallVec::new(); 87 88 // Keep track of variables that we have already suggested unifying so that we don't print 89 // out silly duplicate messages. 90 let mut unified_already = FxIndexSet::default(); 91 92 for (fr, outlived) in &self.constraints_to_add { 93 let Some(fr_name) = self.region_vid_to_name(mbcx, *fr) else { 94 continue; 95 }; 96 97 let outlived = outlived 98 .iter() 99 // if there is a `None`, we will just omit that constraint 100 .filter_map(|fr| self.region_vid_to_name(mbcx, *fr).map(|rname| (fr, rname))) 101 .collect::<Vec<_>>(); 102 103 // No suggestable outlived lifetimes. 104 if outlived.is_empty() { 105 continue; 106 } 107 108 // There are three types of suggestions we can make: 109 // 1) Suggest a bound: 'a: 'b 110 // 2) Suggest replacing 'a with 'static. If any of `outlived` is `'static`, then we 111 // should just replace 'a with 'static. 112 // 3) Suggest unifying 'a with 'b if we have both 'a: 'b and 'b: 'a 113 114 if outlived 115 .iter() 116 .any(|(_, outlived_name)| matches!(outlived_name.source, RegionNameSource::Static)) 117 { 118 suggested.push(SuggestedConstraint::Static(fr_name)); 119 } else { 120 // We want to isolate out all lifetimes that should be unified and print out 121 // separate messages for them. 122 123 let (unified, other): (Vec<_>, Vec<_>) = outlived.into_iter().partition( 124 // Do we have both 'fr: 'r and 'r: 'fr? 125 |(r, _)| { 126 self.constraints_to_add 127 .get(r) 128 .is_some_and(|r_outlived| r_outlived.as_slice().contains(fr)) 129 }, 130 ); 131 132 for (r, bound) in unified.into_iter() { 133 if !unified_already.contains(fr) { 134 suggested.push(SuggestedConstraint::Equal(fr_name.clone(), bound)); 135 unified_already.insert(r); 136 } 137 } 138 139 if !other.is_empty() { 140 let other = 141 other.iter().map(|(_, rname)| rname.clone()).collect::<SmallVec<_>>(); 142 suggested.push(SuggestedConstraint::Outlives(fr_name, other)) 143 } 144 } 145 } 146 147 suggested 148 } 149 150 /// Add the outlives constraint `fr: outlived_fr` to the set of constraints we need to suggest. collect_constraint(&mut self, fr: RegionVid, outlived_fr: RegionVid)151 pub(crate) fn collect_constraint(&mut self, fr: RegionVid, outlived_fr: RegionVid) { 152 debug!("Collected {:?}: {:?}", fr, outlived_fr); 153 154 // Add to set of constraints for final help note. 155 self.constraints_to_add.entry(fr).or_default().push(outlived_fr); 156 } 157 158 /// Emit an intermediate note on the given `Diagnostic` if the involved regions are 159 /// suggestable. intermediate_suggestion( &mut self, mbcx: &MirBorrowckCtxt<'_, '_>, errci: &ErrorConstraintInfo<'_>, diag: &mut Diagnostic, )160 pub(crate) fn intermediate_suggestion( 161 &mut self, 162 mbcx: &MirBorrowckCtxt<'_, '_>, 163 errci: &ErrorConstraintInfo<'_>, 164 diag: &mut Diagnostic, 165 ) { 166 // Emit an intermediate note. 167 let fr_name = self.region_vid_to_name(mbcx, errci.fr); 168 let outlived_fr_name = self.region_vid_to_name(mbcx, errci.outlived_fr); 169 170 if let (Some(fr_name), Some(outlived_fr_name)) = (fr_name, outlived_fr_name) 171 && !matches!(outlived_fr_name.source, RegionNameSource::Static) 172 { 173 diag.help(format!( 174 "consider adding the following bound: `{fr_name}: {outlived_fr_name}`", 175 )); 176 } 177 } 178 179 /// If there is a suggestion to emit, add a diagnostic to the buffer. This is the final 180 /// suggestion including all collected constraints. add_suggestion(&self, mbcx: &mut MirBorrowckCtxt<'_, '_>)181 pub(crate) fn add_suggestion(&self, mbcx: &mut MirBorrowckCtxt<'_, '_>) { 182 // No constraints to add? Done. 183 if self.constraints_to_add.is_empty() { 184 debug!("No constraints to suggest."); 185 return; 186 } 187 188 // If there is only one constraint to suggest, then we already suggested it in the 189 // intermediate suggestion above. 190 if self.constraints_to_add.len() == 1 191 && self.constraints_to_add.values().next().unwrap().len() == 1 192 { 193 debug!("Only 1 suggestion. Skipping."); 194 return; 195 } 196 197 // Get all suggestable constraints. 198 let suggested = self.compile_all_suggestions(mbcx); 199 200 // If there are no suggestable constraints... 201 if suggested.is_empty() { 202 debug!("Only 1 suggestable constraint. Skipping."); 203 return; 204 } 205 206 // If there is exactly one suggestable constraints, then just suggest it. Otherwise, emit a 207 // list of diagnostics. 208 let mut diag = if suggested.len() == 1 { 209 mbcx.infcx.tcx.sess.diagnostic().struct_help(match suggested.last().unwrap() { 210 SuggestedConstraint::Outlives(a, bs) => { 211 let bs: SmallVec<[String; 2]> = bs.iter().map(|r| r.to_string()).collect(); 212 format!("add bound `{a}: {}`", bs.join(" + ")) 213 } 214 215 SuggestedConstraint::Equal(a, b) => { 216 format!("`{a}` and `{b}` must be the same: replace one with the other") 217 } 218 SuggestedConstraint::Static(a) => format!("replace `{a}` with `'static`"), 219 }) 220 } else { 221 // Create a new diagnostic. 222 let mut diag = mbcx 223 .infcx 224 .tcx 225 .sess 226 .diagnostic() 227 .struct_help("the following changes may resolve your lifetime errors"); 228 229 // Add suggestions. 230 for constraint in suggested { 231 match constraint { 232 SuggestedConstraint::Outlives(a, bs) => { 233 let bs: SmallVec<[String; 2]> = bs.iter().map(|r| r.to_string()).collect(); 234 diag.help(format!("add bound `{a}: {}`", bs.join(" + "))); 235 } 236 SuggestedConstraint::Equal(a, b) => { 237 diag.help(format!( 238 "`{a}` and `{b}` must be the same: replace one with the other", 239 )); 240 } 241 SuggestedConstraint::Static(a) => { 242 diag.help(format!("replace `{a}` with `'static`")); 243 } 244 } 245 } 246 247 diag 248 }; 249 250 // We want this message to appear after other messages on the mir def. 251 let mir_span = mbcx.body.span; 252 diag.sort_span = mir_span.shrink_to_hi(); 253 254 // Buffer the diagnostic 255 mbcx.buffer_non_error_diag(diag); 256 } 257 } 258