//! The `Visitor` responsible for actually checking a `mir::Body` for invalid operations.

use std::borrow::Cow;
use std::num::NonZero;
use std::ops::Deref;
use std::{assert_matches, mem};

use rustc_errors::{Diag, ErrorGuaranteed};
use rustc_hir::def::DefKind;
use rustc_hir::def_id::DefId;
use rustc_hir::{self as hir, LangItem, find_attr};
use rustc_index::bit_set::DenseBitSet;
use rustc_infer::infer::TyCtxtInferExt;
use rustc_middle::mir::visit::Visitor;
use rustc_middle::mir::*;
use rustc_middle::span_bug;
use rustc_middle::ty::adjustment::PointerCoercion;
use rustc_middle::ty::{self, Ty, TypeVisitableExt};
use rustc_mir_dataflow::Analysis;
use rustc_mir_dataflow::impls::{MaybeStorageLive, always_storage_live_locals};
use rustc_span::{Span, Symbol, sym};
use rustc_trait_selection::traits::{
    Obligation, ObligationCause, ObligationCauseCode, ObligationCtxt,
};
use tracing::{instrument, trace};

use super::ops::{self, NonConstOp, Status};
use super::qualifs::{self, HasMutInterior, NeedsDrop, NeedsNonConstDrop};
use super::resolver::FlowSensitiveAnalysis;
use super::{ConstCx, Qualif};
use crate::check_consts::is_fn_or_trait_safe_to_expose_on_stable;
use crate::errors;

type QualifResults<'mir, 'tcx, Q> =
    rustc_mir_dataflow::ResultsCursor<'mir, 'tcx, FlowSensitiveAnalysis<'mir, 'tcx, Q>>;

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
enum ConstConditionsHold {
    Yes,
    No,
}

#[derive(Default)]
pub(crate) struct Qualifs<'mir, 'tcx> {
    has_mut_interior: Option<QualifResults<'mir, 'tcx, HasMutInterior>>,
    needs_drop: Option<QualifResults<'mir, 'tcx, NeedsDrop>>,
    needs_non_const_drop: Option<QualifResults<'mir, 'tcx, NeedsNonConstDrop>>,
}

impl<'mir, 'tcx> Qualifs<'mir, 'tcx> {
    /// Returns `true` if `local` is `NeedsDrop` at the given `Location`.
    ///
    /// Only updates the cursor if absolutely necessary
    pub(crate) fn needs_drop(
        &mut self,
        ccx: &'mir ConstCx<'mir, 'tcx>,
        local: Local,
        location: Location,
    ) -> bool {
        let ty = ccx.body.local_decls[local].ty;
        // Peeking into opaque types causes cycles if the current function declares said opaque
        // type. Thus we avoid short circuiting on the type and instead run the more expensive
        // analysis that looks at the actual usage within this function
        if !ty.has_opaque_types() && !NeedsDrop::in_any_value_of_ty(ccx, ty) {
            return false;
        }

        let needs_drop = self.needs_drop.get_or_insert_with(|| {
            let ConstCx { tcx, body, .. } = *ccx;

            FlowSensitiveAnalysis::new(NeedsDrop, ccx)
                .iterate_to_fixpoint(tcx, body, None)
                .into_results_cursor(body)
        });

        needs_drop.seek_before_primary_effect(location);
        needs_drop.get().contains(local)
    }

    /// Returns `true` if `local` is `NeedsNonConstDrop` at the given `Location`.
    ///
    /// Only updates the cursor if absolutely necessary
    pub(crate) fn needs_non_const_drop(
        &mut self,
        ccx: &'mir ConstCx<'mir, 'tcx>,
        local: Local,
        location: Location,
    ) -> bool {
        let ty = ccx.body.local_decls[local].ty;
        // Peeking into opaque types causes cycles if the current function declares said opaque
        // type. Thus we avoid short circuiting on the type and instead run the more expensive
        // analysis that looks at the actual usage within this function
        if !ty.has_opaque_types() && !NeedsNonConstDrop::in_any_value_of_ty(ccx, ty) {
            return false;
        }

        let needs_non_const_drop = self.needs_non_const_drop.get_or_insert_with(|| {
            let ConstCx { tcx, body, .. } = *ccx;

            FlowSensitiveAnalysis::new(NeedsNonConstDrop, ccx)
                .iterate_to_fixpoint(tcx, body, None)
                .into_results_cursor(body)
        });

        needs_non_const_drop.seek_before_primary_effect(location);
        needs_non_const_drop.get().contains(local)
    }

    /// Returns `true` if `local` is `HasMutInterior` at the given `Location`.
    ///
    /// Only updates the cursor if absolutely necessary.
    fn has_mut_interior(
        &mut self,
        ccx: &'mir ConstCx<'mir, 'tcx>,
        local: Local,
        location: Location,
    ) -> bool {
        let ty = ccx.body.local_decls[local].ty;
        // Peeking into opaque types causes cycles if the current function declares said opaque
        // type. Thus we avoid short circuiting on the type and instead run the more expensive
        // analysis that looks at the actual usage within this function
        if !ty.has_opaque_types() && !HasMutInterior::in_any_value_of_ty(ccx, ty) {
            return false;
        }

        let has_mut_interior = self.has_mut_interior.get_or_insert_with(|| {
            let ConstCx { tcx, body, .. } = *ccx;

            FlowSensitiveAnalysis::new(HasMutInterior, ccx)
                .iterate_to_fixpoint(tcx, body, None)
                .into_results_cursor(body)
        });

        has_mut_interior.seek_before_primary_effect(location);
        has_mut_interior.get().contains(local)
    }

    fn in_return_place(
        &mut self,
        ccx: &'mir ConstCx<'mir, 'tcx>,
        tainted_by_errors: Option<ErrorGuaranteed>,
    ) -> ConstQualifs {
        // FIXME(explicit_tail_calls): uhhhh I think we can return without return now, does it change anything

        // Find the `Return` terminator if one exists.
        //
        // If no `Return` terminator exists, this MIR is divergent. Just return the conservative
        // qualifs for the return type.
        let return_block = ccx
            .body
            .basic_blocks
            .iter_enumerated()
            .find(|(_, block)| matches!(block.terminator().kind, TerminatorKind::Return))
            .map(|(bb, _)| bb);

        let Some(return_block) = return_block else {
            return qualifs::in_any_value_of_ty(ccx, ccx.body.return_ty(), tainted_by_errors);
        };

        let return_loc = ccx.body.terminator_loc(return_block);

        ConstQualifs {
            needs_drop: self.needs_drop(ccx, RETURN_PLACE, return_loc),
            needs_non_const_drop: self.needs_non_const_drop(ccx, RETURN_PLACE, return_loc),
            has_mut_interior: self.has_mut_interior(ccx, RETURN_PLACE, return_loc),
            tainted_by_errors,
        }
    }
}

pub struct Checker<'mir, 'tcx> {
    ccx: &'mir ConstCx<'mir, 'tcx>,
    qualifs: Qualifs<'mir, 'tcx>,

    /// The span of the current statement.
    span: Span,

    /// A set that stores for each local whether it is "transient", i.e. guaranteed to be dead
    /// when this MIR body returns.
    transient_locals: Option<DenseBitSet<Local>>,

    error_emitted: Option<ErrorGuaranteed>,
    secondary_errors: Vec<Diag<'tcx>>,
}

impl<'mir, 'tcx> Deref for Checker<'mir, 'tcx> {
    type Target = ConstCx<'mir, 'tcx>;

    fn deref(&self) -> &Self::Target {
        self.ccx
    }
}

impl<'mir, 'tcx> Checker<'mir, 'tcx> {
    pub fn new(ccx: &'mir ConstCx<'mir, 'tcx>) -> Self {
        Checker {
            span: ccx.body.span,
            ccx,
            qualifs: Default::default(),
            transient_locals: None,
            error_emitted: None,
            secondary_errors: Vec::new(),
        }
    }

    pub fn check_body(&mut self) {
        let ConstCx { tcx, body, .. } = *self.ccx;
        let def_id = self.ccx.def_id();

        // `async` functions cannot be `const fn`. This is checked during AST lowering, so there's
        // no need to emit duplicate errors here.
        if self.ccx.is_async() || body.coroutine.is_some() {
            tcx.dcx().span_delayed_bug(body.span, "`async` functions cannot be `const fn`");
            return;
        }

        if !find_attr!(tcx, def_id, RustcDoNotConstCheck) {
            self.visit_body(body);
        }

        // If we got through const-checking without emitting any "primary" errors, emit any
        // "secondary" errors if they occurred. Otherwise, cancel the "secondary" errors.
        let secondary_errors = mem::take(&mut self.secondary_errors);
        if self.error_emitted.is_none() {
            for error in secondary_errors {
                self.error_emitted = Some(error.emit());
            }
        } else {
            assert!(self.tcx.dcx().has_errors().is_some());
            for error in secondary_errors {
                error.cancel();
            }
        }
    }

    fn local_is_transient(&mut self, local: Local) -> bool {
        let ccx = self.ccx;
        self.transient_locals
            .get_or_insert_with(|| {
                // A local is "transient" if it is guaranteed dead at all `Return`.
                // So first compute the say of "maybe live" locals at each program point.
                let always_live_locals = &always_storage_live_locals(&ccx.body);
                let mut maybe_storage_live =
                    MaybeStorageLive::new(Cow::Borrowed(always_live_locals))
                        .iterate_to_fixpoint(ccx.tcx, &ccx.body, None)
                        .into_results_cursor(&ccx.body);

                // And then check all `Return` in the MIR, and if a local is "maybe live" at a
                // `Return` then it is definitely not transient.
                let mut transient = DenseBitSet::new_filled(ccx.body.local_decls.len());
                // Make sure to only visit reachable blocks, the dataflow engine can ICE otherwise.
                for (bb, data) in traversal::reachable(&ccx.body) {
                    if data.terminator().kind == TerminatorKind::Return {
                        let location = ccx.body.terminator_loc(bb);
                        maybe_storage_live.seek_after_primary_effect(location);
                        // If a local may be live here, it is definitely not transient.
                        transient.subtract(maybe_storage_live.get());
                    }
                }

                transient
            })
            .contains(local)
    }

    pub fn qualifs_in_return_place(&mut self) -> ConstQualifs {
        self.qualifs.in_return_place(self.ccx, self.error_emitted)
    }

    /// Emits an error if an expression cannot be evaluated in the current context.
    pub fn check_op(&mut self, op: impl NonConstOp<'tcx>) {
        self.check_op_spanned(op, self.span);
    }

    /// Emits an error at the given `span` if an expression cannot be evaluated in the current
    /// context.
    pub fn check_op_spanned<O: NonConstOp<'tcx>>(&mut self, op: O, span: Span) {
        let gate = match op.status_in_item(self.ccx) {
            Status::Unstable {
                gate,
                safe_to_expose_on_stable,
                is_function_call,
                gate_already_checked,
            } if gate_already_checked || self.tcx.features().enabled(gate) => {
                if gate_already_checked {
                    assert!(
                        !safe_to_expose_on_stable,
                        "setting `gate_already_checked` without `safe_to_expose_on_stable` makes no sense"
                    );
                }
                // Generally this is allowed since the feature gate is enabled -- except
                // if this function wants to be safe-to-expose-on-stable.
                if !safe_to_expose_on_stable
                    && self.enforce_recursive_const_stability()
                    && !super::rustc_allow_const_fn_unstable(self.tcx, self.def_id(), gate)
                {
                    emit_unstable_in_stable_exposed_error(self.ccx, span, gate, is_function_call);
                }

                return;
            }

            Status::Unstable { gate, .. } => Some(gate),
            Status::Forbidden => None,
        };

        if self.tcx.sess.opts.unstable_opts.unleash_the_miri_inside_of_you {
            self.tcx.sess.miri_unleashed_feature(span, gate);
            return;
        }

        let err = op.build_error(self.ccx, span);
        assert!(err.is_error());

        match op.importance() {
            ops::DiagImportance::Primary => {
                let reported = err.emit();
                self.error_emitted = Some(reported);
            }

            ops::DiagImportance::Secondary => {
                self.secondary_errors.push(err);
                self.tcx.dcx().span_delayed_bug(
                    span,
                    "compilation must fail when there is a secondary const checker error",
                );
            }
        }
    }

    fn check_static(&mut self, def_id: DefId, span: Span) {
        if self.tcx.is_thread_local_static(def_id) {
            self.tcx.dcx().span_bug(span, "tls access is checked in `Rvalue::ThreadLocalRef`");
        }
        if let Some(def_id) = def_id.as_local()
            && let Err(guar) = self.tcx.ensure_result().check_well_formed(hir::OwnerId { def_id })
        {
            self.error_emitted = Some(guar);
        }
    }

    /// Returns whether this place can possibly escape the evaluation of the current const/static
    /// initializer. The check assumes that all already existing pointers and references point to
    /// non-escaping places.
    fn place_may_escape(&mut self, place: &Place<'_>) -> bool {
        let is_transient = match self.const_kind() {
            // In a const fn all borrows are transient or point to the places given via
            // references in the arguments (so we already checked them with
            // TransientMutBorrow/MutBorrow as appropriate).
            // The borrow checker guarantees that no new non-transient borrows are created.
            // NOTE: Once we have heap allocations during CTFE we need to figure out
            // how to prevent `const fn` to create long-lived allocations that point
            // to mutable memory.
            hir::ConstContext::ConstFn => true,
            _ => {
                // For indirect places, we are not creating a new permanent borrow, it's just as
                // transient as the already existing one.
                // Locals with StorageDead do not live beyond the evaluation and can
                // thus safely be borrowed without being able to be leaked to the final
                // value of the constant.
                // Note: This is only sound if every local that has a `StorageDead` has a
                // `StorageDead` in every control flow path leading to a `return` terminator.
                // If anything slips through, there's no safety net -- safe code can create
                // references to variants of `!Freeze` enums as long as that variant is `Freeze`, so
                // interning can't protect us here. (There *is* a safety net for mutable references
                // though, interning will ICE if we miss something here.)
                place.is_indirect() || self.local_is_transient(place.local)
            }
        };
        // Transient places cannot possibly escape because the place doesn't exist any more at the
        // end of evaluation.
        !is_transient
    }

    /// Returns whether there are const-conditions.
    fn revalidate_conditional_constness(
        &mut self,
        callee: DefId,
        callee_args: ty::GenericArgsRef<'tcx>,
        call_span: Span,
    ) -> Option<ConstConditionsHold> {
        let tcx = self.tcx;
        if !tcx.is_conditionally_const(callee) {
            return None;
        }

        let const_conditions = tcx.const_conditions(callee).instantiate(tcx, callee_args);
        if const_conditions.is_empty() {
            return None;
        }

        let (infcx, param_env) = tcx.infer_ctxt().build_with_typing_env(self.body.typing_env(tcx));
        let ocx = ObligationCtxt::new(&infcx);

        let body_id = self.body.source.def_id().expect_local();
        let host_polarity = match self.const_kind() {
            hir::ConstContext::ConstFn => ty::BoundConstness::Maybe,
            hir::ConstContext::Static(_) | hir::ConstContext::Const { .. } => {
                ty::BoundConstness::Const
            }
        };
        let const_conditions = const_conditions.into_iter().map(|(c, s)| {
            (ocx.normalize(&ObligationCause::misc(call_span, body_id), param_env, c), s)
        });
        ocx.register_obligations(const_conditions.into_iter().map(|(trait_ref, span)| {
            Obligation::new(
                tcx,
                ObligationCause::new(
                    call_span,
                    body_id,
                    ObligationCauseCode::WhereClause(callee, span),
                ),
                param_env,
                trait_ref.to_host_effect_clause(tcx, host_polarity),
            )
        }));

        let errors = ocx.evaluate_obligations_error_on_ambiguity();
        if errors.is_empty() {
            Some(ConstConditionsHold::Yes)
        } else {
            tcx.dcx()
                .span_delayed_bug(call_span, "this should have reported a [const] error in HIR");
            Some(ConstConditionsHold::No)
        }
    }

    pub fn check_drop_terminator(
        &mut self,
        dropped_place: Place<'tcx>,
        location: Location,
        terminator_span: Span,
    ) {
        let ty_of_dropped_place = dropped_place.ty(self.body, self.tcx).ty;

        let needs_drop = if let Some(local) = dropped_place.as_local() {
            self.qualifs.needs_drop(self.ccx, local, location)
        } else {
            qualifs::NeedsDrop::in_any_value_of_ty(self.ccx, ty_of_dropped_place)
        };
        // If this type doesn't need a drop at all, then there's nothing to enforce.
        if !needs_drop {
            return;
        }

        let mut err_span = self.span;
        let needs_non_const_drop = if let Some(local) = dropped_place.as_local() {
            // Use the span where the local was declared as the span of the drop error.
            err_span = self.body.local_decls[local].source_info.span;
            self.qualifs.needs_non_const_drop(self.ccx, local, location)
        } else {
            qualifs::NeedsNonConstDrop::in_any_value_of_ty(self.ccx, ty_of_dropped_place)
        };

        self.check_op_spanned(
            ops::LiveDrop {
                dropped_at: terminator_span,
                dropped_ty: ty_of_dropped_place,
                needs_non_const_drop,
            },
            err_span,
        );
    }

    /// Check the const stability of the given item (fn or trait).
    fn check_callee_stability(&mut self, def_id: DefId) {
        match self.tcx.lookup_const_stability(def_id) {
            Some(hir::ConstStability { level: hir::StabilityLevel::Stable { .. }, .. }) => {
                // All good.
            }
            None => {
                // This doesn't need a separate const-stability check -- const-stability equals
                // regular stability, and regular stability is checked separately.
                // However, we *do* have to worry about *recursive* const stability.
                if self.enforce_recursive_const_stability()
                    && !is_fn_or_trait_safe_to_expose_on_stable(self.tcx, def_id)
                {
                    self.dcx().emit_err(errors::UnmarkedConstItemExposed {
                        span: self.span,
                        def_path: self.tcx.def_path_str(def_id),
                    });
                }
            }
            Some(hir::ConstStability {
                level: hir::StabilityLevel::Unstable { implied_by: implied_feature, issue, .. },
                feature,
                ..
            }) => {
                // An unstable const fn/trait with a feature gate.
                let callee_safe_to_expose_on_stable =
                    is_fn_or_trait_safe_to_expose_on_stable(self.tcx, def_id);

                // We only honor `span.allows_unstable` aka `#[allow_internal_unstable]` if
                // the callee is safe to expose, to avoid bypassing recursive stability.
                // This is not ideal since it means the user sees an error, not the macro
                // author, but that's also the case if one forgets to set
                // `#[allow_internal_unstable]` in the first place. Note that this cannot be
                // integrated in the check below since we want to enforce
                // `callee_safe_to_expose_on_stable` even if
                // `!self.enforce_recursive_const_stability()`.
                if (self.span.allows_unstable(feature)
                    || implied_feature.is_some_and(|f| self.span.allows_unstable(f)))
                    && callee_safe_to_expose_on_stable
                {
                    return;
                }

                // We can't use `check_op` to check whether the feature is enabled because
                // the logic is a bit different than elsewhere: local functions don't need
                // the feature gate, and there might be an "implied" gate that also suffices
                // to allow this.
                let feature_enabled = def_id.is_local()
                    || self.tcx.features().enabled(feature)
                    || implied_feature.is_some_and(|f| self.tcx.features().enabled(f))
                    || {
                        // When we're compiling the compiler itself we may pull in
                        // crates from crates.io, but those crates may depend on other
                        // crates also pulled in from crates.io. We want to ideally be
                        // able to compile everything without requiring upstream
                        // modifications, so in the case that this looks like a
                        // `rustc_private` crate (e.g., a compiler crate) and we also have
                        // the `-Z force-unstable-if-unmarked` flag present (we're
                        // compiling a compiler crate), then let this missing feature
                        // annotation slide.
                        // This matches what we do in `eval_stability_allow_unstable` for
                        // regular stability.
                        feature == sym::rustc_private
                            && issue == NonZero::new(27812)
                            && self.tcx.sess.opts.unstable_opts.force_unstable_if_unmarked
                    };
                // Even if the feature is enabled, we still need check_op to double-check
                // this if the callee is not safe to expose on stable.
                if !feature_enabled || !callee_safe_to_expose_on_stable {
                    self.check_op(ops::CallUnstable {
                        def_id,
                        feature,
                        feature_enabled,
                        safe_to_expose_on_stable: callee_safe_to_expose_on_stable,
                        is_function_call: self.tcx.def_kind(def_id) != DefKind::Trait,
                    });
                }
            }
        }
    }
}

impl<'tcx> Visitor<'tcx> for Checker<'_, 'tcx> {
    fn visit_basic_block_data(&mut self, bb: BasicBlock, block: &BasicBlockData<'tcx>) {
        trace!("visit_basic_block_data: bb={:?} is_cleanup={:?}", bb, block.is_cleanup);

        // We don't const-check basic blocks on the cleanup path since we never unwind during
        // const-eval: a panic causes an immediate compile error. In other words, cleanup blocks
        // are unreachable during const-eval.
        //
        // We can't be more conservative (e.g., by const-checking cleanup blocks anyways) because
        // locals that would never be dropped during normal execution are sometimes dropped during
        // unwinding, which means backwards-incompatible live-drop errors.
        if block.is_cleanup {
            return;
        }

        self.super_basic_block_data(bb, block);
    }

    fn visit_rvalue(&mut self, rvalue: &Rvalue<'tcx>, location: Location) {
        trace!("visit_rvalue: rvalue={:?} location={:?}", rvalue, location);

        self.super_rvalue(rvalue, location);

        match rvalue {
            Rvalue::ThreadLocalRef(_) => self.check_op(ops::ThreadLocalAccess),

            Rvalue::Use(..)
            | Rvalue::CopyForDeref(..)
            | Rvalue::Repeat(..)
            | Rvalue::Discriminant(..) => {}

            Rvalue::Aggregate(kind, ..) => {
                if let AggregateKind::Coroutine(def_id, ..) = kind.as_ref()
                    && let Some(coroutine_kind) = self.tcx.coroutine_kind(*def_id)
                {
                    self.check_op(ops::Coroutine(coroutine_kind));
                }
            }

            Rvalue::Ref(_, BorrowKind::Mut { .. }, place)
            | Rvalue::RawPtr(RawPtrKind::Mut, place) => {
                // Inside mutable statics, we allow arbitrary mutable references.
                // We've allowed `static mut FOO = &mut [elements];` for a long time (the exact
                // reasons why are lost to history), and there is no reason to restrict that to
                // arrays and slices.
                let is_allowed =
                    self.const_kind() == hir::ConstContext::Static(hir::Mutability::Mut);

                if !is_allowed && self.place_may_escape(place) {
                    self.check_op(ops::EscapingMutBorrow);
                }
            }

            Rvalue::Ref(_, BorrowKind::Shared | BorrowKind::Fake(_), place)
            | Rvalue::RawPtr(RawPtrKind::Const, place) => {
                let borrowed_place_has_mut_interior = qualifs::in_place::<HasMutInterior, _>(
                    self.ccx,
                    &mut |local| self.qualifs.has_mut_interior(self.ccx, local, location),
                    place.as_ref(),
                );

                if borrowed_place_has_mut_interior && self.place_may_escape(place) {
                    self.check_op(ops::EscapingCellBorrow);
                }
            }

            Rvalue::Reborrow(..) => {
                // FIXME(reborrow): figure out if this is relevant at all.
            }

            Rvalue::RawPtr(RawPtrKind::FakeForPtrMetadata, place) => {
                // These are only inserted for slice length, so the place must already be indirect.
                // This implies we do not have to worry about whether the borrow escapes.
                if !place.is_indirect() {
                    self.tcx.dcx().span_delayed_bug(
                        self.body.source_info(location).span,
                        "fake borrows are always indirect",
                    );
                }
            }

            Rvalue::Cast(
                CastKind::PointerCoercion(
                    PointerCoercion::MutToConstPointer
                    | PointerCoercion::ArrayToPointer
                    | PointerCoercion::UnsafeFnPointer
                    | PointerCoercion::ClosureFnPointer(_)
                    | PointerCoercion::ReifyFnPointer(_),
                    _,
                ),
                _,
                _,
            ) => {
                // These are all okay; they only change the type, not the data.
            }

            Rvalue::Cast(CastKind::PointerExposeProvenance, _, _) => {
                self.check_op(ops::RawPtrToIntCast);
            }
            Rvalue::Cast(CastKind::PointerWithExposedProvenance, _, _) => {
                // Since no pointer can ever get exposed (rejected above), this is easy to support.
            }

            Rvalue::Cast(_, _, _) => {}

            Rvalue::UnaryOp(op, operand) => {
                let ty = operand.ty(self.body, self.tcx);
                match op {
                    UnOp::Not | UnOp::Neg => {
                        if is_int_bool_float_or_char(ty) {
                            // Int, bool, float, and char operations are fine.
                        } else {
                            span_bug!(
                                self.span,
                                "non-primitive type in `Rvalue::UnaryOp{op:?}`: {ty:?}",
                            );
                        }
                    }
                    UnOp::PtrMetadata => {
                        // Getting the metadata from a pointer is always const.
                        // We already validated the type is valid in the validator.
                    }
                }
            }

            Rvalue::BinaryOp(op, (lhs, rhs)) => {
                let lhs_ty = lhs.ty(self.body, self.tcx);
                let rhs_ty = rhs.ty(self.body, self.tcx);

                if is_int_bool_float_or_char(lhs_ty) && is_int_bool_float_or_char(rhs_ty) {
                    // Int, bool, float, and char operations are fine.
                } else if lhs_ty.is_fn_ptr() || lhs_ty.is_raw_ptr() {
                    assert_matches!(
                        op,
                        BinOp::Eq
                            | BinOp::Ne
                            | BinOp::Le
                            | BinOp::Lt
                            | BinOp::Ge
                            | BinOp::Gt
                            | BinOp::Offset
                    );

                    self.check_op(ops::RawPtrComparison);
                } else {
                    span_bug!(
                        self.span,
                        "non-primitive type in `Rvalue::BinaryOp`: {:?} ⚬ {:?}",
                        lhs_ty,
                        rhs_ty
                    );
                }
            }

            Rvalue::WrapUnsafeBinder(..) => {
                // Unsafe binders are always trivial to create.
            }
        }
    }

    fn visit_operand(&mut self, op: &Operand<'tcx>, location: Location) {
        self.super_operand(op, location);
        if let Operand::Constant(c) = op
            && let Some(def_id) = c.check_static_ptr(self.tcx)
        {
            self.check_static(def_id, self.span);
        }
    }

    fn visit_source_info(&mut self, source_info: &SourceInfo) {
        trace!("visit_source_info: source_info={:?}", source_info);
        self.span = source_info.span;
    }

    fn visit_statement(&mut self, statement: &Statement<'tcx>, location: Location) {
        trace!("visit_statement: statement={:?} location={:?}", statement, location);

        self.super_statement(statement, location);

        match statement.kind {
            StatementKind::Assign(..)
            | StatementKind::SetDiscriminant { .. }
            | StatementKind::FakeRead(..)
            | StatementKind::StorageLive(_)
            | StatementKind::StorageDead(_)
            | StatementKind::PlaceMention(..)
            | StatementKind::AscribeUserType(..)
            | StatementKind::Coverage(..)
            | StatementKind::Intrinsic(..)
            | StatementKind::ConstEvalCounter
            | StatementKind::BackwardIncompatibleDropHint { .. }
            | StatementKind::Nop => {}
        }
    }

    #[instrument(level = "debug", skip(self))]
    fn visit_terminator(&mut self, terminator: &Terminator<'tcx>, location: Location) {
        self.super_terminator(terminator, location);

        match &terminator.kind {
            TerminatorKind::Call { func, args, fn_span, .. }
            | TerminatorKind::TailCall { func, args, fn_span, .. } => {
                let call_source = match terminator.kind {
                    TerminatorKind::Call { call_source, .. } => call_source,
                    TerminatorKind::TailCall { .. } => CallSource::Normal,
                    _ => unreachable!(),
                };

                let ConstCx { tcx, body, .. } = *self.ccx;

                let fn_ty = func.ty(body, tcx);

                let (callee, fn_args) = match *fn_ty.kind() {
                    ty::FnDef(def_id, fn_args) => (def_id, fn_args),

                    ty::FnPtr(..) => {
                        self.check_op(ops::FnCallIndirect);
                        // We can get here without an error in miri-unleashed mode... might as well
                        // skip the rest of the checks as well then.
                        return;
                    }
                    _ => {
                        span_bug!(terminator.source_info.span, "invalid callee of type {:?}", fn_ty)
                    }
                };

                let has_const_conditions =
                    self.revalidate_conditional_constness(callee, fn_args, *fn_span);

                // Attempting to call a trait method?
                if let Some(trait_did) = tcx.trait_of_assoc(callee) {
                    // We can't determine the actual callee (the underlying impl of the trait) here, so we have
                    // to do different checks than usual.

                    trace!("attempting to call a trait method");
                    let is_const = tcx.constness(callee) == hir::Constness::Const;

                    // Only consider a trait to be const if the const conditions hold.
                    // Otherwise, it's really misleading to call something "conditionally"
                    // const when it's very obviously not conditionally const.
                    if is_const && has_const_conditions == Some(ConstConditionsHold::Yes) {
                        // Trait calls are always conditionally-const.
                        self.check_op(ops::ConditionallyConstCall {
                            callee,
                            args: fn_args,
                            span: *fn_span,
                            call_source,
                        });
                        self.check_callee_stability(trait_did);
                    } else {
                        // Not even a const trait.
                        self.check_op(ops::FnCallNonConst {
                            callee,
                            args: fn_args,
                            span: *fn_span,
                            call_source,
                        });
                    }
                    // That's all we can check here.
                    return;
                }

                // Even if we know the callee, ensure we can use conditionally-const calls.
                if has_const_conditions.is_some() {
                    self.check_op(ops::ConditionallyConstCall {
                        callee,
                        args: fn_args,
                        span: *fn_span,
                        call_source,
                    });
                }

                if self.tcx.fn_sig(callee).skip_binder().c_variadic() {
                    self.check_op(ops::FnCallCVariadic)
                }

                // At this point, we are calling a function, `callee`, whose `DefId` is known...

                // `begin_panic` and `panic_display` functions accept generic
                // types other than str. Check to enforce that only str can be used in
                // const-eval.

                // const-eval of the `begin_panic` fn assumes the argument is `&str`
                if tcx.is_lang_item(callee, LangItem::BeginPanic) {
                    match args[0].node.ty(&self.ccx.body.local_decls, tcx).kind() {
                        ty::Ref(_, ty, _) if ty.is_str() => {}
                        _ => self.check_op(ops::PanicNonStr),
                    }
                    // Allow this call, skip all the checks below.
                    return;
                }

                // const-eval of `panic_display` assumes the argument is `&&str`
                if tcx.is_lang_item(callee, LangItem::PanicDisplay) {
                    if let ty::Ref(_, ty, _) =
                        args[0].node.ty(&self.ccx.body.local_decls, tcx).kind()
                        && let ty::Ref(_, ty, _) = ty.kind()
                        && ty.is_str()
                    {
                    } else {
                        self.check_op(ops::PanicNonStr);
                    }
                    // Allow this call, skip all the checks below.
                    return;
                }

                // Intrinsics are language primitives, not regular calls, so treat them separately.
                if let Some(intrinsic) = tcx.intrinsic(callee) {
                    if !tcx.is_const_fn(callee) {
                        // Non-const intrinsic.
                        self.check_op(ops::IntrinsicNonConst { name: intrinsic.name });
                        // If we allowed this, we're in miri-unleashed mode, so we might
                        // as well skip the remaining checks.
                        return;
                    }
                    // We use `intrinsic.const_stable` to determine if this can be safely exposed to
                    // stable code, rather than `const_stable_indirect`. This is to make
                    // `#[rustc_const_stable_indirect]` an attribute that is always safe to add.
                    // We also ask is_safe_to_expose_on_stable_const_fn; this determines whether the intrinsic
                    // fallback body is safe to expose on stable.
                    let is_const_stable = intrinsic.const_stable
                        || (!intrinsic.must_be_overridden
                            && is_fn_or_trait_safe_to_expose_on_stable(tcx, callee));
                    match tcx.lookup_const_stability(callee) {
                        None => {
                            // This doesn't need a separate const-stability check -- const-stability equals
                            // regular stability, and regular stability is checked separately.
                            // However, we *do* have to worry about *recursive* const stability.
                            if !is_const_stable && self.enforce_recursive_const_stability() {
                                self.dcx().emit_err(errors::UnmarkedIntrinsicExposed {
                                    span: self.span,
                                    def_path: self.tcx.def_path_str(callee),
                                });
                            }
                        }
                        Some(hir::ConstStability {
                            level: hir::StabilityLevel::Unstable { .. },
                            feature,
                            ..
                        }) => {
                            // We only honor `span.allows_unstable` aka `#[allow_internal_unstable]`
                            // if the callee is safe to expose, to avoid bypassing recursive stability.
                            // This is not ideal since it means the user sees an error, not the macro
                            // author, but that's also the case if one forgets to set
                            // `#[allow_internal_unstable]` in the first place.
                            if self.span.allows_unstable(feature) && is_const_stable {
                                return;
                            }

                            self.check_op(ops::IntrinsicUnstable {
                                name: intrinsic.name,
                                feature,
                                const_stable_indirect: is_const_stable,
                            });
                        }
                        Some(hir::ConstStability {
                            level: hir::StabilityLevel::Stable { .. },
                            ..
                        }) => {
                            // All good. Note that a `#[rustc_const_stable]` intrinsic (meaning it
                            // can be *directly* invoked from stable const code) does not always
                            // have the `#[rustc_intrinsic_const_stable_indirect]` attribute (which controls
                            // exposing an intrinsic indirectly); we accept this call anyway.
                        }
                    }
                    // This completes the checks for intrinsics.
                    return;
                }

                if !tcx.is_const_fn(callee) {
                    self.check_op(ops::FnCallNonConst {
                        callee,
                        args: fn_args,
                        span: *fn_span,
                        call_source,
                    });
                    // If we allowed this, we're in miri-unleashed mode, so we might
                    // as well skip the remaining checks.
                    return;
                }

                // Finally, stability for regular function calls -- this is the big one.
                self.check_callee_stability(callee);
            }

            // Forbid all `Drop` terminators unless the place being dropped is a local with no
            // projections that cannot be `NeedsNonConstDrop`.
            TerminatorKind::Drop { place: dropped_place, .. } => {
                // If we are checking live drops after drop-elaboration, don't emit duplicate
                // errors here.
                if super::post_drop_elaboration::checking_enabled(self.ccx) {
                    return;
                }

                self.check_drop_terminator(*dropped_place, location, terminator.source_info.span);
            }

            TerminatorKind::InlineAsm { .. } => self.check_op(ops::InlineAsm),

            TerminatorKind::Yield { .. } => {
                self.check_op(ops::Coroutine(
                    self.tcx
                        .coroutine_kind(self.body.source.def_id())
                        .expect("Only expected to have a yield in a coroutine"),
                ));
            }

            TerminatorKind::CoroutineDrop => {
                span_bug!(
                    self.body.source_info(location).span,
                    "We should not encounter TerminatorKind::CoroutineDrop after coroutine transform"
                );
            }

            TerminatorKind::UnwindTerminate(_) => {
                // Cleanup blocks are skipped for const checking (see `visit_basic_block_data`).
                span_bug!(self.span, "`Terminate` terminator outside of cleanup block")
            }

            TerminatorKind::Assert { .. }
            | TerminatorKind::FalseEdge { .. }
            | TerminatorKind::FalseUnwind { .. }
            | TerminatorKind::Goto { .. }
            | TerminatorKind::UnwindResume
            | TerminatorKind::Return
            | TerminatorKind::SwitchInt { .. }
            | TerminatorKind::Unreachable => {}
        }
    }
}

fn is_int_bool_float_or_char(ty: Ty<'_>) -> bool {
    ty.is_bool() || ty.is_integral() || ty.is_char() || ty.is_floating_point()
}

fn emit_unstable_in_stable_exposed_error(
    ccx: &ConstCx<'_, '_>,
    span: Span,
    gate: Symbol,
    is_function_call: bool,
) -> ErrorGuaranteed {
    let attr_span = ccx.tcx.def_span(ccx.def_id()).shrink_to_lo();

    ccx.dcx().emit_err(errors::UnstableInStableExposed {
        gate: gate.to_string(),
        span,
        attr_span,
        is_function_call,
        is_function_call2: is_function_call,
    })
}
