Skip to main content

clippy_utils/
qualify_min_const_fn.rs

1// This code used to be a part of `rustc` but moved to Clippy as a result of
2// https://github.com/rust-lang/rust/issues/76618. Because of that, it contains unused code and some
3// of terminologies might not be relevant in the context of Clippy. Note that its behavior might
4// differ from the time of `rustc` even if the name stays the same.
5
6use crate::msrvs::{self, Msrv};
7use hir::LangItem;
8use rustc_const_eval::check_consts::ConstCx;
9use rustc_hir::def_id::DefId;
10use rustc_hir::{self as hir, HirId, RustcVersion, StableSince};
11use rustc_infer::infer::TyCtxtInferExt;
12use rustc_infer::traits::Obligation;
13use rustc_lint::LateContext;
14use rustc_middle::mir::{
15    Body, CastKind, NonDivergingIntrinsic, Operand, Place, ProjectionElem, Rvalue, Statement, StatementKind,
16    Terminator, TerminatorKind, UnOp,
17};
18use rustc_middle::traits::{BuiltinImplSource, ImplSource, ObligationCause};
19use rustc_middle::ty::adjustment::PointerCoercion;
20use rustc_middle::ty::{self, GenericArgKind, Instance, TraitRef, Ty, TyCtxt};
21use rustc_span::Span;
22use rustc_span::symbol::sym;
23use rustc_trait_selection::traits::{ObligationCtxt, SelectionContext};
24use std::borrow::Cow;
25
26type McfResult = Result<(), (Span, Cow<'static, str>)>;
27
28pub fn is_min_const_fn<'tcx>(cx: &LateContext<'tcx>, body: &Body<'tcx>, msrv: Msrv) -> McfResult {
29    let def_id = body.source.def_id();
30
31    for local in &body.local_decls {
32        check_ty(cx, local.ty, local.source_info.span, msrv)?;
33    }
34    if !msrv.meets(cx, msrvs::CONST_FN_TRAIT_BOUND)
35        && let Some(sized_did) = cx.tcx.lang_items().sized_trait()
36        && let Some(meta_sized_did) = cx.tcx.lang_items().meta_sized_trait()
37        && cx.tcx.param_env(def_id).caller_bounds().iter().any(|bound| {
38            bound.as_trait_clause().is_some_and(|clause| {
39                let did = clause.def_id();
40                did != sized_did && did != meta_sized_did
41            })
42        })
43    {
44        return Err((
45            body.span,
46            "non-`Sized` trait clause before `const_fn_trait_bound` is stabilized".into(),
47        ));
48    }
49    // impl trait is gone in MIR, so check the return type manually
50    check_ty(
51        cx,
52        cx.tcx
53            .fn_sig(def_id)
54            .instantiate_identity()
55            .skip_norm_wip()
56            .output()
57            .skip_binder(),
58        body.local_decls.iter().next().unwrap().source_info.span,
59        msrv,
60    )?;
61
62    for bb in &*body.basic_blocks {
63        // Cleanup blocks are ignored entirely by const eval, so we can too:
64        // https://github.com/rust-lang/rust/blob/1dea922ea6e74f99a0e97de5cdb8174e4dea0444/compiler/rustc_const_eval/src/transform/check_consts/check.rs#L382
65        if !bb.is_cleanup {
66            check_terminator(cx, body, bb.terminator(), msrv)?;
67            for stmt in &bb.statements {
68                check_statement(cx, body, def_id, stmt, msrv)?;
69            }
70        }
71    }
72    Ok(())
73}
74
75fn check_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, span: Span, msrv: Msrv) -> McfResult {
76    for arg in ty.walk() {
77        let ty = match arg.kind() {
78            GenericArgKind::Type(ty) => ty,
79
80            // No constraints on lifetimes or constants, except potentially
81            // constants' types, but `walk` will get to them as well.
82            GenericArgKind::Lifetime(_) | GenericArgKind::Const(_) => continue,
83        };
84
85        match ty.kind() {
86            ty::Ref(_, _, hir::Mutability::Mut) if !msrv.meets(cx, msrvs::CONST_MUT_REFS) => {
87                return Err((span, "mutable references in const fn are unstable".into()));
88            },
89            ty::Alias(_, ty::AliasTy {
90                kind: ty::Opaque { .. },
91                ..
92            }) => return Err((span, "`impl Trait` in const fn is unstable".into())),
93            ty::FnPtr(..) => {
94                return Err((span, "function pointers in const fn are unstable".into()));
95            },
96            ty::Dynamic(preds, _) => {
97                for pred in *preds {
98                    match pred.skip_binder() {
99                        ty::ExistentialPredicate::AutoTrait(_) | ty::ExistentialPredicate::Projection(_) => {
100                            return Err((
101                                span,
102                                "trait bounds other than `Sized` \
103                                 on const fn parameters are unstable"
104                                    .into(),
105                            ));
106                        },
107                        ty::ExistentialPredicate::Trait(trait_ref) => {
108                            if Some(trait_ref.def_id) != cx.tcx.lang_items().sized_trait() {
109                                return Err((
110                                    span,
111                                    "trait bounds other than `Sized` \
112                                     on const fn parameters are unstable"
113                                        .into(),
114                                ));
115                            }
116                        },
117                    }
118                }
119            },
120            _ => {},
121        }
122    }
123    Ok(())
124}
125
126fn check_rvalue<'tcx>(
127    cx: &LateContext<'tcx>,
128    body: &Body<'tcx>,
129    def_id: DefId,
130    rvalue: &Rvalue<'tcx>,
131    span: Span,
132    msrv: Msrv,
133) -> McfResult {
134    match rvalue {
135        Rvalue::ThreadLocalRef(_) => Err((span, "cannot access thread local storage in const fn".into())),
136        Rvalue::Discriminant(place)
137        | Rvalue::Ref(_, _, place)
138        | Rvalue::Reborrow(_, _, place)
139        | Rvalue::RawPtr(_, place)
140        | Rvalue::CopyForDeref(place) => check_place(cx, *place, span, body, msrv),
141        Rvalue::Repeat(operand, _)
142        | Rvalue::Use(operand, _)
143        | Rvalue::WrapUnsafeBinder(operand, _)
144        | Rvalue::UnaryOp(UnOp::PtrMetadata, operand)
145        | Rvalue::Cast(
146            CastKind::PointerWithExposedProvenance
147            | CastKind::IntToInt
148            | CastKind::FloatToInt
149            | CastKind::IntToFloat
150            | CastKind::FloatToFloat
151            | CastKind::FnPtrToPtr
152            | CastKind::PtrToPtr
153            | CastKind::PointerCoercion(PointerCoercion::MutToConstPointer | PointerCoercion::ArrayToPointer, _)
154            | CastKind::Subtype,
155            operand,
156            _,
157        ) => check_operand(cx, operand, span, body, msrv),
158        Rvalue::Cast(
159            CastKind::PointerCoercion(
160                PointerCoercion::UnsafeFnPointer
161                | PointerCoercion::ClosureFnPointer(_)
162                | PointerCoercion::ReifyFnPointer(_),
163                _,
164            ),
165            _,
166            _,
167        ) => Err((span, "function pointer casts are not allowed in const fn".into())),
168        Rvalue::Cast(CastKind::PointerCoercion(PointerCoercion::Unsize, _), op, cast_ty) => {
169            let Some(pointee_ty) = cast_ty.builtin_deref(true) else {
170                // We cannot allow this for now.
171                return Err((span, "unsizing casts are only allowed for references right now".into()));
172            };
173            let unsized_ty = cx
174                .tcx
175                .struct_tail_for_codegen(pointee_ty, ty::TypingEnv::post_analysis(cx.tcx, def_id));
176            if let ty::Slice(_) | ty::Str = unsized_ty.kind() {
177                check_operand(cx, op, span, body, msrv)?;
178                // Casting/coercing things to slices is fine.
179                Ok(())
180            } else {
181                // We just can't allow trait objects until we have figured out trait method calls.
182                Err((span, "unsizing casts are not allowed in const fn".into()))
183            }
184        },
185        Rvalue::Cast(CastKind::PointerExposeProvenance, _, _) => {
186            Err((span, "casting pointers to ints is unstable in const fn".into()))
187        },
188        Rvalue::Cast(CastKind::Transmute, _, _) => Err((
189            span,
190            "transmute can attempt to turn pointers into integers, so is unstable in const fn".into(),
191        )),
192        // binops are fine on integers
193        Rvalue::BinaryOp(_, box (lhs, rhs)) => {
194            check_operand(cx, lhs, span, body, msrv)?;
195            check_operand(cx, rhs, span, body, msrv)?;
196            let ty = lhs.ty(body, cx.tcx);
197            if ty.is_integral() || ty.is_bool() || ty.is_char() {
198                Ok(())
199            } else {
200                Err((
201                    span,
202                    "only int, `bool` and `char` operations are stable in const fn".into(),
203                ))
204            }
205        },
206        Rvalue::UnaryOp(_, operand) => {
207            let ty = operand.ty(body, cx.tcx);
208            if ty.is_integral() | ty.is_bool() {
209                check_operand(cx, operand, span, body, msrv)
210            } else {
211                Err((
212                    span,
213                    "only int, `bool`, and pointer metadata operations are stable in const fn".into(),
214                ))
215            }
216        },
217        Rvalue::Aggregate(_, operands) => {
218            for operand in operands {
219                check_operand(cx, operand, span, body, msrv)?;
220            }
221            Ok(())
222        },
223    }
224}
225
226fn check_statement<'tcx>(
227    cx: &LateContext<'tcx>,
228    body: &Body<'tcx>,
229    def_id: DefId,
230    statement: &Statement<'tcx>,
231    msrv: Msrv,
232) -> McfResult {
233    let span = statement.source_info.span;
234    match &statement.kind {
235        StatementKind::Assign(box (place, rval)) => {
236            check_place(cx, *place, span, body, msrv)?;
237            check_rvalue(cx, body, def_id, rval, span, msrv)
238        },
239
240        StatementKind::FakeRead(box (_, place)) => check_place(cx, *place, span, body, msrv),
241        // just an assignment
242        StatementKind::SetDiscriminant { place, .. } => check_place(cx, **place, span, body, msrv),
243
244        StatementKind::Intrinsic(box NonDivergingIntrinsic::Assume(op)) => check_operand(cx, op, span, body, msrv),
245
246        StatementKind::Intrinsic(box NonDivergingIntrinsic::CopyNonOverlapping(
247            rustc_middle::mir::CopyNonOverlapping { dst, src, count },
248        )) => {
249            check_operand(cx, dst, span, body, msrv)?;
250            check_operand(cx, src, span, body, msrv)?;
251            check_operand(cx, count, span, body, msrv)
252        },
253        // These are all NOPs
254        StatementKind::StorageLive(_)
255        | StatementKind::StorageDead(_)
256        | StatementKind::AscribeUserType(..)
257        | StatementKind::PlaceMention(..)
258        | StatementKind::Coverage(..)
259        | StatementKind::ConstEvalCounter
260        | StatementKind::BackwardIncompatibleDropHint { .. }
261        | StatementKind::Nop => Ok(()),
262    }
263}
264
265fn check_operand<'tcx>(
266    cx: &LateContext<'tcx>,
267    operand: &Operand<'tcx>,
268    span: Span,
269    body: &Body<'tcx>,
270    msrv: Msrv,
271) -> McfResult {
272    match operand {
273        Operand::Move(place) => {
274            if !place.projection.as_ref().is_empty()
275                && !is_ty_const_destruct(cx.tcx, place.ty(&body.local_decls, cx.tcx).ty, body)
276            {
277                return Err((
278                    span,
279                    "cannot drop locals with a non constant destructor in const fn".into(),
280                ));
281            }
282
283            check_place(cx, *place, span, body, msrv)
284        },
285        Operand::Copy(place) => check_place(cx, *place, span, body, msrv),
286        Operand::Constant(c) => match c.check_static_ptr(cx.tcx) {
287            Some(_) => Err((span, "cannot access `static` items in const fn".into())),
288            None => Ok(()),
289        },
290        Operand::RuntimeChecks(..) => Ok(()),
291    }
292}
293
294fn check_place<'tcx>(
295    cx: &LateContext<'tcx>,
296    place: Place<'tcx>,
297    span: Span,
298    body: &Body<'tcx>,
299    msrv: Msrv,
300) -> McfResult {
301    for (base, elem) in place.as_ref().iter_projections() {
302        match elem {
303            ProjectionElem::Field(..) => {
304                if base.ty(body, cx.tcx).ty.is_union() && !msrv.meets(cx, msrvs::CONST_FN_UNION) {
305                    return Err((span, "accessing union fields is unstable".into()));
306                }
307            },
308            ProjectionElem::Deref => match base.ty(body, cx.tcx).ty.kind() {
309                ty::RawPtr(_, hir::Mutability::Mut) => {
310                    return Err((span, "dereferencing raw mut pointer in const fn is unstable".into()));
311                },
312                ty::RawPtr(_, hir::Mutability::Not) if !msrv.meets(cx, msrvs::CONST_RAW_PTR_DEREF) => {
313                    return Err((span, "dereferencing raw const pointer in const fn is unstable".into()));
314                },
315                _ => (),
316            },
317            ProjectionElem::ConstantIndex { .. }
318            | ProjectionElem::OpaqueCast(..)
319            | ProjectionElem::Downcast(..)
320            | ProjectionElem::Subslice { .. }
321            | ProjectionElem::Index(_)
322            | ProjectionElem::UnwrapUnsafeBinder(_) => {},
323        }
324    }
325
326    Ok(())
327}
328
329fn check_terminator<'tcx>(
330    cx: &LateContext<'tcx>,
331    body: &Body<'tcx>,
332    terminator: &Terminator<'tcx>,
333    msrv: Msrv,
334) -> McfResult {
335    let span = terminator.source_info.span;
336    match &terminator.kind {
337        TerminatorKind::FalseEdge { .. }
338        | TerminatorKind::FalseUnwind { .. }
339        | TerminatorKind::Goto { .. }
340        | TerminatorKind::Return
341        | TerminatorKind::UnwindResume
342        | TerminatorKind::UnwindTerminate(_)
343        | TerminatorKind::Unreachable => Ok(()),
344        TerminatorKind::Drop { place, .. } => {
345            if !is_ty_const_destruct(cx.tcx, place.ty(&body.local_decls, cx.tcx).ty, body) {
346                return Err((
347                    span,
348                    "cannot drop locals with a non constant destructor in const fn".into(),
349                ));
350            }
351            Ok(())
352        },
353        TerminatorKind::SwitchInt { discr, targets: _ } => check_operand(cx, discr, span, body, msrv),
354        TerminatorKind::CoroutineDrop | TerminatorKind::Yield { .. } => {
355            Err((span, "const fn coroutines are unstable".into()))
356        },
357        TerminatorKind::Call {
358            func,
359            args,
360            call_source: _,
361            destination: _,
362            target: _,
363            unwind: _,
364            fn_span: _,
365        }
366        | TerminatorKind::TailCall { func, args, fn_span: _ } => {
367            let fn_ty = func.ty(body, cx.tcx);
368            if let ty::FnDef(fn_def_id, fn_substs) = fn_ty.kind() {
369                // FIXME: when analyzing a function with generic parameters, we may not have enough information to
370                // resolve to an instance. However, we could check if a host effect predicate can guarantee that
371                // this can be made a `const` call.
372                let fn_def_id = match Instance::try_resolve(cx.tcx, cx.typing_env(), *fn_def_id, fn_substs) {
373                    Ok(Some(fn_inst)) => fn_inst.def_id(),
374                    Ok(None) => return Err((span, format!("cannot resolve instance for {func:?}").into())),
375                    Err(_) => return Err((span, format!("error during instance resolution of {func:?}").into())),
376                };
377                if !is_stable_const_fn(cx, fn_def_id, msrv) {
378                    return Err((
379                        span,
380                        format!(
381                            "can only call other `const fn` within a `const fn`, \
382                             but `{func:?}` is not stable as `const fn`",
383                        )
384                        .into(),
385                    ));
386                }
387
388                // HACK: This is to "unstabilize" the `transmute` intrinsic
389                // within const fns. `transmute` is allowed in all other const contexts.
390                // This won't really scale to more intrinsics or functions. Let's allow const
391                // transmutes in const fn before we add more hacks to this.
392                if cx.tcx.is_intrinsic(fn_def_id, sym::transmute) {
393                    return Err((
394                        span,
395                        "can only call `transmute` from const items, not `const fn`".into(),
396                    ));
397                }
398
399                check_operand(cx, func, span, body, msrv)?;
400
401                for arg in args {
402                    check_operand(cx, &arg.node, span, body, msrv)?;
403                }
404                Ok(())
405            } else {
406                Err((span, "can only call other const fns within const fn".into()))
407            }
408        },
409        TerminatorKind::Assert {
410            cond,
411            expected: _,
412            msg: _,
413            target: _,
414            unwind: _,
415        } => check_operand(cx, cond, span, body, msrv),
416        TerminatorKind::InlineAsm { .. } => Err((span, "cannot use inline assembly in const fn".into())),
417    }
418}
419
420/// Checks if the given `def_id` is a stable const fn, in respect to the given MSRV.
421pub fn is_stable_const_fn(cx: &LateContext<'_>, def_id: DefId, msrv: Msrv) -> bool {
422    is_stable_const_fn_at(cx.tcx, cx.last_node_with_lint_attrs, def_id, msrv)
423}
424
425/// Checks if the given `def_id` is a stable const fn, in respect to the given MSRV.
426pub fn is_stable_const_fn_at(tcx: TyCtxt<'_>, node: HirId, def_id: DefId, msrv: Msrv) -> bool {
427    tcx.is_const_fn(def_id)
428        && tcx
429            .lookup_const_stability(def_id)
430            .or_else(|| {
431                tcx.trait_of_assoc(def_id)
432                    .and_then(|trait_def_id| tcx.lookup_const_stability(trait_def_id))
433            })
434            .is_none_or(|const_stab| {
435                if let rustc_hir::StabilityLevel::Stable { since, .. } = const_stab.level {
436                    // Checking MSRV is manually necessary because `rustc` has no such concept. This entire
437                    // function could be removed if `rustc` provided a MSRV-aware version of `is_stable_const_fn`.
438                    // as a part of an unimplemented MSRV check https://github.com/rust-lang/rust/issues/65262.
439
440                    let const_stab_rust_version = match since {
441                        StableSince::Version(version) => version,
442                        StableSince::Current => RustcVersion::CURRENT,
443                        StableSince::Err(_) => return false,
444                    };
445
446                    msrv.meets_at(tcx, node, const_stab_rust_version)
447                } else {
448                    // Unstable const fn, check if the feature is enabled.
449                    tcx.features().enabled(const_stab.feature) && msrv.at(tcx, node).is_none()
450                }
451            })
452}
453
454fn is_ty_const_destruct<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, body: &Body<'tcx>) -> bool {
455    // FIXME(const_trait_impl, fee1-dead) revert to const destruct once it works again
456    #[expect(unused)]
457    fn is_ty_const_destruct_unused<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, body: &Body<'tcx>) -> bool {
458        // If this doesn't need drop at all, then don't select `[const] Destruct`.
459        if !ty.needs_drop(tcx, body.typing_env(tcx)) {
460            return false;
461        }
462
463        let (infcx, param_env) = tcx.infer_ctxt().build_with_typing_env(body.typing_env(tcx));
464        // FIXME(const_trait_impl) constness
465        let obligation = Obligation::new(
466            tcx,
467            ObligationCause::dummy_with_span(body.span),
468            param_env,
469            TraitRef::new(tcx, tcx.require_lang_item(LangItem::Destruct, body.span), [ty]),
470        );
471
472        let mut selcx = SelectionContext::new(&infcx);
473        let Some(impl_src) = selcx.select(&obligation).ok().flatten() else {
474            return false;
475        };
476
477        if !matches!(
478            impl_src,
479            ImplSource::Builtin(BuiltinImplSource::Misc, _) | ImplSource::Param(_)
480        ) {
481            return false;
482        }
483
484        let ocx = ObligationCtxt::new(&infcx);
485        ocx.register_obligations(impl_src.nested_obligations());
486        ocx.evaluate_obligations_error_on_ambiguity().is_empty()
487    }
488
489    !ty.needs_drop(tcx, ConstCx::new(tcx, body).typing_env)
490}