/-
Copyright (c) 2019 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
import Lean.Meta.InferType
import Lean.Meta.LevelDefEq

/-
This is not the Kernel type checker, but an auxiliary method for checking
whether terms produced by tactics and `isDefEq` are type correct.
-/

namespace Lean
namespace Meta

private def ensureType (e : Expr) : MetaM Unit := do
_ ← getLevel e; pure ()

def throwLetTypeMismatchMessage {α} (fvarId : FVarId) : MetaM α := do
lctx ← getLCtx;
match lctx.find? fvarId with
| some (LocalDecl.ldecl _ _ n t v _) => do
  vType ← inferType v;
  throwError $
     "invalid let declaration, term" ++ indentExpr v
    ++ Format.line ++ "has type " ++ indentExpr vType
    ++ Format.line ++ "but is expected to have type" ++ indentExpr t
| _ => unreachable!

@[specialize] private def checkLambdaLet
    (check   : Expr → MetaM Unit)
    (e : Expr) : MetaM Unit :=
lambdaLetTelescope e $ fun xs b => do
  xs.forM $ fun x => do {
    xDecl ← getFVarLocalDecl x;
    match xDecl with
    | LocalDecl.cdecl _ _ _ t _ => do
      ensureType t;
      check t
    | LocalDecl.ldecl _ _ _ t v _ => do
      ensureType t;
      check t;
      vType ← inferType v;
      unlessM (isDefEq t vType) $ throwLetTypeMismatchMessage x.fvarId!;
      check v
  };
  check b

@[specialize] private def checkForall
    (check   : Expr → MetaM Unit)
    (e : Expr) : MetaM Unit :=
forallTelescope e $ fun xs b => do
  xs.forM $ fun x => do {
    xDecl ← getFVarLocalDecl x;
    ensureType xDecl.type;
    check xDecl.type
  };
  ensureType b;
  check b

private def checkConstant (constName : Name) (us : List Level) : MetaM Unit := do
cinfo ← getConstInfo constName;
unless (us.length == cinfo.lparams.length) $ throwIncorrectNumberOfLevels constName us

private def getFunctionDomain (f : Expr) : MetaM Expr := do
fType ← inferType f;
fType ← whnfD fType;
match fType with
| Expr.forallE _ d _ _ => pure d
| _                    => throwFunctionExpected f

def throwAppTypeMismatch {α} {m} [Monad m] [MonadExceptOf Exception m] [Ref m] [AddErrorMessageContext m] [MonadLiftT MetaM m]
    (f a : Expr) (extraMsg : MessageData := Format.nil) : m α := do
let e := mkApp f a;
aType ← inferType a;
expectedType ← liftM $ getFunctionDomain f;
throwError $
  "application type mismatch" ++ indentExpr e
  ++ Format.line ++ "argument" ++ indentExpr a
  ++ Format.line ++ "has type" ++ indentExpr aType
  ++ Format.line ++ "but is expected to have type" ++ indentExpr expectedType
  ++ extraMsg

@[specialize] private def checkApp
    (check   : Expr → MetaM Unit)
    (f a : Expr) : MetaM Unit := do
check f;
check a;
fType ← inferType f;
fType ← whnf fType;
match fType with
| Expr.forallE _ d _ _ => do
  aType ← inferType a;
  unlessM (isDefEq d aType) $ throwAppTypeMismatch f a
| _ => throwFunctionExpected (mkApp f a)

private partial def checkAux : Expr → MetaM Unit
| e@(Expr.forallE _ _ _ _) => checkForall checkAux e
| e@(Expr.lam _ _ _ _)     => checkLambdaLet checkAux e
| e@(Expr.letE _ _ _ _ _)  => checkLambdaLet checkAux e
| Expr.const c lvls _      => checkConstant c lvls
| Expr.app f a _           => checkApp checkAux f a
| Expr.mdata _ e _         => checkAux e
| Expr.proj _ _ e _        => checkAux e
| _                        => pure ()

def check (e : Expr) : MetaM Unit :=
traceCtx `Meta.check $
  withTransparency TransparencyMode.all $ checkAux e

def isTypeCorrect (e : Expr) : MetaM Bool :=
catch
  (do check e; pure true)
  (fun ex => do
    trace! `Meta.typeError ex.toMessageData;
    pure false)

@[init] private def regTraceClasses : IO Unit := do
registerTraceClass `Meta.check;
registerTraceClass `Meta.typeError

end Meta
end Lean
