diplomat_core/hir/elision.rs
1//! This module provides the functionality for lowering lifetimes from the AST
2//! to the HIR, while simultaneously inferencing elided lifetimes.
3//!
4//! Full elision rules can be found in the [Nomicon].
5//!
6//! The key factor about lifetime elision is that all elision in the output of
7//! the method (if there is any) corresponds to exactly one lifetime in the method
8//! arguments, which may or may not be elided. Therefore, our task is to find this
9//! potential lifetime first, so that if we encounter an elided lifetime while
10//! lowering the output, we know which lifetime it corresponds to.
11//!
12//! # Unspoken Rules of Elision.
13//!
14//! Broadly speaking, the Nomicon defines the elision rules are such:
15//! 1. If there's a `&self` or `&mut self`, the lifetime of that borrow
16//! corresponds to elision in the output.
17//! 2. Otherwise, if there's exactly one lifetime in the input, then that lifetime
18//! corresponds to elision in the output.
19//! 3. If neither of these cases hold, then the output cannot contain elision.
20//!
21//! What the Nomicon doesn't tell you is that there are weird corner cases around
22//! using the `Self` type. Specifically, lifetimes in the `Self` type and in the
23//! type of the `self` argument (optional) aren't considered when figuring out
24//! which lifetime should correspond to elision in the output.
25//!
26//! Check out the following code:
27//! ```compile_fail
28//! struct Foo<'a>(&'a str);
29//!
30//! impl<'a> Foo<'a> {
31//! fn get(self) -> &str { self.0 }
32//! }
33//! ```
34//! This code will fail to compile because it doesn't look at the `'a` in the
35//! `Foo<'a>`, which is what the type of `self` expands to. Therefore, it will
36//! conclude that there's nothing for the output to borrow from.
37//! This can be fixed by returning `&'a str` though. Many of the design
38//! decisions in this module were made to be able to replicate this behavior.
39//!
40//! You may be asking "why would we care about rejecting code that rustc rejects
41//! before it reaches us?" And the answer is this:
42//! ```rust
43//! # struct Foo<'a>(&'a str);
44//! impl<'a> Foo<'a> {
45//! fn get(self, s: &str) -> &str { s }
46//! }
47//! ```
48//! This code is accepted by rustc, since it only considers the lifetime of `s`
49//! when searching for a lifetime that corresponds to output elision. If we were
50//! to naively look at all the lifetimes, we would see the lifetime in the `self`
51//! argument and the lifetime of `s`, making us reject this method. Therefore, we
52//! have to be extremely careful when traversing lifetimes, and make sure that
53//! lifetimes of `Self` are lowered but _not_ considered for elision, while other
54//! lifetimes are lowered while also being considered for elision.
55//!
56//! # Lowering and Inference
57//!
58//! Lowering and elision inference is broken into three distinct stages:
59//! 1. Lowering the borrow in `&self` or `&mut self`, if there is one.
60//! 2. Lowering lifetimes of other params.
61//! 3. Lowering lifetimes of the output.
62//!
63//! Although each stage fundementally lowers lifetimes, they behave differently
64//! when lowering elided lifetimes. Naturally, this module represents each stage
65//! as a state in a state machine.
66//!
67//! The first state is represented by the [`SelfParamLifetimeLowerer`] type.
68//! Since there is either zero or one occurrences of `&self` or `&mut self`, it
69//! exposes the `.no_self_ref()` and `.lower_self_ref(lt)` methods respectively,
70//! which consume the `SelfParamLifetimeLowerer` and return the next state,
71//! [`ParamLifetimeLowerer`], as well as the lowered lifetime. The reason these
72//! are two distinct types is that the lifetime in `&self` and `&mut self` takes
73//! precedence over any other lifetimes in the input, so `.lower_self_ref(lt)`
74//! tells the next state that the candidate lifetime is already found, and to
75//! generate fresh anonymous lifetimes for any elided lifetimes.
76//!
77//! The second state is represented by the [`ParamLifetimeLowerer`] type.
78//! It implements a helper trait, [`LifetimeLowerer`], which abstracts the lowering
79//! of references and generic lifetimes. Internally, it wraps an [`ElisionSource`],
80//! which acts as a state machine for tracking candidate lifetimes to correspond
81//! to elision in the output. When a lifetime that's not in the type of the `self`
82//! argument or in the expanded generics of the `Self` type is visited, this
83//! state machine is potentially updated to another state. If the lifetime is
84//! anonymous, it's added to the internal list of nodes that go into the final
85//! [`LifetimeEnv`] after lowering. Once all the lifetimes in the input are
86//! lowered, the `into_return_ltl()` method is called to transition into the
87//! final state.
88//!
89//! The third and final state is represented by the [`ReturnLifetimeLowerer`] type.
90//! Similar to `ParamLifetimeLowerer`, it also implements the [`LifetimeLowerer`]
91//! helper trait. However, it differs from `ParamLifetimeLowerer` since instead
92//! of potentially updating the internal `ElisionSource` when visiting a lifetime,
93//! it instead reads from it when an elided lifetime occurs. Once all the output
94//! lifetimes are lowered, `.finish()` is called to return the finalized
95//! [`LifetimeEnv`].
96//!
97//! [Nomicon]: https://doc.rust-lang.org/nomicon/lifetime-elision.html
98
99use super::lifetimes::{BoundedLifetime, Lifetime, LifetimeEnv, Lifetimes, MaybeStatic};
100use super::LoweringContext;
101use crate::ast;
102use smallvec::SmallVec;
103
104/// Lower [`ast::Lifetime`]s to [`Lifetime`]s.
105///
106/// This helper traits allows the [`lower_type`] and [`lower_out_type`] methods
107/// to abstractly lower lifetimes without concern for what sort of tracking
108/// goes on. In particular, elision inference requires updating internal state
109/// when visiting lifetimes in the input.
110pub trait LifetimeLowerer {
111 /// Lowers an [`ast::Lifetime`].
112 fn lower_lifetime(&mut self, lifetime: &ast::Lifetime) -> MaybeStatic<Lifetime>;
113
114 /// Lowers a slice of [`ast::Lifetime`]s by calling
115 /// [`LifetimeLowerer::lower_lifetime`] repeatedly.
116 ///
117 /// `type_generics` is the full list of generics on the type definition of the type
118 /// this lifetimes list is found on (needed for generating anon lifetimes)
119 fn lower_lifetimes(
120 &mut self,
121 lifetimes: &[ast::Lifetime],
122 type_generics: &ast::LifetimeEnv,
123 ) -> Lifetimes {
124 let mut lifetimes = Lifetimes::from_fn(lifetimes, |lifetime| self.lower_lifetime(lifetime));
125
126 for _ in lifetimes.as_slice().len()..type_generics.nodes.len() {
127 lifetimes.append_lifetime(self.lower_lifetime(&ast::Lifetime::Anonymous))
128 }
129 lifetimes
130 }
131
132 /// Lowers a slice of [`ast::Lifetime`], where the strategy may vary depending
133 /// on whether or not the lifetimes are expanded from the `Self` type.
134 ///
135 /// The distinction between this and [`LifetimeLowerer::lower_lifetimes`] is
136 /// that if `Self` expands to a type with anonymous lifetimes like `Foo<'_>`,
137 /// then multiple instances of `Self` should expand to have the same anonymous
138 /// lifetime, and this lifetime can be cached inside of the `self` argument.
139 /// Additionally, elision inferences knows to not search inside the generics
140 /// of `Self` types for candidate lifetimes to correspond to elided lifetimes
141 /// in the output.
142 ///
143 /// `type_generics` is the full list of generics on the type definition of the type
144 /// this generics list is found on (needed for generating anon lifetimes)
145 fn lower_generics(
146 &mut self,
147 lifetimes: &[ast::Lifetime],
148 type_generics: &ast::LifetimeEnv,
149 is_self: bool,
150 ) -> Lifetimes;
151}
152
153/// A state machine for tracking which lifetime in a function's parameters
154/// may correspond to elided lifetimes in the output.
155#[derive(Copy, Clone)]
156enum ElisionSource {
157 /// No borrows in the input, no elision.
158 NoBorrows,
159 /// `&self` or `&mut self`, elision allowed.
160 SelfParam(MaybeStatic<Lifetime>),
161 /// One param contains a borrow, elision allowed.
162 OneParam(MaybeStatic<Lifetime>),
163 /// Multiple borrows and no self borrow, no elision.
164 MultipleBorrows,
165}
166
167impl ElisionSource {
168 /// Potentially transition to a new state.
169 fn visit_lifetime(&mut self, lifetime: MaybeStatic<Lifetime>) {
170 match self {
171 ElisionSource::NoBorrows => *self = ElisionSource::OneParam(lifetime),
172 ElisionSource::SelfParam(_) => {
173 // References to self have the highest precedence, do nothing.
174 }
175 ElisionSource::OneParam(_) => *self = ElisionSource::MultipleBorrows,
176 ElisionSource::MultipleBorrows => {
177 // There's ambiguity. This is valid when there's no elision in
178 // the output.
179 }
180 };
181 }
182}
183
184/// A type for storing shared information between the different states of elision
185/// inference.
186///
187/// This contains data for generating fresh elided lifetimes, looking up named
188/// lifetimes, and caching lifetimes of `Self`.
189pub(super) struct BaseLifetimeLowerer<'ast> {
190 lifetime_env: &'ast ast::LifetimeEnv,
191 self_lifetimes: Option<Lifetimes>,
192 nodes: SmallVec<[BoundedLifetime; super::lifetimes::INLINE_NUM_LIFETIMES]>,
193 num_lifetimes: usize,
194}
195
196/// The first phase of output elision inference.
197///
198/// In the first phase, the type signature of the `&self` or `&mut self` type
199/// is lowered into its HIR representation, if present. According to elision
200/// rules, this reference has the highest precedence as the lifetime that
201/// goes into elision in the output, and so it's checked first.
202pub(super) struct SelfParamLifetimeLowerer<'ast> {
203 base: BaseLifetimeLowerer<'ast>,
204}
205
206/// The second phase of output elision inference.
207///
208/// In the second phase, all lifetimes in the parameter type signatures
209/// (besides the lifetime of self, if present) are lowered. If a self param
210/// didn't claim the potential output elided lifetime, then if there's a
211/// single lifetime (elided or not) in the inputs, it will claim the
212/// potential output elided lifetime.
213pub(super) struct ParamLifetimeLowerer<'ast> {
214 elision_source: ElisionSource,
215 base: BaseLifetimeLowerer<'ast>,
216}
217
218/// The third and final phase of output elision inference.
219///
220/// In the third phase, the type signature of the output type is lowered into
221/// its HIR representation. If one of the input lifetimes were marked as
222/// responsible for any elision in the output, then anonymous lifetimes get
223/// that lifetime. If none did and there is elision in the output, then
224/// rustc should have errored and said the elision was ambiguous, meaning
225/// that state should be impossible so it panics.
226pub(super) struct ReturnLifetimeLowerer<'ast> {
227 elision_source: ElisionSource,
228 base: BaseLifetimeLowerer<'ast>,
229}
230
231impl<'ast> BaseLifetimeLowerer<'ast> {
232 /// Returns a [`Lifetime`] representing a new anonymous lifetime, and
233 /// pushes it to the nodes vector.
234 fn new_elided(&mut self) -> Lifetime {
235 let index = self.num_lifetimes;
236 self.num_lifetimes += 1;
237 Lifetime::new(index)
238 }
239
240 /// Lowers a single [`ast::Lifetime`]. If the lifetime is elided, then a fresh
241 /// [`ImplicitLifetime`] is generated.
242 fn lower_lifetime(&mut self, lifetime: &ast::Lifetime) -> MaybeStatic<Lifetime> {
243 match lifetime {
244 ast::Lifetime::Static => MaybeStatic::Static,
245 ast::Lifetime::Named(named) => {
246 MaybeStatic::NonStatic(Lifetime::from_ast(named, self.lifetime_env))
247 }
248 ast::Lifetime::Anonymous => MaybeStatic::NonStatic(self.new_elided()),
249 }
250 }
251
252 /// Retrieves the cached `Self` lifetimes, or caches newly generated
253 /// lifetimes and returns those.
254 fn self_lifetimes_or_new(&mut self, ast_lifetimes: &[ast::Lifetime]) -> Lifetimes {
255 if let Some(lifetimes) = &self.self_lifetimes {
256 lifetimes.clone()
257 } else {
258 let lifetimes = Lifetimes::from_fn(ast_lifetimes, |lt| self.lower_lifetime(lt));
259 self.self_lifetimes = Some(lifetimes.clone());
260 lifetimes
261 }
262 }
263}
264
265impl<'ast> SelfParamLifetimeLowerer<'ast> {
266 /// Returns a new [`SelfParamLifetimeLowerer`].
267 pub fn new(
268 lifetime_env: &'ast ast::LifetimeEnv,
269 ctx: &mut LoweringContext,
270 ) -> Result<Self, ()> {
271 let mut hir_nodes = Ok(SmallVec::new());
272
273 for ast_node in lifetime_env.nodes.iter() {
274 let lifetime = ctx.lower_ident(ast_node.lifetime.name(), "named lifetime");
275 match (lifetime, &mut hir_nodes) {
276 (Ok(lifetime), Ok(hir_nodes)) => {
277 hir_nodes.push(BoundedLifetime::new(
278 lifetime,
279 ast_node.longer.iter().map(|i| Lifetime::new(*i)).collect(),
280 ast_node.shorter.iter().map(|i| Lifetime::new(*i)).collect(),
281 ));
282 }
283 _ => hir_nodes = Err(()),
284 }
285 }
286
287 hir_nodes.map(|nodes| Self {
288 base: BaseLifetimeLowerer {
289 lifetime_env,
290 self_lifetimes: None,
291 num_lifetimes: nodes.len(),
292 nodes,
293 },
294 })
295 }
296
297 /// Lowers the lifetime of `&self` or `&mut self`.
298 ///
299 /// The lifetimes of `&self` and `&mut self` are special, because they
300 /// automatically take priority over any other lifetime in the input for
301 /// being tied to any elided lifetimes in the output.
302 ///
303 /// Along with returning the lowered lifetime, this method also returns the
304 /// next state in elision inference, the [`ParamLifetimeLowerer`].
305 pub fn lower_self_ref(
306 mut self,
307 lifetime: &ast::Lifetime,
308 ) -> (MaybeStatic<Lifetime>, ParamLifetimeLowerer<'ast>) {
309 let self_lifetime = self.base.lower_lifetime(lifetime);
310
311 (
312 self_lifetime,
313 self.into_param_ltl(ElisionSource::SelfParam(self_lifetime)),
314 )
315 }
316
317 /// Acknowledges that there's no `&self` or `&mut self`, and transitions
318 /// to the next state, [`ParamLifetimeLowerer`].
319 pub fn no_self_ref(self) -> ParamLifetimeLowerer<'ast> {
320 self.into_param_ltl(ElisionSource::NoBorrows)
321 }
322
323 /// Transition into the next state, [`ParamLifetimeLowerer`].
324 fn into_param_ltl(self, elision_source: ElisionSource) -> ParamLifetimeLowerer<'ast> {
325 ParamLifetimeLowerer {
326 elision_source,
327 base: self.base,
328 }
329 }
330}
331
332impl<'ast> ParamLifetimeLowerer<'ast> {
333 /// Once all lifetimes in the parameters are lowered, this function is
334 /// called to transition to the next state, [`ReturnLifetimeLowerer`].
335 pub fn into_return_ltl(self) -> ReturnLifetimeLowerer<'ast> {
336 ReturnLifetimeLowerer {
337 elision_source: self.elision_source,
338 base: self.base,
339 }
340 }
341}
342
343impl<'ast> LifetimeLowerer for ParamLifetimeLowerer<'ast> {
344 fn lower_lifetime(&mut self, borrow: &ast::Lifetime) -> MaybeStatic<Lifetime> {
345 let lifetime = self.base.lower_lifetime(borrow);
346 self.elision_source.visit_lifetime(lifetime);
347 lifetime
348 }
349
350 fn lower_generics(
351 &mut self,
352 lifetimes: &[ast::Lifetime],
353 type_generics: &ast::LifetimeEnv,
354 is_self: bool,
355 ) -> Lifetimes {
356 if is_self {
357 self.base.self_lifetimes_or_new(lifetimes)
358 } else {
359 self.lower_lifetimes(lifetimes, type_generics)
360 }
361 }
362}
363
364impl<'ast> ReturnLifetimeLowerer<'ast> {
365 /// Finalize the lifetimes in the method, returning the resulting [`LifetimeEnv`].
366 pub fn finish(self) -> LifetimeEnv {
367 LifetimeEnv::new(self.base.nodes, self.base.num_lifetimes)
368 }
369}
370
371impl<'ast> LifetimeLowerer for ReturnLifetimeLowerer<'ast> {
372 fn lower_lifetime(&mut self, borrow: &ast::Lifetime) -> MaybeStatic<Lifetime> {
373 match borrow {
374 ast::Lifetime::Static => MaybeStatic::Static,
375 ast::Lifetime::Named(named) => {
376 MaybeStatic::NonStatic(Lifetime::from_ast(named, self.base.lifetime_env))
377 }
378 ast::Lifetime::Anonymous => match self.elision_source {
379 ElisionSource::SelfParam(lifetime) | ElisionSource::OneParam(lifetime) => lifetime,
380 ElisionSource::NoBorrows => {
381 panic!("nothing to borrow from, this shouldn't pass rustc's checks")
382 }
383 ElisionSource::MultipleBorrows => {
384 panic!("source of elision is ambiguous, this shouldn't pass rustc's checks")
385 }
386 },
387 }
388 }
389
390 fn lower_generics(
391 &mut self,
392 lifetimes: &[ast::Lifetime],
393 type_generics: &ast::LifetimeEnv,
394 is_self: bool,
395 ) -> Lifetimes {
396 if is_self {
397 self.base.self_lifetimes_or_new(lifetimes)
398 } else {
399 self.lower_lifetimes(lifetimes, type_generics)
400 }
401 }
402}
403
404impl LifetimeLowerer for &ast::LifetimeEnv {
405 fn lower_lifetime(&mut self, lifetime: &ast::Lifetime) -> MaybeStatic<Lifetime> {
406 match lifetime {
407 ast::Lifetime::Static => MaybeStatic::Static,
408 ast::Lifetime::Named(named) => MaybeStatic::NonStatic(Lifetime::from_ast(named, self)),
409 ast::Lifetime::Anonymous => {
410 panic!("anonymous lifetime inside struct, this shouldn't pass rustc's checks")
411 }
412 }
413 }
414
415 fn lower_generics(
416 &mut self,
417 lifetimes: &[ast::Lifetime],
418 type_generics: &ast::LifetimeEnv,
419 _: bool,
420 ) -> Lifetimes {
421 self.lower_lifetimes(lifetimes, type_generics)
422 }
423}
424
425// Things to test:
426// 1. ensure that if there are multiple inputs that are `Self`, where `Self` has
427// an elided lifetime, all expansions of `Self` have the same anonymous lifetimes.
428
429#[cfg(test)]
430mod tests {
431 use strck::IntoCk;
432
433 /// Convert a syntax tree into a [`TypeContext`].
434 macro_rules! tcx {
435 ($($tokens:tt)*) => {{
436 let m = crate::ast::Module::from_syn(&syn::parse_quote! { $($tokens)* }, true);
437
438 let mut env = crate::Env::default();
439 let mut top_symbols = crate::ModuleEnv::new(Default::default());
440
441 m.insert_all_types(crate::ast::Path::empty(), &mut env);
442 top_symbols.insert(m.name.clone(), crate::ast::ModSymbol::SubModule(m.name.clone()));
443
444 env.insert(crate::ast::Path::empty(), top_symbols);
445
446 let mut backend = crate::hir::BasicAttributeValidator::new("test-backend");
447 backend.support.static_slices = true;
448
449 // Don't run validation: it will error on elision. We want this code to support
450 // elision even if we don't actually allow it, since good diagnostics involve understanding
451 // broken code.
452 let (_, tcx) = crate::hir::TypeContext::from_ast_without_validation(&env, Default::default(), backend).unwrap();
453
454 tcx
455 }}
456 }
457
458 macro_rules! do_test {
459 ($($tokens:tt)*) => {{
460 let mut settings = insta::Settings::new();
461 settings.set_sort_maps(true);
462
463 settings.bind(|| {
464 let tcx = tcx! { $($tokens)* };
465
466 insta::assert_debug_snapshot!(tcx);
467 })
468 }}
469 }
470
471 #[test]
472 fn simple_mod() {
473 do_test! {
474 mod ffi {
475 #[diplomat::opaque]
476 struct Opaque<'a> {
477 s: DiplomatStrSlice<'a>,
478 }
479
480 struct Struct<'a> {
481 s: DiplomatStrSlice<'a>,
482 }
483
484 #[diplomat::out]
485 struct OutStruct<'a> {
486 inner: Box<Opaque<'a>>,
487 }
488
489 impl<'a> OutStruct<'a> {
490 pub fn new(s: &'a DiplomatStr) -> Self {
491 Self { inner: Box::new(Opaque { s }) }
492 }
493
494 }
495
496 impl<'a> Struct<'a> {
497 pub fn rustc_elision(self, s: &DiplomatStr) -> &DiplomatStr {
498 s
499 }
500 }
501 }
502 }
503 }
504
505 #[test]
506 fn test_elision_in_struct() {
507 let tcx = tcx! {
508 mod ffi {
509 #[diplomat::opaque]
510 struct Opaque;
511
512 #[diplomat::opaque]
513 struct Opaque2<'a>(&'a str);
514
515 impl Opaque {
516 // This should have two elided lifetimes
517 pub fn elided(&self, x: &Opaque2) {
518
519 }
520 }
521 }
522 };
523
524 let method = &tcx
525 .opaques()
526 .iter()
527 .find(|def| def.name == "Opaque")
528 .unwrap()
529 .methods[0];
530
531 assert_eq!(
532 method.lifetime_env.num_lifetimes(),
533 3,
534 "elided() must have three anon lifetimes"
535 );
536 insta::assert_debug_snapshot!(method);
537 }
538
539 #[test]
540 fn test_borrowing_fields() {
541 use std::collections::BTreeMap;
542 use std::fmt;
543
544 let tcx = tcx! {
545 mod ffi {
546 #[diplomat::opaque]
547 pub struct Opaque;
548
549 struct Input<'p, 'q> {
550 p_data: &'p Opaque,
551 q_data: &'q Opaque,
552 name: DiplomatStrSlice<'static>,
553 inner: Inner<'q>,
554 }
555
556 struct Inner<'a> {
557 more_data: DiplomatStrSlice<'a>,
558 }
559
560 struct Output<'p,'q> {
561 p_data: &'p Opaque,
562 q_data: &'q Opaque,
563 }
564
565 impl<'a, 'b> Input<'a, 'b> {
566 pub fn as_output(self, _s: &'static DiplomatStr) -> Output<'b, 'a> {
567 Output { data: self.data }
568 }
569
570 }
571 }
572 };
573
574 let method = &tcx
575 .structs()
576 .iter()
577 .find(|def| def.name == "Input")
578 .unwrap()
579 .methods[0];
580
581 let visitor = method.borrowing_field_visitor(&tcx, "this".ck().unwrap());
582 let mut lt_to_borrowing_fields: BTreeMap<_, Vec<_>> = BTreeMap::new();
583 visitor.visit_borrowing_fields(|lt, bf| {
584 lt_to_borrowing_fields
585 .entry(lt)
586 .or_default()
587 .push(DebugBorrowingField(bf));
588 });
589
590 struct DebugBorrowingField<'m>(crate::hir::borrowing_field::BorrowingField<'m>);
591
592 impl<'m> fmt::Debug for DebugBorrowingField<'m> {
593 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594 f.write_str("\"")?;
595 self.0.try_backtrace(|i, ident| {
596 if i != 0 {
597 f.write_str(".")?;
598 }
599 f.write_str(ident.as_str())
600 })?;
601 f.write_str("\"")
602 }
603 }
604
605 let mut settings = insta::Settings::new();
606 settings.set_sort_maps(true);
607
608 settings.bind(|| {
609 insta::assert_debug_snapshot!(lt_to_borrowing_fields);
610 })
611 }
612}