/-
Copyright (c) 2018 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import init.lean.ir.instances init.lean.ir.format

namespace lean
namespace ir
@[reducible] def ssa_pre_m := except_t format (state_t var2blockid id)

def var.declare (x : var) : reader_t blockid ssa_pre_m unit :=
do m ← get,
   if m.contains x then throw ("already defined " ++ to_fmt x)
   else do b ← read, put (m.insert x b)

def instr.declare_vars : instr → reader_t blockid ssa_pre_m unit
| (instr.assign x _ _)           := x.declare
| (instr.assign_lit x _ _)       := x.declare
| (instr.assign_unop x _ _ _)    := x.declare
| (instr.assign_binop x _ _ _ _) := x.declare
| (instr.call xs _ _)            := xs.mfor var.declare
| (instr.cnstr o _ _ _)          := o.declare
| (instr.get x _ _)              := x.declare
| (instr.sget x _ _ _)           := x.declare
| (instr.closure x _ _)          := x.declare
| (instr.apply x _)              := x.declare
| (instr.array a _ _)            := a.declare
| (instr.sarray x _ _ _)         := x.declare
| _                              := pure ()

def phi.declare  (p : phi) : reader_t blockid ssa_pre_m unit :=
p.decorate_error p.x.declare

def block.declare_vars (b : block) : ssa_pre_m unit :=
b.decorate_error $ (b.phis.mfor phi.declare *> b.instrs.mfor instr.declare_vars).run b.id

def arg.declare (a : arg) : reader_t blockid ssa_pre_m unit :=
a.n.declare

/- Collect where each variable is declared, and
   check whether each variable was declared at most once. -/
def decl.declare_vars : decl → ssa_pre_m unit
| (decl.defn h (b::bs)) :=
  /- We assume that arguments are declared in the first basic block. -/
  h.decorate_error $ (h.args.mfor arg.declare).run b.id *> b.declare_vars *> bs.mfor block.declare_vars
| (decl.defn _  []) := throw "declaration must have at least one basic block"
| _                 := pure ()

/- Generate the mapping from variable to blockid for the given declaration.
   This function assumes `d` is in SSA. -/
def decl.var2blockid (d : decl) : except format var2blockid :=
run (d.declare_vars *> get) mk_var2blockid

@[reducible] def ssa_valid_m := except_t format (reader_t var2blockid (state_t var_set id))

def ssa_valid_m.run {α} (a : ssa_valid_m α) (m : var2blockid) : except format α :=
run a m mk_var_set

/- Mark `x` as a variable defined in the current basic block. -/
def var.define (x : var) : ssa_valid_m unit :=
modify $ λ s, s.insert x

def arg.define (a : arg) : ssa_valid_m unit :=
a.n.define

/- Check whether `x` has been already defined in the current basic block or not. -/
def var.defined (x : var) : ssa_valid_m unit :=
do s ← get,
   if s.contains x then pure ()
   else throw ("undefined '" ++ to_fmt x ++ "'")

/- Given, x := phi ys,
   check whether every ys is declared at the var2blockid mapping,
   and update the set of already defined variables in the basic block with `x`. -/
def phi.valid_ssa (p : phi) : ssa_valid_m unit :=
p.decorate_error $
do m ← read,
   p.ys.mfor $ λ y, unless (m.contains y) $ throw ("undefined '" ++ to_fmt y ++ "'"),
   p.x.define

def instr.valid_ssa (ins : instr) : ssa_valid_m unit :=
ins.decorate_error $
match ins with
| (instr.assign x _ y)           := x.define *> y.defined
| (instr.assign_lit x _ _)       := x.define
| (instr.assign_unop x _ _ y)    := x.define *> y.defined
| (instr.assign_binop x _ _ y z) := x.define *> y.defined *> z.defined
| (instr.unop _ x)               := x.defined
| (instr.call xs _ ys)           := xs.mfor var.define *> ys.mfor var.defined
| (instr.cnstr o _ _ _)          := o.define
| (instr.set o _ x)              := o.defined *> x.defined
| (instr.get x y _)              := x.define *> y.defined
| (instr.sset o _ x)             := o.defined *> x.defined
| (instr.sget x _ y _)           := x.define *> y.defined
| (instr.closure x _ ys)         := x.define *> ys.mfor var.defined
| (instr.apply x ys)             := x.define *> ys.mfor var.defined
| (instr.array a sz c)           := a.define *> sz.defined *> c.defined
| (instr.sarray x _ sz c)        := x.define *> sz.defined *> c.defined
| (instr.array_write a i v)      := a.defined *> i.defined *> v.defined

def terminator.valid_ssa (term : terminator) : ssa_valid_m unit :=
term.decorate_error $
match term with
| (terminator.ret ys)     := ys.mfor var.defined
| (terminator.case x _)   := x.defined
| (terminator.jmp _)      := pure ()

def phi.predecessors (p : phi) : ssa_valid_m blockid_set :=
p.ys.mfoldl (λ s y,
  do m ← read,
     match m.find y with
     | some bid := if s.contains bid
                   then throw ("multiple predecessors at '" ++ to_fmt p ++ "'")
                   else pure $ s.insert bid
     | none   := throw ("undefined '" ++ to_fmt y ++ "' at '" ++ to_fmt p ++ "'"))
  mk_blockid_set

def phis.check_predecessors (ps : list phi) : ssa_valid_m unit :=
do ps.mfoldl (λ (os : option blockid_set) (p : phi),
     p.decorate_error $
     do s' ← p.predecessors,
        match os with
        | (some s) := if s.seteq s' then pure os
                      else throw ("missing predecessor '" ++ to_fmt p.x ++ "' at '" ++ to_fmt p ++ "'")
        | none      := pure (some s'))
     none,
   pure ()

def block.valid_ssa_core (b : block) : ssa_valid_m unit :=
b.decorate_error $
do phis.check_predecessors b.phis,
   b.phis.mfor phi.valid_ssa,
   b.instrs.mfor instr.valid_ssa,
   b.term.valid_ssa

/-
We first check whether every variable `x` was declared only once
and store the blockid where `x` is defined (action: `decl.declare_vars`).
Then, we check whether every used variable in basic block has been
defined before being used.
-/
def decl.valid_ssa (d : decl) : except format var2blockid :=
d.decorate_error $
do m ← d.var2blockid,
   match d with
   | decl.defn {args:=args, ..} (b::bs) :=
       (args.mfor arg.define *> block.valid_ssa_core b).run m
    *> (bs.mfor block.valid_ssa_core).run m
    *> pure m
   | _                   := pure m

/- Check blockids -/
@[reducible] def blockid_check_m :=
except_t format (state blockid_set)

def blockid_check_m.run {α} (a : blockid_check_m α) : except format α :=
run a mk_blockid_set

def block.declare (b : block) : blockid_check_m unit :=
do s ← get,
   if s.contains b.id then throw $ "block label '" ++ to_fmt b.id ++ "' has been used more than once"
   else put (s.insert b.id)

def blockid.defined (bid : blockid) : blockid_check_m unit :=
do s ← get,
   if s.contains bid then pure ()
   else throw $ "unknown basic block '" ++ to_fmt bid ++ "'"

def terminator.check_blockids (term : terminator) : blockid_check_m unit :=
term.decorate_error $
match term with
| (terminator.ret ys)      := pure ()
| (terminator.case _ bids) := bids.mfor blockid.defined
| (terminator.jmp bid)     := bid.defined

def block.check_blockids (b : block) : blockid_check_m unit :=
b.term.check_blockids

def decl.check_blockids : decl → blockid_check_m unit
| (decl.defn h bs) := h.decorate_error $ bs.mfor block.declare *> bs.mfor block.check_blockids
| _                := pure ()

def check_blockids (d : decl) : except format blockid_set :=
(d.check_blockids *> get).run

end ir
end lean
