// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

// Verifies that the types and values of const and static items
// are safe. The rules enforced by this module are:
//
// - For each *mutable* static item, it checks that its **type**:
//     - doesn't have a destructor
//     - doesn't own a box
//
// - For each *immutable* static item, it checks that its **value**:
//       - doesn't own a box
//       - doesn't contain a struct literal or a call to an enum variant / struct constructor where
//           - the type of the struct/enum has a dtor
//
// Rules Enforced Elsewhere:
// - It's not possible to take the address of a static item with unsafe interior. This is enforced
// by borrowck::gather_loans

use rustc::dep_graph::DepNode;
use rustc::ty::cast::CastKind;
use rustc_const_eval::{ConstEvalErr, ConstContext};
use rustc_const_eval::ErrKind::{IndexOpFeatureGated, UnimplementedConstVal, MiscCatchAll, Math};
use rustc_const_eval::ErrKind::{ErroneousReferencedConstant, MiscBinaryOp, NonConstPath, BadType};
use rustc_const_eval::ErrKind::UnresolvedPath;
use rustc_const_eval::EvalHint::ExprTypeChecked;
use rustc_const_math::{ConstMathErr, Op};
use rustc::hir::def::{Def, CtorKind};
use rustc::hir::def_id::DefId;
use rustc::hir::map::blocks::FnLikeNode;
use rustc::middle::expr_use_visitor as euv;
use rustc::middle::mem_categorization as mc;
use rustc::middle::mem_categorization::Categorization;
use rustc::mir::transform::MirSource;
use rustc::ty::{self, Ty, TyCtxt};
use rustc::traits::Reveal;
use rustc::util::common::ErrorReported;
use rustc::util::nodemap::NodeSet;
use rustc::lint::builtin::CONST_ERR;

use rustc::hir::{self, PatKind, RangeEnd};
use syntax::ast;
use syntax_pos::Span;
use rustc::hir::intravisit::{self, Visitor, NestedVisitorMap};

use std::collections::hash_map::Entry;
use std::cmp::Ordering;
use std::mem;

struct CheckCrateVisitor<'a, 'tcx: 'a> {
    tcx: TyCtxt<'a, 'tcx, 'tcx>,
    in_fn: bool,
    promotable: bool,
    mut_rvalue_borrows: NodeSet,
    param_env: ty::ParameterEnvironment<'tcx>,
    tables: &'a ty::TypeckTables<'tcx>,
}

impl<'a, 'gcx> CheckCrateVisitor<'a, 'gcx> {
    fn check_const_eval(&self, expr: &'gcx hir::Expr) {
        let const_cx = ConstContext::with_tables(self.tcx, self.tables);
        if let Err(err) = const_cx.eval(expr, ExprTypeChecked) {
            match err.kind {
                UnimplementedConstVal(_) => {}
                IndexOpFeatureGated => {}
                ErroneousReferencedConstant(_) => {}
                BadType(_) => {}
                _ => {
                    self.tcx.sess.add_lint(CONST_ERR,
                                           expr.id,
                                           expr.span,
                                           format!("constant evaluation error: {}. This will \
                                                    become a HARD ERROR in the future",
                                                   err.description().into_oneline()))
                }
            }
        }
    }

    // Adds the worst effect out of all the values of one type.
    fn add_type(&mut self, ty: Ty<'gcx>) {
        if ty.type_contents(self.tcx).interior_unsafe() {
            self.promotable = false;
        }

        if self.tcx.type_needs_drop_given_env(ty, &self.param_env) {
            self.promotable = false;
        }
    }

    fn handle_const_fn_call(&mut self, def_id: DefId, ret_ty: Ty<'gcx>) {
        self.add_type(ret_ty);

        self.promotable &= if let Some(fn_id) = self.tcx.hir.as_local_node_id(def_id) {
            FnLikeNode::from_node(self.tcx.hir.get(fn_id)).map_or(false, |fn_like| {
                fn_like.constness() == hir::Constness::Const
            })
        } else {
            self.tcx.sess.cstore.is_const_fn(def_id)
        };
    }
}

impl<'a, 'tcx> Visitor<'tcx> for CheckCrateVisitor<'a, 'tcx> {
    fn nested_visit_map<'this>(&'this mut self) -> NestedVisitorMap<'this, 'tcx> {
        NestedVisitorMap::None
    }

    fn visit_nested_body(&mut self, body_id: hir::BodyId) {
        match self.tcx.rvalue_promotable_to_static.borrow_mut().entry(body_id.node_id) {
            Entry::Occupied(_) => return,
            Entry::Vacant(entry) => {
                // Prevent infinite recursion on re-entry.
                entry.insert(false);
            }
        }

        let item_id = self.tcx.hir.body_owner(body_id);

        let outer_in_fn = self.in_fn;
        self.in_fn = match MirSource::from_node(self.tcx, item_id) {
            MirSource::Fn(_) => true,
            _ => false
        };

        let outer_tables = self.tables;
        self.tables = self.tcx.item_tables(self.tcx.hir.local_def_id(item_id));

        let body = self.tcx.hir.body(body_id);
        if !self.in_fn {
            self.check_const_eval(&body.value);
        }

        let outer_penv = self.tcx.infer_ctxt(body_id, Reveal::NotSpecializable).enter(|infcx| {
            let param_env = infcx.parameter_environment.clone();
            let outer_penv = mem::replace(&mut self.param_env, param_env);
            euv::ExprUseVisitor::new(self, &infcx).consume_body(body);
            outer_penv
        });

        self.visit_body(body);

        self.param_env = outer_penv;
        self.tables = outer_tables;
        self.in_fn = outer_in_fn;
    }

    fn visit_pat(&mut self, p: &'tcx hir::Pat) {
        match p.node {
            PatKind::Lit(ref lit) => {
                self.check_const_eval(lit);
            }
            PatKind::Range(ref start, ref end, RangeEnd::Excluded) => {
                let const_cx = ConstContext::with_tables(self.tcx, self.tables);
                match const_cx.compare_lit_exprs(p.span, start, end) {
                    Ok(Ordering::Less) => {}
                    Ok(Ordering::Equal) |
                    Ok(Ordering::Greater) => {
                        span_err!(self.tcx.sess,
                                  start.span,
                                  E0579,
                                  "lower range bound must be less than upper");
                    }
                    Err(ErrorReported) => {}
                }
            }
            PatKind::Range(ref start, ref end, RangeEnd::Included) => {
                let const_cx = ConstContext::with_tables(self.tcx, self.tables);
                match const_cx.compare_lit_exprs(p.span, start, end) {
                    Ok(Ordering::Less) |
                    Ok(Ordering::Equal) => {}
                    Ok(Ordering::Greater) => {
                        struct_span_err!(self.tcx.sess, start.span, E0030,
                            "lower range bound must be less than or equal to upper")
                            .span_label(start.span, &format!("lower bound larger than upper bound"))
                            .emit();
                    }
                    Err(ErrorReported) => {}
                }
            }
            _ => {}
        }
        intravisit::walk_pat(self, p);
    }

    fn visit_stmt(&mut self, stmt: &'tcx hir::Stmt) {
        match stmt.node {
            hir::StmtDecl(ref decl, _) => {
                match decl.node {
                    hir::DeclLocal(_) => {
                        self.promotable = false;
                    }
                    // Item statements are allowed
                    hir::DeclItem(_) => {}
                }
            }
            hir::StmtExpr(..) |
            hir::StmtSemi(..) => {
                self.promotable = false;
            }
        }
        intravisit::walk_stmt(self, stmt);
    }

    fn visit_expr(&mut self, ex: &'tcx hir::Expr) {
        let outer = self.promotable;
        self.promotable = true;

        let node_ty = self.tables.node_id_to_type(ex.id);
        check_expr(self, ex, node_ty);
        check_adjustments(self, ex);

        if let hir::ExprMatch(ref discr, ref arms, _) = ex.node {
            // Compute the most demanding borrow from all the arms'
            // patterns and set that on the discriminator.
            let mut mut_borrow = false;
            for pat in arms.iter().flat_map(|arm| &arm.pats) {
                if self.mut_rvalue_borrows.remove(&pat.id) {
                    mut_borrow = true;
                }
            }
            if mut_borrow {
                self.mut_rvalue_borrows.insert(discr.id);
            }
        }

        intravisit::walk_expr(self, ex);

        // Handle borrows on (or inside the autorefs of) this expression.
        if self.mut_rvalue_borrows.remove(&ex.id) {
            self.promotable = false;
        }

        if self.in_fn && self.promotable {
            let const_cx = ConstContext::with_tables(self.tcx, self.tables);
            match const_cx.eval(ex, ExprTypeChecked) {
                Ok(_) => {}
                Err(ConstEvalErr { kind: UnimplementedConstVal(_), .. }) |
                Err(ConstEvalErr { kind: MiscCatchAll, .. }) |
                Err(ConstEvalErr { kind: MiscBinaryOp, .. }) |
                Err(ConstEvalErr { kind: NonConstPath, .. }) |
                Err(ConstEvalErr { kind: UnresolvedPath, .. }) |
                Err(ConstEvalErr { kind: ErroneousReferencedConstant(_), .. }) |
                Err(ConstEvalErr { kind: Math(ConstMathErr::Overflow(Op::Shr)), .. }) |
                Err(ConstEvalErr { kind: Math(ConstMathErr::Overflow(Op::Shl)), .. }) |
                Err(ConstEvalErr { kind: IndexOpFeatureGated, .. }) => {}
                Err(ConstEvalErr { kind: BadType(_), .. }) => {}
                Err(msg) => {
                    self.tcx.sess.add_lint(CONST_ERR,
                                           ex.id,
                                           msg.span,
                                           msg.description().into_oneline().into_owned())
                }
            }
        }

        self.tcx.rvalue_promotable_to_static.borrow_mut().insert(ex.id, self.promotable);
        self.promotable &= outer;
    }
}

/// This function is used to enforce the constraints on
/// const/static items. It walks through the *value*
/// of the item walking down the expression and evaluating
/// every nested expression. If the expression is not part
/// of a const/static item, it is qualified for promotion
/// instead of producing errors.
fn check_expr<'a, 'tcx>(v: &mut CheckCrateVisitor<'a, 'tcx>, e: &hir::Expr, node_ty: Ty<'tcx>) {
    match node_ty.sty {
        ty::TyAdt(def, _) if def.has_dtor() => {
            v.promotable = false;
        }
        _ => {}
    }

    let method_call = ty::MethodCall::expr(e.id);
    match e.node {
        hir::ExprUnary(..) |
        hir::ExprBinary(..) |
        hir::ExprIndex(..) if v.tables.method_map.contains_key(&method_call) => {
            v.promotable = false;
        }
        hir::ExprBox(_) => {
            v.promotable = false;
        }
        hir::ExprUnary(op, ref inner) => {
            match v.tables.node_id_to_type(inner.id).sty {
                ty::TyRawPtr(_) => {
                    assert!(op == hir::UnDeref);

                    v.promotable = false;
                }
                _ => {}
            }
        }
        hir::ExprBinary(op, ref lhs, _) => {
            match v.tables.node_id_to_type(lhs.id).sty {
                ty::TyRawPtr(_) => {
                    assert!(op.node == hir::BiEq || op.node == hir::BiNe ||
                            op.node == hir::BiLe || op.node == hir::BiLt ||
                            op.node == hir::BiGe || op.node == hir::BiGt);

                    v.promotable = false;
                }
                _ => {}
            }
        }
        hir::ExprCast(ref from, _) => {
            debug!("Checking const cast(id={})", from.id);
            match v.tables.cast_kinds.get(&from.id) {
                None => span_bug!(e.span, "no kind for cast"),
                Some(&CastKind::PtrAddrCast) | Some(&CastKind::FnPtrAddrCast) => {
                    v.promotable = false;
                }
                _ => {}
            }
        }
        hir::ExprPath(ref qpath) => {
            let def = v.tables.qpath_def(qpath, e.id);
            match def {
                Def::VariantCtor(..) | Def::StructCtor(..) |
                Def::Fn(..) | Def::Method(..) => {}
                Def::AssociatedConst(_) => v.add_type(node_ty),
                Def::Const(did) => {
                    v.promotable &= if let Some(node_id) = v.tcx.hir.as_local_node_id(did) {
                        match v.tcx.hir.expect_item(node_id).node {
                            hir::ItemConst(_, body) => {
                                v.visit_nested_body(body);
                                v.tcx.rvalue_promotable_to_static.borrow()[&body.node_id]
                            }
                            _ => false
                        }
                    } else {
                        v.tcx.sess.cstore.const_is_rvalue_promotable_to_static(did)
                    };
                }
                _ => {
                    v.promotable = false;
                }
            }
        }
        hir::ExprCall(ref callee, _) => {
            let mut callee = &**callee;
            loop {
                callee = match callee.node {
                    hir::ExprBlock(ref block) => match block.expr {
                        Some(ref tail) => &tail,
                        None => break
                    },
                    _ => break
                };
            }
            // The callee is an arbitrary expression, it doesn't necessarily have a definition.
            let def = if let hir::ExprPath(ref qpath) = callee.node {
                v.tables.qpath_def(qpath, callee.id)
            } else {
                Def::Err
            };
            match def {
                Def::StructCtor(_, CtorKind::Fn) |
                Def::VariantCtor(_, CtorKind::Fn) => {}
                Def::Fn(did) => {
                    v.handle_const_fn_call(did, node_ty)
                }
                Def::Method(did) => {
                    match v.tcx.associated_item(did).container {
                        ty::ImplContainer(_) => {
                            v.handle_const_fn_call(did, node_ty)
                        }
                        ty::TraitContainer(_) => v.promotable = false
                    }
                }
                _ => v.promotable = false
            }
        }
        hir::ExprMethodCall(..) => {
            let method = v.tables.method_map[&method_call];
            match v.tcx.associated_item(method.def_id).container {
                ty::ImplContainer(_) => v.handle_const_fn_call(method.def_id, node_ty),
                ty::TraitContainer(_) => v.promotable = false
            }
        }
        hir::ExprStruct(..) => {
            if let ty::TyAdt(adt, ..) = v.tables.expr_ty(e).sty {
                // unsafe_cell_type doesn't necessarily exist with no_core
                if Some(adt.did) == v.tcx.lang_items.unsafe_cell_type() {
                    v.promotable = false;
                }
            }
        }

        hir::ExprLit(_) |
        hir::ExprAddrOf(..) |
        hir::ExprRepeat(..) => {}

        hir::ExprClosure(..) => {
            // Paths in constant contexts cannot refer to local variables,
            // as there are none, and thus closures can't have upvars there.
            if v.tcx.with_freevars(e.id, |fv| !fv.is_empty()) {
                v.promotable = false;
            }
        }

        hir::ExprBlock(_) |
        hir::ExprIndex(..) |
        hir::ExprField(..) |
        hir::ExprTupField(..) |
        hir::ExprArray(_) |
        hir::ExprType(..) |
        hir::ExprTup(..) => {}

        // Conditional control flow (possible to implement).
        hir::ExprMatch(..) |
        hir::ExprIf(..) |

        // Loops (not very meaningful in constants).
        hir::ExprWhile(..) |
        hir::ExprLoop(..) |

        // More control flow (also not very meaningful).
        hir::ExprBreak(..) |
        hir::ExprAgain(_) |
        hir::ExprRet(_) |

        // Expressions with side-effects.
        hir::ExprAssign(..) |
        hir::ExprAssignOp(..) |
        hir::ExprInlineAsm(..) => {
            v.promotable = false;
        }
    }
}

/// Check the adjustments of an expression
fn check_adjustments<'a, 'tcx>(v: &mut CheckCrateVisitor<'a, 'tcx>, e: &hir::Expr) {
    use rustc::ty::adjustment::*;

    match v.tables.adjustments.get(&e.id).map(|adj| adj.kind) {
        None |
        Some(Adjust::NeverToAny) |
        Some(Adjust::ReifyFnPointer) |
        Some(Adjust::UnsafeFnPointer) |
        Some(Adjust::MutToConstPointer) => {}

        Some(Adjust::DerefRef { autoderefs, .. }) => {
            if (0..autoderefs as u32)
                .any(|autoderef| v.tables.is_overloaded_autoderef(e.id, autoderef)) {
                v.promotable = false;
            }
        }
    }
}

pub fn check_crate<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>) {
    tcx.visit_all_item_likes_in_krate(DepNode::CheckConst,
                                      &mut CheckCrateVisitor {
                                          tcx: tcx,
                                          tables: &ty::TypeckTables::empty(),
                                          in_fn: false,
                                          promotable: false,
                                          mut_rvalue_borrows: NodeSet(),
                                          param_env: tcx.empty_parameter_environment(),
                                      }.as_deep_visitor());
    tcx.sess.abort_if_errors();
}

impl<'a, 'gcx, 'tcx> euv::Delegate<'tcx> for CheckCrateVisitor<'a, 'gcx> {
    fn consume(&mut self,
               _consume_id: ast::NodeId,
               _consume_span: Span,
               _cmt: mc::cmt,
               _mode: euv::ConsumeMode) {}

    fn borrow(&mut self,
              borrow_id: ast::NodeId,
              _borrow_span: Span,
              cmt: mc::cmt<'tcx>,
              _loan_region: &'tcx ty::Region,
              bk: ty::BorrowKind,
              loan_cause: euv::LoanCause) {
        // Kind of hacky, but we allow Unsafe coercions in constants.
        // These occur when we convert a &T or *T to a *U, as well as
        // when making a thin pointer (e.g., `*T`) into a fat pointer
        // (e.g., `*Trait`).
        match loan_cause {
            euv::LoanCause::AutoUnsafe => {
                return;
            }
            _ => {}
        }

        let mut cur = &cmt;
        loop {
            match cur.cat {
                Categorization::Rvalue(..) => {
                    if loan_cause == euv::MatchDiscriminant {
                        // Ignore the dummy immutable borrow created by EUV.
                        break;
                    }
                    if bk.to_mutbl_lossy() == hir::MutMutable {
                        self.mut_rvalue_borrows.insert(borrow_id);
                    }
                    break;
                }
                Categorization::StaticItem => {
                    break;
                }
                Categorization::Deref(ref cmt, ..) |
                Categorization::Downcast(ref cmt, _) |
                Categorization::Interior(ref cmt, _) => {
                    cur = cmt;
                }

                Categorization::Upvar(..) |
                Categorization::Local(..) => break,
            }
        }
    }

    fn decl_without_init(&mut self, _id: ast::NodeId, _span: Span) {}
    fn mutate(&mut self,
              _assignment_id: ast::NodeId,
              _assignment_span: Span,
              _assignee_cmt: mc::cmt,
              _mode: euv::MutateMode) {
    }

    fn matched_pat(&mut self, _: &hir::Pat, _: mc::cmt, _: euv::MatchMode) {}

    fn consume_pat(&mut self, _consume_pat: &hir::Pat, _cmt: mc::cmt, _mode: euv::ConsumeMode) {}
}
