Skip to main content

rustc_mir_build/
check_tail_calls.rs

1use rustc_abi::ExternAbi;
2use rustc_data_structures::stack::ensure_sufficient_stack;
3use rustc_errors::Applicability;
4use rustc_hir::LangItem;
5use rustc_hir::def::DefKind;
6use rustc_hir::def_id::CRATE_DEF_ID;
7use rustc_middle::middle::codegen_fn_attrs::CodegenFnAttrFlags;
8use rustc_middle::span_bug;
9use rustc_middle::thir::visit::{self, Visitor};
10use rustc_middle::thir::{BodyTy, Expr, ExprId, ExprKind, Thir};
11use rustc_middle::ty::{self, Ty, TyCtxt};
12use rustc_span::def_id::{DefId, LocalDefId};
13use rustc_span::{ErrorGuaranteed, Span};
14
15pub(crate) fn check_tail_calls(tcx: TyCtxt<'_>, def: LocalDefId) -> Result<(), ErrorGuaranteed> {
16    let (thir, expr) = tcx.thir_body(def)?;
17    let thir = &thir.borrow();
18
19    // If `thir` is empty, a type error occurred, skip this body.
20    if thir.exprs.is_empty() {
21        return Ok(());
22    }
23
24    let is_closure = #[allow(non_exhaustive_omitted_patterns)] match tcx.def_kind(def) {
    DefKind::Closure => true,
    _ => false,
}matches!(tcx.def_kind(def), DefKind::Closure);
25
26    let mut visitor = TailCallCkVisitor {
27        tcx,
28        thir,
29        found_errors: Ok(()),
30        typing_env: ty::TypingEnv::post_typeck_until_borrowck_for_mir_build(tcx, def),
31        is_closure,
32        caller_def_id: def,
33    };
34
35    visitor.visit_expr(&thir[expr]);
36
37    visitor.found_errors
38}
39
40struct TailCallCkVisitor<'a, 'tcx> {
41    tcx: TyCtxt<'tcx>,
42    thir: &'a Thir<'tcx>,
43    typing_env: ty::TypingEnv<'tcx>,
44    /// Whatever the currently checked body is one of a closure
45    is_closure: bool,
46    /// The result of the checks, `Err(_)` if there was a problem with some
47    /// tail call, `Ok(())` if all of them were fine.
48    found_errors: Result<(), ErrorGuaranteed>,
49    /// `LocalDefId` of the caller function.
50    caller_def_id: LocalDefId,
51}
52
53impl<'tcx> TailCallCkVisitor<'_, 'tcx> {
54    fn check_tail_call(&mut self, call: &Expr<'_>, expr: &Expr<'_>) {
55        if self.is_closure {
56            self.report_in_closure(expr);
57            return;
58        }
59
60        let BodyTy::Fn(caller_sig) = self.thir.body_type else {
61            ::rustc_middle::util::bug::span_bug_fmt(call.span,
    format_args!("`become` outside of functions should have been disallowed by hir_typeck"))span_bug!(
62                call.span,
63                "`become` outside of functions should have been disallowed by hir_typeck"
64            )
65        };
66        // While the `caller_sig` does have its free regions erased, it does not have its
67        // binders anonymized. We call `erase_and_anonymize_regions` once again to anonymize any binders
68        // within the signature, such as in function pointer or `dyn Trait` args.
69        let caller_sig = self.tcx.erase_and_anonymize_regions(caller_sig);
70
71        let ExprKind::Scope { value, .. } = call.kind else {
72            ::rustc_middle::util::bug::span_bug_fmt(call.span,
    format_args!("expected scope, found: {0:?}", call))span_bug!(call.span, "expected scope, found: {call:?}")
73        };
74        let value = &self.thir[value];
75
76        if #[allow(non_exhaustive_omitted_patterns)] match value.kind {
    ExprKind::Binary { .. } | ExprKind::Unary { .. } | ExprKind::AssignOp { ..
        } | ExprKind::Index { .. } => true,
    _ => false,
}matches!(
77            value.kind,
78            ExprKind::Binary { .. }
79                | ExprKind::Unary { .. }
80                | ExprKind::AssignOp { .. }
81                | ExprKind::Index { .. }
82        ) {
83            self.report_builtin_op(call, expr);
84            return;
85        }
86
87        let ExprKind::Call { ty, fun, ref args, from_hir_call, fn_span } = value.kind else {
88            self.report_non_call(value, expr);
89            return;
90        };
91
92        if !from_hir_call {
93            self.report_op(ty, args, fn_span, expr);
94        }
95
96        if let &ty::FnDef(did, args) = ty.kind() {
97            // Closures in thir look something akin to
98            // `for<'a> extern "rust-call" fn(&'a [closure@...], ()) -> <[closure@...] as FnOnce<()>>::Output {<[closure@...] as Fn<()>>::call}`
99            // So we have to check for them in this weird way...
100            let parent = self.tcx.parent(did);
101            if self.tcx.fn_trait_kind_from_def_id(parent).is_some()
102                && let Some(this) = args.first()
103                && let Some(this) = this.as_type()
104            {
105                if this.is_closure() {
106                    self.report_calling_closure(&self.thir[fun], args[1].as_type().unwrap(), expr);
107                } else {
108                    // This can happen when tail calling `Box` that wraps a function
109                    self.report_nonfn_callee(fn_span, self.thir[fun].span, this);
110                }
111
112                // Tail calling is likely to cause unrelated errors (ABI, argument mismatches),
113                // skip them, producing an error about calling a closure is enough.
114                return;
115            };
116
117            if self.tcx.intrinsic(did).is_some() {
118                self.report_calling_intrinsic(expr);
119            }
120        }
121
122        let (ty::FnDef(..) | ty::FnPtr(..)) = ty.kind() else {
123            self.report_nonfn_callee(fn_span, self.thir[fun].span, ty);
124
125            // `fn_sig` below panics otherwise
126            return;
127        };
128
129        // Erase regions since tail calls don't care about lifetimes
130        let callee_sig =
131            self.tcx.normalize_erasing_late_bound_regions(self.typing_env, ty.fn_sig(self.tcx));
132
133        if caller_sig.abi() != callee_sig.abi() {
134            self.report_abi_mismatch(expr.span, caller_sig.abi(), callee_sig.abi());
135        }
136
137        if !callee_sig.abi().supports_guaranteed_tail_call() {
138            self.report_unsupported_abi(expr.span, callee_sig.abi());
139        }
140
141        // FIXME(explicit_tail_calls): this currently fails for cases where opaques are used.
142        // e.g.
143        // ```
144        // fn a() -> impl Sized { become b() } // ICE
145        // fn b() -> u8 { 0 }
146        // ```
147        // we should think what is the expected behavior here.
148        // (we should probably just accept this by revealing opaques?)
149        if caller_sig.inputs_and_output != callee_sig.inputs_and_output
150            && !#[allow(non_exhaustive_omitted_patterns)] match callee_sig.abi() {
    ExternAbi::RustTail => true,
    _ => false,
}matches!(callee_sig.abi(), ExternAbi::RustTail)
151        {
152            let caller_ty = self.tcx.type_of(self.caller_def_id).skip_binder();
153
154            self.report_signature_mismatch(
155                expr.span,
156                self.tcx.liberate_late_bound_regions(
157                    CRATE_DEF_ID.to_def_id(),
158                    caller_ty.fn_sig(self.tcx),
159                ),
160                self.tcx.liberate_late_bound_regions(CRATE_DEF_ID.to_def_id(), ty.fn_sig(self.tcx)),
161            );
162        }
163
164        {
165            // `#[track_caller]` affects the ABI of a function (by adding a location argument),
166            // so a `track_caller` can only tail call other `track_caller` functions.
167            //
168            // The issue is however that we can't know if a function is `track_caller` or not at
169            // this point (THIR can be polymorphic, we may have an unresolved trait function).
170            // We could only allow functions that we *can* resolve and *are* `track_caller`,
171            // but that would turn changing `track_caller`-ness into a breaking change,
172            // which is probably undesirable.
173            //
174            // Also note that we don't check callee's `track_caller`-ness at all, mostly for the
175            // reasons above, but also because we can always tailcall the shim we'd generate for
176            // coercing the function to an `fn()` pointer. (although in that case the tailcall is
177            // basically useless -- the shim calls the actual function, so tailcalling the shim is
178            // equivalent to calling the function)
179            let caller_needs_location = self.caller_needs_location();
180
181            if caller_needs_location {
182                self.report_track_caller_caller(expr.span);
183            }
184        }
185
186        if caller_sig.c_variadic() {
187            self.report_c_variadic_caller(expr.span);
188        }
189
190        if callee_sig.c_variadic() {
191            self.report_c_variadic_callee(expr.span);
192        }
193
194        for &arg_ty in callee_sig.inputs() {
195            if !arg_ty.is_sized(self.tcx, self.typing_env) {
196                self.report_unsized_argument(expr.span, arg_ty);
197            }
198        }
199    }
200
201    /// Returns true if the caller function needs a location argument
202    /// (i.e. if a function is marked as `#[track_caller]`)
203    fn caller_needs_location(&self) -> bool {
204        let flags = self.tcx.codegen_fn_attrs(self.caller_def_id).flags;
205        flags.contains(CodegenFnAttrFlags::TRACK_CALLER)
206    }
207
208    fn report_in_closure(&mut self, expr: &Expr<'_>) {
209        let err = self.tcx.dcx().span_err(expr.span, "`become` is not allowed in closures");
210        self.found_errors = Err(err);
211    }
212
213    fn report_builtin_op(&mut self, value: &Expr<'_>, expr: &Expr<'_>) {
214        let err = self
215            .tcx
216            .dcx()
217            .struct_span_err(value.span, "`become` does not support operators")
218            .with_note("using `become` on a builtin operator is not useful")
219            .with_span_suggestion(
220                value.span.until(expr.span),
221                "try using `return` instead",
222                "return ",
223                Applicability::MachineApplicable,
224            )
225            .emit();
226        self.found_errors = Err(err);
227    }
228
229    fn report_op(&mut self, fun_ty: Ty<'_>, args: &[ExprId], fn_span: Span, expr: &Expr<'_>) {
230        let mut err =
231            self.tcx.dcx().struct_span_err(fn_span, "`become` does not support operators");
232
233        if let &ty::FnDef(did, _substs) = fun_ty.kind()
234            && let parent = self.tcx.parent(did)
235            && #[allow(non_exhaustive_omitted_patterns)] match self.tcx.def_kind(parent) {
    DefKind::Trait => true,
    _ => false,
}matches!(self.tcx.def_kind(parent), DefKind::Trait)
236            && let Some(method) = op_trait_as_method_name(self.tcx, parent)
237        {
238            match args {
239                &[arg] => {
240                    let arg = &self.thir[arg];
241
242                    err.multipart_suggestion(
243                        "try using the method directly",
244                        ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [(fn_span.shrink_to_lo().until(arg.span), "(".to_owned()),
                (arg.span.shrink_to_hi(),
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!(").{0}()", method))
                        }))]))vec![
245                            (fn_span.shrink_to_lo().until(arg.span), "(".to_owned()),
246                            (arg.span.shrink_to_hi(), format!(").{method}()")),
247                        ],
248                        Applicability::MaybeIncorrect,
249                    );
250                }
251                &[lhs, rhs] => {
252                    let lhs = &self.thir[lhs];
253                    let rhs = &self.thir[rhs];
254
255                    err.multipart_suggestion(
256                        "try using the method directly",
257                        ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [(lhs.span.shrink_to_lo(),
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("("))
                        })),
                (lhs.span.between(rhs.span),
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!(").{0}(", method))
                        })),
                (rhs.span.between(expr.span.shrink_to_hi()),
                    ")".to_owned())]))vec![
258                            (lhs.span.shrink_to_lo(), format!("(")),
259                            (lhs.span.between(rhs.span), format!(").{method}(")),
260                            (rhs.span.between(expr.span.shrink_to_hi()), ")".to_owned()),
261                        ],
262                        Applicability::MaybeIncorrect,
263                    );
264                }
265                _ => ::rustc_middle::util::bug::span_bug_fmt(expr.span,
    format_args!("operator with more than 2 args? {0:?}", args))span_bug!(expr.span, "operator with more than 2 args? {args:?}"),
266            }
267        }
268
269        self.found_errors = Err(err.emit());
270    }
271
272    fn report_non_call(&mut self, value: &Expr<'_>, expr: &Expr<'_>) {
273        let err = self
274            .tcx
275            .dcx()
276            .struct_span_err(value.span, "`become` requires a function call")
277            .with_span_note(value.span, "not a function call")
278            .with_span_suggestion(
279                value.span.until(expr.span),
280                "try using `return` instead",
281                "return ",
282                Applicability::MaybeIncorrect,
283            )
284            .emit();
285        self.found_errors = Err(err);
286    }
287
288    fn report_calling_closure(&mut self, fun: &Expr<'_>, tupled_args: Ty<'_>, expr: &Expr<'_>) {
289        let underscored_args = match tupled_args.kind() {
290            ty::Tuple(tys) if tys.is_empty() => "".to_owned(),
291            ty::Tuple(tys) => std::iter::repeat_n("_, ", tys.len() - 1).chain(["_"]).collect(),
292            _ => "_".to_owned(),
293        };
294
295        let err = self
296            .tcx
297            .dcx()
298            .struct_span_err(expr.span, "tail calling closures directly is not allowed")
299            .with_multipart_suggestion(
300                "try casting the closure to a function pointer type",
301                ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [(fun.span.shrink_to_lo(), "(".to_owned()),
                (fun.span.shrink_to_hi(),
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!(" as fn({0}) -> _)",
                                    underscored_args))
                        }))]))vec![
302                    (fun.span.shrink_to_lo(), "(".to_owned()),
303                    (fun.span.shrink_to_hi(), format!(" as fn({underscored_args}) -> _)")),
304                ],
305                Applicability::MaybeIncorrect,
306            )
307            .emit();
308        self.found_errors = Err(err);
309    }
310
311    fn report_calling_intrinsic(&mut self, expr: &Expr<'_>) {
312        let err = self
313            .tcx
314            .dcx()
315            .struct_span_err(expr.span, "tail calling intrinsics is not allowed")
316            .emit();
317
318        self.found_errors = Err(err);
319    }
320
321    fn report_nonfn_callee(&mut self, call_sp: Span, fun_sp: Span, ty: Ty<'_>) {
322        let mut err = self
323            .tcx
324            .dcx()
325            .struct_span_err(
326                call_sp,
327                "tail calls can only be performed with function definitions or pointers",
328            )
329            .with_note(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("callee has type `{0}`", ty))
    })format!("callee has type `{ty}`"));
330
331        let mut ty = ty;
332        let mut refs = 0;
333        while ty.is_box() || ty.is_ref() {
334            ty = ty.builtin_deref(false).unwrap();
335            refs += 1;
336        }
337
338        if refs > 0 && ty.is_fn() {
339            let thing = if ty.is_fn_ptr() { "pointer" } else { "definition" };
340
341            let derefs =
342                std::iter::once('(').chain(std::iter::repeat_n('*', refs)).collect::<String>();
343
344            err.multipart_suggestion(
345                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("consider dereferencing the expression to get a function {0}",
                thing))
    })format!("consider dereferencing the expression to get a function {thing}"),
346                ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [(fun_sp.shrink_to_lo(), derefs),
                (fun_sp.shrink_to_hi(), ")".to_owned())]))vec![(fun_sp.shrink_to_lo(), derefs), (fun_sp.shrink_to_hi(), ")".to_owned())],
347                Applicability::MachineApplicable,
348            );
349        }
350
351        let err = err.emit();
352        self.found_errors = Err(err);
353    }
354
355    fn report_abi_mismatch(&mut self, sp: Span, caller_abi: ExternAbi, callee_abi: ExternAbi) {
356        let err = self
357            .tcx
358            .dcx()
359            .struct_span_err(sp, "mismatched function ABIs")
360            .with_note("`become` requires caller and callee to have the same ABI")
361            .with_note(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("caller ABI is `{0}`, while callee ABI is `{1}`",
                caller_abi, callee_abi))
    })format!("caller ABI is `{caller_abi}`, while callee ABI is `{callee_abi}`"))
362            .emit();
363        self.found_errors = Err(err);
364    }
365
366    fn report_unsupported_abi(&mut self, sp: Span, callee_abi: ExternAbi) {
367        let err = self
368            .tcx
369            .dcx()
370            .struct_span_err(sp, "ABI does not support guaranteed tail calls")
371            .with_note(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`become` is not supported for `extern {0}` functions",
                callee_abi))
    })format!("`become` is not supported for `extern {callee_abi}` functions"))
372            .emit();
373        self.found_errors = Err(err);
374    }
375
376    fn report_signature_mismatch(
377        &mut self,
378        sp: Span,
379        caller_sig: ty::FnSig<'_>,
380        callee_sig: ty::FnSig<'_>,
381    ) {
382        let err = self
383            .tcx
384            .dcx()
385            .struct_span_err(sp, "mismatched signatures")
386            .with_note("`become` requires caller and callee to have matching signatures")
387            .with_note(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("caller signature: `{0}`",
                caller_sig))
    })format!("caller signature: `{caller_sig}`"))
388            .with_note(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("callee signature: `{0}`",
                callee_sig))
    })format!("callee signature: `{callee_sig}`"))
389            .emit();
390        self.found_errors = Err(err);
391    }
392
393    fn report_track_caller_caller(&mut self, sp: Span) {
394        let err = self
395            .tcx
396            .dcx()
397            .struct_span_err(
398                sp,
399                "a function marked with `#[track_caller]` cannot perform a tail-call",
400            )
401            .emit();
402
403        self.found_errors = Err(err);
404    }
405
406    fn report_c_variadic_caller(&mut self, sp: Span) {
407        let err = self
408            .tcx
409            .dcx()
410            // FIXME(explicit_tail_calls): highlight the `...`
411            .struct_span_err(sp, "tail-calls are not allowed in c-variadic functions")
412            .emit();
413
414        self.found_errors = Err(err);
415    }
416
417    fn report_c_variadic_callee(&mut self, sp: Span) {
418        let err = self
419            .tcx
420            .dcx()
421            // FIXME(explicit_tail_calls): highlight the function or something...
422            .struct_span_err(sp, "c-variadic functions can't be tail-called")
423            .emit();
424
425        self.found_errors = Err(err);
426    }
427
428    fn report_unsized_argument(&mut self, sp: Span, arg_ty: Ty<'tcx>) {
429        let err = self
430            .tcx
431            .dcx()
432            .struct_span_err(sp, ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unsized arguments cannot be used in a tail call"))
    })format!("unsized arguments cannot be used in a tail call"))
433            .with_note(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unsized argument of type `{0}`",
                arg_ty))
    })format!("unsized argument of type `{arg_ty}`"))
434            .emit();
435
436        self.found_errors = Err(err);
437    }
438}
439
440impl<'a, 'tcx> Visitor<'a, 'tcx> for TailCallCkVisitor<'a, 'tcx> {
441    fn thir(&self) -> &'a Thir<'tcx> {
442        &self.thir
443    }
444
445    fn visit_expr(&mut self, expr: &'a Expr<'tcx>) {
446        ensure_sufficient_stack(|| {
447            if let ExprKind::Become { value } = expr.kind {
448                let call = &self.thir[value];
449                self.check_tail_call(call, expr);
450            }
451
452            visit::walk_expr(self, expr);
453        });
454    }
455}
456
457fn op_trait_as_method_name(tcx: TyCtxt<'_>, trait_did: DefId) -> Option<&'static str> {
458    let m = match tcx.as_lang_item(trait_did)? {
459        LangItem::Add => "add",
460        LangItem::Sub => "sub",
461        LangItem::Mul => "mul",
462        LangItem::Div => "div",
463        LangItem::Rem => "rem",
464        LangItem::Neg => "neg",
465        LangItem::Not => "not",
466        LangItem::BitXor => "bitxor",
467        LangItem::BitAnd => "bitand",
468        LangItem::BitOr => "bitor",
469        LangItem::Shl => "shl",
470        LangItem::Shr => "shr",
471        LangItem::AddAssign => "add_assign",
472        LangItem::SubAssign => "sub_assign",
473        LangItem::MulAssign => "mul_assign",
474        LangItem::DivAssign => "div_assign",
475        LangItem::RemAssign => "rem_assign",
476        LangItem::BitXorAssign => "bitxor_assign",
477        LangItem::BitAndAssign => "bitand_assign",
478        LangItem::BitOrAssign => "bitor_assign",
479        LangItem::ShlAssign => "shl_assign",
480        LangItem::ShrAssign => "shr_assign",
481        LangItem::Index => "index",
482        LangItem::IndexMut => "index_mut",
483        _ => return None,
484    };
485
486    Some(m)
487}