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