Skip to main content

clippy_utils/
eager_or_lazy.rs

1//! Utilities for evaluating whether eagerly evaluated expressions can be made lazy and vice versa.
2//!
3//! Things to consider:
4//!  - does the expression have side-effects?
5//!  - is the expression computationally expensive?
6//!
7//! See lints:
8//!  - unnecessary-lazy-evaluations
9//!  - or-fun-call
10//!  - option-if-let-else
11
12use crate::consts::{ConstEvalCtxt, FullInt};
13use crate::sym;
14use crate::ty::all_predicates_of;
15use crate::visitors::is_const_evaluatable;
16use rustc_hir::def::{DefKind, Res};
17use rustc_hir::def_id::DefId;
18use rustc_hir::intravisit::{Visitor, walk_expr};
19use rustc_hir::{BinOpKind, Block, Expr, ExprKind, QPath, UnOp};
20use rustc_lint::LateContext;
21use rustc_middle::ty::adjustment::{Adjust, DerefAdjustKind};
22use rustc_middle::ty::{self, TyCtxt};
23use rustc_span::Symbol;
24use std::{cmp, ops};
25
26#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27enum EagernessSuggestion {
28    // The expression is cheap and should be evaluated eagerly
29    Eager,
30    // The expression may be cheap, so don't suggested lazy evaluation; or the expression may not be safe to switch to
31    // eager evaluation.
32    NoChange,
33    // The expression is likely expensive and should be evaluated lazily.
34    Lazy,
35    // The expression cannot be placed into a closure.
36    ForceNoChange,
37}
38impl ops::BitOr for EagernessSuggestion {
39    type Output = Self;
40    fn bitor(self, rhs: Self) -> Self {
41        cmp::max(self, rhs)
42    }
43}
44impl ops::BitOrAssign for EagernessSuggestion {
45    fn bitor_assign(&mut self, rhs: Self) {
46        *self = *self | rhs;
47    }
48}
49
50/// Determine the eagerness of the given function call.
51fn fn_eagerness(tcx: TyCtxt<'_>, fn_id: DefId, name: Symbol, have_one_arg: bool) -> EagernessSuggestion {
52    use EagernessSuggestion::{Eager, Lazy, NoChange};
53
54    let ty = match tcx.impl_of_assoc(fn_id) {
55        Some(id) => tcx.type_of(id).instantiate_identity().skip_norm_wip(),
56        None => return Lazy,
57    };
58
59    if (matches!(name, sym::is_empty | sym::len) || name.as_str().starts_with("as_")) && have_one_arg {
60        if matches!(
61            tcx.crate_name(fn_id.krate),
62            sym::std | sym::core | sym::alloc | sym::proc_macro
63        ) {
64            Eager
65        } else {
66            NoChange
67        }
68    } else if let ty::Adt(def, subs) = ty.kind() {
69        // Types where the only fields are generic types (or references to) with no trait bounds other
70        // than marker traits.
71        // Due to the limited operations on these types functions should be fairly cheap.
72        if def.variants().iter().flat_map(|v| v.fields.iter()).any(|x| {
73            matches!(
74                tcx.type_of(x.did)
75                    .instantiate_identity()
76                    .skip_norm_wip()
77                    .peel_refs()
78                    .kind(),
79                ty::Param(_)
80            )
81        }) && all_predicates_of(tcx, fn_id).all(|(pred, _)| match pred.kind().skip_binder() {
82            ty::ClauseKind::Trait(pred) => tcx.trait_def(pred.trait_ref.def_id).is_marker,
83            _ => true,
84        }) && subs.types().all(|x| matches!(x.peel_refs().kind(), ty::Param(_)))
85        {
86            // Limit the function to either `(self) -> bool` or `(&self) -> bool`
87            match &**tcx
88                .fn_sig(fn_id)
89                .instantiate_identity()
90                .skip_norm_wip()
91                .skip_binder()
92                .inputs_and_output
93            {
94                [arg, res] if !arg.is_mutable_ptr() && arg.peel_refs() == ty && res.is_bool() => NoChange,
95                _ => Lazy,
96            }
97        } else {
98            Lazy
99        }
100    } else {
101        Lazy
102    }
103}
104
105fn res_has_significant_drop(res: Res, ecx: &ConstEvalCtxt<'_>, e: &Expr<'_>) -> bool {
106    if let Res::Def(DefKind::Ctor(..) | DefKind::Variant | DefKind::Enum | DefKind::Struct, _)
107    | Res::SelfCtor(_)
108    | Res::SelfTyAlias { .. } = res
109    {
110        ecx.typeck.expr_ty(e).has_significant_drop(ecx.tcx, ecx.typing_env)
111    } else {
112        false
113    }
114}
115
116#[expect(clippy::too_many_lines)]
117fn expr_eagerness<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> EagernessSuggestion {
118    struct V<'tcx> {
119        ecx: ConstEvalCtxt<'tcx>,
120        eagerness: EagernessSuggestion,
121    }
122
123    impl<'tcx> Visitor<'tcx> for V<'tcx> {
124        fn visit_expr(&mut self, e: &'tcx Expr<'_>) {
125            use EagernessSuggestion::{ForceNoChange, Lazy, NoChange};
126            if self.eagerness == ForceNoChange {
127                return;
128            }
129
130            // Autoderef through a user-defined `Deref` impl can have side-effects,
131            // so don't suggest changing it.
132            if self
133                .ecx
134                .typeck
135                .expr_adjustments(e)
136                .iter()
137                .any(|adj| matches!(adj.kind, Adjust::Deref(DerefAdjustKind::Overloaded(_))))
138            {
139                self.eagerness |= NoChange;
140                return;
141            }
142
143            match e.kind {
144                ExprKind::Call(
145                    &Expr {
146                        kind: ExprKind::Path(ref path),
147                        hir_id,
148                        ..
149                    },
150                    args,
151                ) => match self.ecx.typeck.qpath_res(path, hir_id) {
152                    res @ (Res::Def(DefKind::Ctor(..) | DefKind::Variant, _) | Res::SelfCtor(_)) => {
153                        if res_has_significant_drop(res, &self.ecx, e) {
154                            self.eagerness = ForceNoChange;
155                            return;
156                        }
157                    },
158                    Res::Def(_, id) if self.ecx.tcx.is_promotable_const_fn(id) => (),
159                    // No need to walk the arguments here, `is_const_evaluatable` already did
160                    Res::Def(..) if is_const_evaluatable(self.ecx.tcx, self.ecx.typeck, e) => {
161                        self.eagerness |= NoChange;
162                        return;
163                    },
164                    Res::Def(_, id) => match path {
165                        QPath::Resolved(_, p) => {
166                            self.eagerness |= fn_eagerness(
167                                self.ecx.tcx,
168                                id,
169                                p.segments.last().unwrap().ident.name,
170                                !args.is_empty(),
171                            );
172                        },
173                        QPath::TypeRelative(_, name) => {
174                            self.eagerness |= fn_eagerness(self.ecx.tcx, id, name.ident.name, !args.is_empty());
175                        },
176                    },
177                    _ => self.eagerness = Lazy,
178                },
179                // No need to walk the arguments here, `is_const_evaluatable` already did
180                ExprKind::MethodCall(..) if is_const_evaluatable(self.ecx.tcx, self.ecx.typeck, e) => {
181                    self.eagerness |= NoChange;
182                    return;
183                },
184                #[expect(clippy::match_same_arms)] // arm pattern can't be merged due to `ref`, see rust#105778
185                ExprKind::Struct(path, ..) => {
186                    if res_has_significant_drop(self.ecx.typeck.qpath_res(path, e.hir_id), &self.ecx, e) {
187                        self.eagerness = ForceNoChange;
188                        return;
189                    }
190                },
191                ExprKind::Path(ref path) => {
192                    if res_has_significant_drop(self.ecx.typeck.qpath_res(path, e.hir_id), &self.ecx, e) {
193                        self.eagerness = ForceNoChange;
194                        return;
195                    }
196                },
197                ExprKind::MethodCall(name, ..) => {
198                    self.eagerness |= self
199                        .ecx
200                        .typeck
201                        .type_dependent_def_id(e.hir_id)
202                        .map_or(Lazy, |id| fn_eagerness(self.ecx.tcx, id, name.ident.name, true));
203                },
204                ExprKind::Index(_, e, _) => {
205                    let ty = self.ecx.typeck.expr_ty_adjusted(e);
206                    if self.ecx.tcx.type_is_copy_modulo_regions(self.ecx.typing_env, ty) && !ty.is_ref() {
207                        self.eagerness |= NoChange;
208                    } else {
209                        self.eagerness = Lazy;
210                    }
211                },
212
213                // `-i32::MIN` panics with overflow checks
214                ExprKind::Unary(UnOp::Neg, right) if self.ecx.eval(right).is_none() => {
215                    self.eagerness |= NoChange;
216                },
217
218                // Custom `Deref` impl might have side effects
219                ExprKind::Unary(UnOp::Deref, e) if self.ecx.typeck.expr_ty(e).builtin_deref(true).is_none() => {
220                    self.eagerness |= NoChange;
221                },
222                // Dereferences should be cheap, but dereferencing a raw pointer earlier may not be safe.
223                ExprKind::Unary(UnOp::Deref, e) if !self.ecx.typeck.expr_ty(e).is_raw_ptr() => (),
224                ExprKind::Unary(UnOp::Deref, _) => self.eagerness |= NoChange,
225                ExprKind::Unary(_, e)
226                    if matches!(self.ecx.typeck.expr_ty(e).kind(), ty::Bool | ty::Int(_) | ty::Uint(_),) => {},
227
228                // `>>` and `<<` panic when the right-hand side is greater than or equal to the number of bits in the
229                // type of the left-hand side, or is negative.
230                // We intentionally only check if the right-hand isn't a constant, because even if the suggestion would
231                // overflow with constants, the compiler emits an error for it and the programmer will have to fix it.
232                // Thus, we would realistically only delay the lint.
233                ExprKind::Binary(op, _, right)
234                    if matches!(op.node, BinOpKind::Shl | BinOpKind::Shr) && self.ecx.eval(right).is_none() =>
235                {
236                    self.eagerness |= NoChange;
237                },
238
239                ExprKind::Binary(op, left, right)
240                    if matches!(op.node, BinOpKind::Div | BinOpKind::Rem)
241                        && let right_ty = self.ecx.typeck.expr_ty(right)
242                        && let left = self.ecx.eval(left)
243                        && let right = self.ecx.eval(right).and_then(|c| c.int_value(self.ecx.tcx, right_ty))
244                        && matches!(
245                            (left, right),
246                            // `1 / x`: x might be zero
247                            (_, None)
248                            // `x / -1`: x might be T::MIN
249                            | (None, Some(FullInt::S(-1)))
250                        ) =>
251                {
252                    self.eagerness |= NoChange;
253                },
254
255                // Similar to `>>` and `<<`, we only want to avoid linting entirely if either side is unknown and the
256                // compiler can't emit an error for an overflowing expression.
257                // Suggesting eagerness for `true.then(|| i32::MAX + 1)` is okay because the compiler will emit an
258                // error and it's good to have the eagerness warning up front when the user fixes the logic error.
259                ExprKind::Binary(op, left, right)
260                    if matches!(op.node, BinOpKind::Add | BinOpKind::Sub | BinOpKind::Mul)
261                        && !self.ecx.typeck.expr_ty(e).is_floating_point()
262                        && (self.ecx.eval(left).is_none() || self.ecx.eval(right).is_none()) =>
263                {
264                    self.eagerness |= NoChange;
265                },
266
267                ExprKind::Binary(_, lhs, rhs)
268                    if self.ecx.typeck.expr_ty(lhs).is_primitive() && self.ecx.typeck.expr_ty(rhs).is_primitive() => {},
269
270                // Can't be moved into a closure
271                ExprKind::Break(..)
272                | ExprKind::Continue(_)
273                | ExprKind::Ret(_)
274                | ExprKind::Become(_)
275                | ExprKind::InlineAsm(_)
276                | ExprKind::Yield(..)
277                | ExprKind::Err(_) => {
278                    self.eagerness = ForceNoChange;
279                    return;
280                },
281
282                // Memory allocation, custom operator, loop, or call to an unknown function
283                ExprKind::Unary(..) | ExprKind::Binary(..) | ExprKind::Loop(..) | ExprKind::Call(..) => {
284                    self.eagerness = Lazy;
285                },
286
287                ExprKind::ConstBlock(_)
288                | ExprKind::Array(_)
289                | ExprKind::Tup(_)
290                | ExprKind::Use(..)
291                | ExprKind::Lit(_)
292                | ExprKind::Cast(..)
293                | ExprKind::Type(..)
294                | ExprKind::DropTemps(_)
295                | ExprKind::Let(..)
296                | ExprKind::If(..)
297                | ExprKind::Match(..)
298                | ExprKind::Closure { .. }
299                | ExprKind::Field(..)
300                | ExprKind::AddrOf(..)
301                | ExprKind::Repeat(..)
302                | ExprKind::Block(Block { stmts: [], .. }, _)
303                | ExprKind::OffsetOf(..)
304                | ExprKind::UnsafeBinderCast(..) => (),
305
306                // Assignment might be to a local defined earlier, so don't eagerly evaluate.
307                // Blocks with multiple statements might be expensive, so don't eagerly evaluate.
308                // TODO: Actually check if either of these are true here.
309                ExprKind::Assign(..) | ExprKind::AssignOp(..) | ExprKind::Block(..) => self.eagerness |= NoChange,
310            }
311            walk_expr(self, e);
312        }
313    }
314
315    let mut v = V {
316        ecx: ConstEvalCtxt::new(cx),
317        eagerness: EagernessSuggestion::Eager,
318    };
319    v.visit_expr(e);
320    v.eagerness
321}
322
323/// Whether the given expression should be changed to evaluate eagerly
324pub fn switch_to_eager_eval<'tcx>(cx: &'_ LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool {
325    expr_eagerness(cx, expr) == EagernessSuggestion::Eager
326}
327
328/// Whether the given expression should be changed to evaluate lazily
329pub fn switch_to_lazy_eval<'tcx>(cx: &'_ LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool {
330    expr_eagerness(cx, expr) == EagernessSuggestion::Lazy
331}