//! Checking that constant values used in types can be successfully evaluated.
//!
//! For concrete constants, this is fairly simple as we can just try and evaluate it.
//!
//! When dealing with polymorphic constants, for example `size_of::<T>() - 1`,
//! this is not as easy.
//!
//! In this case we try to build an abstract representation of this constant using
//! `thir_abstract_const` which can then be checked for structural equality with other
//! generic constants mentioned in the `caller_bounds` of the current environment.

use rustc_hir::def::DefKind;
use rustc_infer::infer::InferCtxt;
use rustc_middle::bug;
use rustc_middle::traits::ObligationCause;
use rustc_middle::ty::abstract_const::NotConstEvaluatable;
use rustc_middle::ty::{self, TyCtxt, TypeVisitable, TypeVisitableExt, TypeVisitor};
use rustc_span::{DUMMY_SP, Span};
use tracing::{debug, instrument};

use super::EvaluateConstErr;
use crate::traits::ObligationCtxt;

/// Check if a given constant can be evaluated.
#[instrument(skip(infcx), level = "debug")]
pub fn is_const_evaluatable<'tcx>(
    infcx: &InferCtxt<'tcx>,
    unexpanded_ct: ty::Const<'tcx>,
    param_env: ty::ParamEnv<'tcx>,
    span: Span,
) -> Result<(), NotConstEvaluatable> {
    let tcx = infcx.tcx;
    match tcx.expand_abstract_consts(unexpanded_ct).kind() {
        ty::ConstKind::Unevaluated(_) | ty::ConstKind::Expr(_) => (),
        ty::ConstKind::Param(_)
        | ty::ConstKind::Bound(_, _)
        | ty::ConstKind::Placeholder(_)
        | ty::ConstKind::Value(_)
        | ty::ConstKind::Error(_) => return Ok(()),
        ty::ConstKind::Infer(_) => return Err(NotConstEvaluatable::MentionsInfer),
    };

    if tcx.features().generic_const_exprs() {
        let ct = tcx.expand_abstract_consts(unexpanded_ct);

        let is_anon_ct = if let ty::ConstKind::Unevaluated(uv) = ct.kind() {
            tcx.def_kind(uv.def) == DefKind::AnonConst
        } else {
            false
        };

        if !is_anon_ct {
            if satisfied_from_param_env(tcx, infcx, ct, param_env) {
                return Ok(());
            }
            if ct.has_non_region_infer() {
                return Err(NotConstEvaluatable::MentionsInfer);
            } else if ct.has_non_region_param() {
                return Err(NotConstEvaluatable::MentionsParam);
            }
        }

        match unexpanded_ct.kind() {
            ty::ConstKind::Expr(_) => {
                // FIXME(generic_const_exprs): we have a fully concrete `ConstKind::Expr`, but
                // haven't implemented evaluating `ConstKind::Expr` yet, so we are unable to tell
                // if it is evaluatable or not. As this is unreachable for now, we can simple ICE
                // here.
                tcx.dcx().span_bug(span, "evaluating `ConstKind::Expr` is not currently supported");
            }
            ty::ConstKind::Unevaluated(_) => {
                match crate::traits::try_evaluate_const(infcx, unexpanded_ct, param_env) {
                    Err(EvaluateConstErr::HasGenericsOrInfers) => {
                        Err(NotConstEvaluatable::Error(infcx.dcx().span_delayed_bug(
                            span,
                            "Missing value for constant, but no error reported?",
                        )))
                    }
                    Err(
                        EvaluateConstErr::EvaluationFailure(e)
                        | EvaluateConstErr::InvalidConstParamTy(e),
                    ) => Err(NotConstEvaluatable::Error(e)),
                    Ok(_) => Ok(()),
                }
            }
            _ => bug!("unexpected constkind in `is_const_evalautable: {unexpanded_ct:?}`"),
        }
    } else if tcx.features().min_generic_const_args() {
        // This is a sanity check to make sure that non-generics consts are checked to
        // be evaluatable in case they aren't cchecked elsewhere. This will NOT error
        // if the const uses generics, as desired.
        crate::traits::evaluate_const(infcx, unexpanded_ct, param_env);
        Ok(())
    } else {
        let uv = match unexpanded_ct.kind() {
            ty::ConstKind::Unevaluated(uv) => uv,
            ty::ConstKind::Expr(_) => {
                bug!("`ConstKind::Expr` without `feature(generic_const_exprs)` enabled")
            }
            _ => bug!("unexpected constkind in `is_const_evalautable: {unexpanded_ct:?}`"),
        };

        match crate::traits::try_evaluate_const(infcx, unexpanded_ct, param_env) {
            // If we're evaluating a generic foreign constant, under a nightly compiler while
            // the current crate does not enable `feature(generic_const_exprs)`, abort
            // compilation with a useful error.
            Err(_)
                if tcx.sess.is_nightly_build()
                    && satisfied_from_param_env(
                        tcx,
                        infcx,
                        tcx.expand_abstract_consts(unexpanded_ct),
                        param_env,
                    ) =>
            {
                tcx.dcx()
                    .struct_span_fatal(
                        // Slightly better span than just using `span` alone
                        if span == DUMMY_SP { tcx.def_span(uv.def) } else { span },
                        "failed to evaluate generic const expression",
                    )
                    .with_note("the crate this constant originates from uses `#![feature(generic_const_exprs)]`")
                    .with_span_suggestion_verbose(
                        DUMMY_SP,
                        "consider enabling this feature",
                        "#![feature(generic_const_exprs)]\n",
                        rustc_errors::Applicability::MaybeIncorrect,
                    )
                    .emit()
            }

            Err(EvaluateConstErr::HasGenericsOrInfers) => {
                let err = if uv.has_non_region_infer() {
                    NotConstEvaluatable::MentionsInfer
                } else if uv.has_non_region_param() {
                    NotConstEvaluatable::MentionsParam
                } else {
                    let guar = infcx.dcx().span_delayed_bug(
                        span,
                        "Missing value for constant, but no error reported?",
                    );
                    NotConstEvaluatable::Error(guar)
                };

                Err(err)
            }
            Err(
                EvaluateConstErr::EvaluationFailure(e) | EvaluateConstErr::InvalidConstParamTy(e),
            ) => Err(NotConstEvaluatable::Error(e)),
            Ok(_) => Ok(()),
        }
    }
}

#[instrument(skip(infcx, tcx), level = "debug")]
fn satisfied_from_param_env<'tcx>(
    tcx: TyCtxt<'tcx>,
    infcx: &InferCtxt<'tcx>,
    ct: ty::Const<'tcx>,
    param_env: ty::ParamEnv<'tcx>,
) -> bool {
    // Try to unify with each subtree in the AbstractConst to allow for
    // `N + 1` being const evaluatable even if theres only a `ConstEvaluatable`
    // predicate for `(N + 1) * 2`
    struct Visitor<'a, 'tcx> {
        ct: ty::Const<'tcx>,
        param_env: ty::ParamEnv<'tcx>,

        infcx: &'a InferCtxt<'tcx>,
        single_match: Option<Result<ty::Const<'tcx>, ()>>,
    }

    impl<'a, 'tcx> TypeVisitor<TyCtxt<'tcx>> for Visitor<'a, 'tcx> {
        fn visit_const(&mut self, c: ty::Const<'tcx>) {
            debug!("is_const_evaluatable: candidate={:?}", c);
            if self.infcx.probe(|_| {
                let ocx = ObligationCtxt::new(self.infcx);
                ocx.eq(&ObligationCause::dummy(), self.param_env, c, self.ct).is_ok()
                    && ocx.evaluate_obligations_error_on_ambiguity().is_empty()
            }) {
                self.single_match = match self.single_match {
                    None => Some(Ok(c)),
                    Some(Ok(o)) if o == c => Some(Ok(c)),
                    Some(_) => Some(Err(())),
                };
            }

            if let ty::ConstKind::Expr(e) = c.kind() {
                e.visit_with(self);
            } else {
                // FIXME(generic_const_exprs): This doesn't recurse into `<T as Trait<U>>::ASSOC`'s args.
                // This is currently unobservable as `<T as Trait<{ U + 1 }>>::ASSOC` creates an anon const
                // with its own `ConstEvaluatable` bound in the param env which we will visit separately.
                //
                // If we start allowing directly writing `ConstKind::Expr` without an intermediate anon const
                // this will be incorrect. It might be worth investigating making `predicates_of` elaborate
                // all of the `ConstEvaluatable` bounds rather than having a visitor here.
            }
        }
    }

    let mut single_match: Option<Result<ty::Const<'tcx>, ()>> = None;

    for pred in param_env.caller_bounds() {
        match pred.kind().skip_binder() {
            ty::ClauseKind::ConstEvaluatable(ce) => {
                let b_ct = tcx.expand_abstract_consts(ce);
                let mut v = Visitor { ct, infcx, param_env, single_match };
                let _ = b_ct.visit_with(&mut v);

                single_match = v.single_match;
            }
            _ => {} // don't care
        }
    }

    if let Some(Ok(c)) = single_match {
        let ocx = ObligationCtxt::new(infcx);
        assert!(ocx.eq(&ObligationCause::dummy(), param_env, c, ct).is_ok());
        assert!(ocx.evaluate_obligations_error_on_ambiguity().is_empty());
        return true;
    }

    debug!("is_const_evaluatable: no");
    false
}
