/-
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.Compiler.IR.CompilerM
import Lean.Compiler.IR.Format

namespace Lean.IR.Checker

@[extern c inline "lean_box(LEAN_MAX_CTOR_FIELDS)"]
constant getMaxCtorFields : Unit → Nat
def maxCtorFields := getMaxCtorFields ()

@[extern c inline "lean_box(LEAN_MAX_CTOR_SCALARS_SIZE)"]
constant getMaxCtorScalarsSize : Unit → Nat
def maxCtorScalarsSize := getMaxCtorScalarsSize ()

@[extern c inline "lean_box(sizeof(size_t))"]
constant getUSizeSize : Unit → Nat
def usizeSize := getUSizeSize ()

structure CheckerContext where
  env : Environment
  localCtx : LocalContext := {}
  decls : Array Decl

structure CheckerState where
  foundVars : IndexSet := {}

abbrev M := ReaderT CheckerContext (ExceptT String (StateT CheckerState Id))

def markIndex (i : Index) : M Unit := do
  let s ← get
  if s.foundVars.contains i then
    throw s!"variable / joinpoint index {i} has already been used"
  modify fun s => { s with foundVars := s.foundVars.insert i }

def markVar (x : VarId) : M Unit :=
  markIndex x.idx

def markJP (j : JoinPointId) : M Unit :=
  markIndex j.idx

def getDecl (c : Name) : M Decl := do
  let ctx ← read
  match findEnvDecl' ctx.env c ctx.decls with
  | none   => throw s!"unknown declaration '{c}'"
  | some d => pure d

def checkVar (x : VarId) : M Unit := do
  let ctx ← read
  unless ctx.localCtx.isLocalVar x.idx || ctx.localCtx.isParam x.idx do
   throw s!"unknown variable '{x}'"

def checkJP (j : JoinPointId) : M Unit := do
  let ctx ← read
  unless ctx.localCtx.isJP j.idx do
   throw s!"unknown join point '{j}'"

def checkArg (a : Arg) : M Unit :=
  match a with
  | Arg.var x => checkVar x
  | _ => pure ()

def checkArgs (as : Array Arg) : M Unit :=
  as.forM checkArg

@[inline] def checkEqTypes (ty₁ ty₂ : IRType) : M Unit := do
  unless ty₁ == ty₂ do
    throw "unexpected type '{ty₁}' != '{ty₂}'"

@[inline] def checkType (ty : IRType) (p : IRType → Bool) (suffix? : Option String := none): M Unit := do
  unless p ty do
   let mut msg := s!"unexpected type '{ty}'"
   if let some suffix := suffix? then
     msg := s!"{msg}, {suffix}"
   throw msg

def checkObjType (ty : IRType) : M Unit := checkType ty IRType.isObj "object expected"

def checkScalarType (ty : IRType) : M Unit := checkType ty IRType.isScalar "scalar expected"

def getType (x : VarId) : M IRType := do
  let ctx ← read
  match ctx.localCtx.getType x with
  | some ty => pure ty
  | none    => throw s!"unknown variable '{x}'"

@[inline] def checkVarType (x : VarId) (p : IRType → Bool) (suffix? : Option String := none) : M Unit := do
  let ty ← getType x; checkType ty p suffix?

def checkObjVar (x : VarId) : M Unit :=
  checkVarType x IRType.isObj "object expected"

def checkScalarVar (x : VarId) : M Unit :=
  checkVarType x IRType.isScalar "scalar expected"

def checkFullApp (c : FunId) (ys : Array Arg) : M Unit := do
  let decl ← getDecl c
  unless ys.size == decl.params.size do
    throw s!"incorrect number of arguments to '{c}', {ys.size} provided, {decl.params.size} expected"
  checkArgs ys

def checkPartialApp (c : FunId) (ys : Array Arg) : M Unit := do
  let decl ← getDecl c
  unless ys.size < decl.params.size do
    throw s!"too many arguments too partial application '{c}', num. args: {ys.size}, arity: {decl.params.size}"
  checkArgs ys

def checkExpr (ty : IRType) : Expr → M Unit
  | Expr.pap f ys           => checkPartialApp f ys *> checkObjType ty -- partial applications should always produce a closure object
  | Expr.ap x ys            => checkObjVar x *> checkArgs ys
  | Expr.fap f ys           => checkFullApp f ys
  | Expr.ctor c ys          => do
    if c.size > maxCtorFields then
      throw s!"constructor '{c.name}' has too many fields"
    if c.ssize + c.usize * usizeSize > maxCtorScalarsSize then
      throw s!"constructor '{c.name}' has too many scalar fields"
    if !ty.isStruct && !ty.isUnion && c.isRef then
      (checkObjType ty) *> checkArgs ys
  | Expr.reset _ x          => checkObjVar x *> checkObjType ty
  | Expr.reuse x _ _ ys     => checkObjVar x *> checkArgs ys *> checkObjType ty
  | Expr.box xty x          => checkObjType ty *> checkScalarVar x *> checkVarType x (fun t => t == xty)
  | Expr.unbox x            => checkScalarType ty *> checkObjVar x
  | Expr.proj i x           => do
    let xType ← getType x;
    match xType with
    | IRType.object       => checkObjType ty
    | IRType.tobject      => checkObjType ty
    | IRType.struct _ tys => if h : i < tys.size then checkEqTypes (tys.get ⟨i,h⟩) ty else throw "invalid proj index"
    | IRType.union _ tys  => if h : i < tys.size then checkEqTypes (tys.get ⟨i,h⟩) ty else throw "invalid proj index"
    | _                   => throw s!"unexpected IR type '{xType}'"
  | Expr.uproj _ x          => checkObjVar x *> checkType ty (fun t => t == IRType.usize)
  | Expr.sproj _ _ x        => checkObjVar x *> checkScalarType ty
  | Expr.isShared x         => checkObjVar x *> checkType ty (fun t => t == IRType.uint8)
  | Expr.isTaggedPtr x      => checkObjVar x *> checkType ty (fun t => t == IRType.uint8)
  | Expr.lit (LitVal.str _) => checkObjType ty
  | Expr.lit _              => pure ()

@[inline] def withParams (ps : Array Param) (k : M Unit) : M Unit := do
  let ctx ← read
  let localCtx ← ps.foldlM (init := ctx.localCtx) fun (ctx : LocalContext) p => do
     markVar p.x
     pure $ ctx.addParam p
  withReader (fun _ => { ctx with localCtx := localCtx }) k

partial def checkFnBody : FnBody → M Unit
  | FnBody.vdecl x t v b    => do
    checkExpr t v
    markVar x
    withReader (fun ctx => { ctx with localCtx := ctx.localCtx.addLocal x t v }) (checkFnBody b)
  | FnBody.jdecl j ys v b => do
    markJP j
    withParams ys (checkFnBody v)
    withReader (fun ctx => { ctx with localCtx := ctx.localCtx.addJP j ys v }) (checkFnBody b)
  | FnBody.set x _ y b      => checkVar x *> checkArg y *> checkFnBody b
  | FnBody.uset x _ y b     => checkVar x *> checkVar y *> checkFnBody b
  | FnBody.sset x _ _ y _ b => checkVar x *> checkVar y *> checkFnBody b
  | FnBody.setTag x _ b     => checkVar x *> checkFnBody b
  | FnBody.inc x _ _ _ b    => checkVar x *> checkFnBody b
  | FnBody.dec x _ _ _ b    => checkVar x *> checkFnBody b
  | FnBody.del x b          => checkVar x *> checkFnBody b
  | FnBody.mdata _ b        => checkFnBody b
  | FnBody.jmp j ys         => checkJP j *> checkArgs ys
  | FnBody.ret x            => checkArg x
  | FnBody.case _ x _ alts  => checkVar x *> alts.forM (fun alt => checkFnBody alt.body)
  | FnBody.unreachable      => pure ()

def checkDecl : Decl → M Unit
  | Decl.fdecl (xs := xs) (body := b) .. => withParams xs (checkFnBody b)
  | Decl.extern (xs := xs) .. => withParams xs (pure ())

end Checker

def checkDecl (decls : Array Decl) (decl : Decl) : CompilerM Unit := do
  let env ← getEnv
  match (Checker.checkDecl decl { env := env, decls := decls }).run' {} with
  | .error msg => throw s!"IR check failed at '{decl.name}', error: {msg}"
  | _ => pure ()

def checkDecls (decls : Array Decl) : CompilerM Unit :=
  decls.forM (checkDecl decls)

end IR
end Lean
