• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //! Utilities for evaluating whether eagerly evaluated expressions can be made lazy and vice versa.
2 //!
3 //! Things to consider:
4 //!  - has the expression 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 
12 use crate::ty::{all_predicates_of, is_copy};
13 use crate::visitors::is_const_evaluatable;
14 use rustc_hir::def::{DefKind, Res};
15 use rustc_hir::intravisit::{walk_expr, Visitor};
16 use rustc_hir::{def_id::DefId, Block, Expr, ExprKind, QPath, UnOp};
17 use rustc_lint::LateContext;
18 use rustc_middle::ty;
19 use rustc_middle::ty::adjustment::Adjust;
20 use rustc_span::{sym, Symbol};
21 use std::cmp;
22 use std::ops;
23 
24 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
25 enum EagernessSuggestion {
26     // The expression is cheap and should be evaluated eagerly
27     Eager,
28     // The expression may be cheap, so don't suggested lazy evaluation; or the expression may not be safe to switch to
29     // eager evaluation.
30     NoChange,
31     // The expression is likely expensive and should be evaluated lazily.
32     Lazy,
33     // The expression cannot be placed into a closure.
34     ForceNoChange,
35 }
36 impl ops::BitOr for EagernessSuggestion {
37     type Output = Self;
bitor(self, rhs: Self) -> Self38     fn bitor(self, rhs: Self) -> Self {
39         cmp::max(self, rhs)
40     }
41 }
42 impl ops::BitOrAssign for EagernessSuggestion {
bitor_assign(&mut self, rhs: Self)43     fn bitor_assign(&mut self, rhs: Self) {
44         *self = *self | rhs;
45     }
46 }
47 
48 /// Determine the eagerness of the given function call.
fn_eagerness(cx: &LateContext<'_>, fn_id: DefId, name: Symbol, have_one_arg: bool) -> EagernessSuggestion49 fn fn_eagerness(cx: &LateContext<'_>, fn_id: DefId, name: Symbol, have_one_arg: bool) -> EagernessSuggestion {
50     use EagernessSuggestion::{Eager, Lazy, NoChange};
51     let name = name.as_str();
52 
53     let ty = match cx.tcx.impl_of_method(fn_id) {
54         Some(id) => cx.tcx.type_of(id).subst_identity(),
55         None => return Lazy,
56     };
57 
58     if (name.starts_with("as_") || name == "len" || name == "is_empty") && have_one_arg {
59         if matches!(
60             cx.tcx.crate_name(fn_id.krate),
61             sym::std | sym::core | sym::alloc | sym::proc_macro
62         ) {
63             Eager
64         } else {
65             NoChange
66         }
67     } else if let ty::Adt(def, subs) = ty.kind() {
68         // Types where the only fields are generic types (or references to) with no trait bounds other
69         // than marker traits.
70         // Due to the limited operations on these types functions should be fairly cheap.
71         if def
72             .variants()
73             .iter()
74             .flat_map(|v| v.fields.iter())
75             .any(|x| matches!(cx.tcx.type_of(x.did).subst_identity().peel_refs().kind(), ty::Param(_)))
76             && all_predicates_of(cx.tcx, fn_id).all(|(pred, _)| match pred.kind().skip_binder() {
77                 ty::ClauseKind::Trait(pred) => cx.tcx.trait_def(pred.trait_ref.def_id).is_marker,
78                 _ => true,
79             })
80             && subs.types().all(|x| matches!(x.peel_refs().kind(), ty::Param(_)))
81         {
82             // Limit the function to either `(self) -> bool` or `(&self) -> bool`
83             match &**cx.tcx.fn_sig(fn_id).subst_identity().skip_binder().inputs_and_output {
84                 [arg, res] if !arg.is_mutable_ptr() && arg.peel_refs() == ty && res.is_bool() => NoChange,
85                 _ => Lazy,
86             }
87         } else {
88             Lazy
89         }
90     } else {
91         Lazy
92     }
93 }
94 
res_has_significant_drop(res: Res, cx: &LateContext<'_>, e: &Expr<'_>) -> bool95 fn res_has_significant_drop(res: Res, cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
96     if let Res::Def(DefKind::Ctor(..) | DefKind::Variant, _) | Res::SelfCtor(_) = res {
97         cx.typeck_results()
98             .expr_ty(e)
99             .has_significant_drop(cx.tcx, cx.param_env)
100     } else {
101         false
102     }
103 }
104 
105 #[expect(clippy::too_many_lines)]
expr_eagerness<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> EagernessSuggestion106 fn expr_eagerness<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> EagernessSuggestion {
107     struct V<'cx, 'tcx> {
108         cx: &'cx LateContext<'tcx>,
109         eagerness: EagernessSuggestion,
110     }
111 
112     impl<'cx, 'tcx> Visitor<'tcx> for V<'cx, 'tcx> {
113         fn visit_expr(&mut self, e: &'tcx Expr<'_>) {
114             use EagernessSuggestion::{ForceNoChange, Lazy, NoChange};
115             if self.eagerness == ForceNoChange {
116                 return;
117             }
118 
119             // Autoderef through a user-defined `Deref` impl can have side-effects,
120             // so don't suggest changing it.
121             if self
122                 .cx
123                 .typeck_results()
124                 .expr_adjustments(e)
125                 .iter()
126                 .any(|adj| matches!(adj.kind, Adjust::Deref(Some(_))))
127             {
128                 self.eagerness |= NoChange;
129                 return;
130             }
131 
132             match e.kind {
133                 ExprKind::Call(
134                     &Expr {
135                         kind: ExprKind::Path(ref path),
136                         hir_id,
137                         ..
138                     },
139                     args,
140                 ) => match self.cx.qpath_res(path, hir_id) {
141                     res @ (Res::Def(DefKind::Ctor(..) | DefKind::Variant, _) | Res::SelfCtor(_)) => {
142                         if res_has_significant_drop(res, self.cx, e) {
143                             self.eagerness = ForceNoChange;
144                             return;
145                         }
146                     },
147                     Res::Def(_, id) if self.cx.tcx.is_promotable_const_fn(id) => (),
148                     // No need to walk the arguments here, `is_const_evaluatable` already did
149                     Res::Def(..) if is_const_evaluatable(self.cx, e) => {
150                         self.eagerness |= NoChange;
151                         return;
152                     },
153                     Res::Def(_, id) => match path {
154                         QPath::Resolved(_, p) => {
155                             self.eagerness |=
156                                 fn_eagerness(self.cx, id, p.segments.last().unwrap().ident.name, !args.is_empty());
157                         },
158                         QPath::TypeRelative(_, name) => {
159                             self.eagerness |= fn_eagerness(self.cx, id, name.ident.name, !args.is_empty());
160                         },
161                         QPath::LangItem(..) => self.eagerness = Lazy,
162                     },
163                     _ => self.eagerness = Lazy,
164                 },
165                 // No need to walk the arguments here, `is_const_evaluatable` already did
166                 ExprKind::MethodCall(..) if is_const_evaluatable(self.cx, e) => {
167                     self.eagerness |= NoChange;
168                     return;
169                 },
170                 ExprKind::Path(ref path) => {
171                     if res_has_significant_drop(self.cx.qpath_res(path, e.hir_id), self.cx, e) {
172                         self.eagerness = ForceNoChange;
173                         return;
174                     }
175                 },
176                 ExprKind::MethodCall(name, ..) => {
177                     self.eagerness |= self
178                         .cx
179                         .typeck_results()
180                         .type_dependent_def_id(e.hir_id)
181                         .map_or(Lazy, |id| fn_eagerness(self.cx, id, name.ident.name, true));
182                 },
183                 ExprKind::Index(_, e) => {
184                     let ty = self.cx.typeck_results().expr_ty_adjusted(e);
185                     if is_copy(self.cx, ty) && !ty.is_ref() {
186                         self.eagerness |= NoChange;
187                     } else {
188                         self.eagerness = Lazy;
189                     }
190                 },
191                 // Custom `Deref` impl might have side effects
192                 ExprKind::Unary(UnOp::Deref, e)
193                     if self.cx.typeck_results().expr_ty(e).builtin_deref(true).is_none() =>
194                 {
195                     self.eagerness |= NoChange;
196                 },
197                 // Dereferences should be cheap, but dereferencing a raw pointer earlier may not be safe.
198                 ExprKind::Unary(UnOp::Deref, e) if !self.cx.typeck_results().expr_ty(e).is_unsafe_ptr() => (),
199                 ExprKind::Unary(UnOp::Deref, _) => self.eagerness |= NoChange,
200                 ExprKind::Unary(_, e)
201                     if matches!(
202                         self.cx.typeck_results().expr_ty(e).kind(),
203                         ty::Bool | ty::Int(_) | ty::Uint(_),
204                     ) => {},
205                 ExprKind::Binary(_, lhs, rhs)
206                     if self.cx.typeck_results().expr_ty(lhs).is_primitive()
207                         && self.cx.typeck_results().expr_ty(rhs).is_primitive() => {},
208 
209                 // Can't be moved into a closure
210                 ExprKind::Break(..)
211                 | ExprKind::Continue(_)
212                 | ExprKind::Ret(_)
213                 | ExprKind::Become(_)
214                 | ExprKind::InlineAsm(_)
215                 | ExprKind::Yield(..)
216                 | ExprKind::Err(_) => {
217                     self.eagerness = ForceNoChange;
218                     return;
219                 },
220 
221                 // Memory allocation, custom operator, loop, or call to an unknown function
222                 ExprKind::Unary(..) | ExprKind::Binary(..) | ExprKind::Loop(..) | ExprKind::Call(..) => {
223                     self.eagerness = Lazy;
224                 },
225 
226                 ExprKind::ConstBlock(_)
227                 | ExprKind::Array(_)
228                 | ExprKind::Tup(_)
229                 | ExprKind::Lit(_)
230                 | ExprKind::Cast(..)
231                 | ExprKind::Type(..)
232                 | ExprKind::DropTemps(_)
233                 | ExprKind::Let(..)
234                 | ExprKind::If(..)
235                 | ExprKind::Match(..)
236                 | ExprKind::Closure { .. }
237                 | ExprKind::Field(..)
238                 | ExprKind::AddrOf(..)
239                 | ExprKind::Struct(..)
240                 | ExprKind::Repeat(..)
241                 | ExprKind::Block(Block { stmts: [], .. }, _)
242                 | ExprKind::OffsetOf(..) => (),
243 
244                 // Assignment might be to a local defined earlier, so don't eagerly evaluate.
245                 // Blocks with multiple statements might be expensive, so don't eagerly evaluate.
246                 // TODO: Actually check if either of these are true here.
247                 ExprKind::Assign(..) | ExprKind::AssignOp(..) | ExprKind::Block(..) => self.eagerness |= NoChange,
248             }
249             walk_expr(self, e);
250         }
251     }
252 
253     let mut v = V {
254         cx,
255         eagerness: EagernessSuggestion::Eager,
256     };
257     v.visit_expr(e);
258     v.eagerness
259 }
260 
261 /// Whether the given expression should be changed to evaluate eagerly
switch_to_eager_eval<'tcx>(cx: &'_ LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool262 pub fn switch_to_eager_eval<'tcx>(cx: &'_ LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool {
263     expr_eagerness(cx, expr) == EagernessSuggestion::Eager
264 }
265 
266 /// Whether the given expression should be changed to evaluate lazily
switch_to_lazy_eval<'tcx>(cx: &'_ LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool267 pub fn switch_to_lazy_eval<'tcx>(cx: &'_ LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool {
268     expr_eagerness(cx, expr) == EagernessSuggestion::Lazy
269 }
270