/-
Copyright (c) 2019 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import init.control.state
import init.control.reader
import init.lean.compiler.ir.compilerm
import init.lean.compiler.ir.normids
import init.lean.compiler.ir.freevars

namespace Lean
namespace IR
namespace ExpandResetReuse
/- Mapping from variable to projections -/
abbrev ProjMap  := HashMap VarId Expr
namespace CollectProjMap
abbrev Collector := ProjMap → ProjMap
@[inline] def collectVDecl (x : VarId) (v : Expr) : Collector :=
fun m => match v with
 | Expr.proj _ _    => m.insert x v
 | Expr.sproj _ _ _ => m.insert x v
 | Expr.uproj _ _   => m.insert x v
 | _                => m

partial def collectFnBody : FnBody → Collector
| (FnBody.vdecl x _ v b)  := collectVDecl x v ∘ collectFnBody b
| (FnBody.jdecl _ _ v b)  := collectFnBody v ∘ collectFnBody b
| (FnBody.case _ _ alts)  := fun s => alts.foldl (fun s alt => collectFnBody alt.body s) s
| e                       := if e.isTerminal then id else collectFnBody e.body
end CollectProjMap

/- Create a mapping from variables to projections.
   This function assumes variable ids have been normalized -/
def mkProjMap (d : Decl) : ProjMap :=
match d with
| Decl.fdecl _ _ _ b => CollectProjMap.collectFnBody b {}
| _ => {}

structure Context :=
(projMap : ProjMap)

/- Return true iff `x` is consumed in all branches of the current block.
   Here consumption means the block contains a `dec x` or `reuse x ...`. -/
partial def consumed (x : VarId) : FnBody → Bool
| (FnBody.vdecl _ _ v b) :=
  match v with
  | Expr.reuse y _ _ _ => x == y || consumed b
  | _                  => consumed b
| (FnBody.dec y _ _ b)   := x == y || consumed b
| (FnBody.case _ _ alts) := alts.all $ fun alt => consumed alt.body
| e := !e.isTerminal && consumed e.body

abbrev Mask := Array (Option VarId)

/- Auxiliary function for eraseProjIncFor -/
partial def eraseProjIncForAux (y : VarId) : Array FnBody → Mask → Array FnBody → Array FnBody × Mask
| bs mask keep :=
  let done (_ : Unit)        := (bs ++ keep.reverse, mask);
  let keepInstr (b : FnBody) := eraseProjIncForAux bs.pop mask (keep.push b);
  if bs.size < 2 then done ()
  else
    let b := bs.back;
    match b with
    | (FnBody.vdecl _ _ (Expr.sproj _ _ _) _) => keepInstr b
    | (FnBody.vdecl _ _ (Expr.uproj _ _) _)   => keepInstr b
    | (FnBody.inc z n c _) =>
      if n == 0 then done () else
      let b' := bs.get (bs.size - 2);
      match b' with
      | (FnBody.vdecl w _ (Expr.proj i x) _) =>
        if w == z && y == x then
          /- Found
             ```
             let z := proj[i] y;
             inc z n c
             ```
             We keep `proj`, and `inc` when `n > 1`
          -/
          let bs   := bs.pop.pop;
          let mask := mask.set i (some z);
          let keep := keep.push b';
          let keep := if n == 1 then keep else keep.push (FnBody.inc z (n-1) c FnBody.nil);
          eraseProjIncForAux bs mask keep
        else done ()
      | other => done ()
    | other => done ()

/- Try to erase `inc` instructions on projections of `y` occurring in the tail of `bs`.
   Return the updated `bs` and a bit mask specifying which `inc`s have been removed. -/
def eraseProjIncFor (n : Nat) (y : VarId) (bs : Array FnBody) : Array FnBody × Mask :=
eraseProjIncForAux y bs (mkArray n none) Array.empty

/- Replace `reuse x ctor ...` with `ctor ...`, and remoce `dec x` -/
partial def reuseToCtor (x : VarId) : FnBody → FnBody
| (FnBody.dec y n c b) :=
  if x == y then b -- n must be 1 since `x := reset ...`
  else FnBody.dec y n c (reuseToCtor b)
| (FnBody.vdecl z t v b) :=
  match v with
  | Expr.reuse y c u xs =>
    if x == y then FnBody.vdecl z t (Expr.ctor c xs) b
    else FnBody.vdecl z t v (reuseToCtor b)
  | _ =>
    FnBody.vdecl z t v (reuseToCtor b)
| (FnBody.case tid y alts) :=
  let alts := alts.map $ fun alt => alt.modifyBody reuseToCtor;
  FnBody.case tid y alts
| e :=
  if e.isTerminal then e
  else
    let (instr, b) := e.split;
    let b := reuseToCtor b;
    instr.setBody b

/-
replace
```
x := reset y; b
```
with
```
inc z_1; ...; inc z_i; dec y; b'
```
where `z_i`'s are the variables in `mask`,
and `b'` is `b` where we removed `dec x` and replaced `reuse x ctor_i ...` with `ctor_i ...`.
-/
def mkSlowPath (x y : VarId) (mask : Mask) (b : FnBody) : FnBody :=
let b := reuseToCtor x b;
let b := FnBody.dec y 1 true b;
mask.foldl
  (fun b m => match m with
    | some z => FnBody.inc z 1 true b
    | none   => b)
  b

abbrev M := ReaderT Context (State Nat)
def mkFresh : M VarId :=
do idx ← get; modify (fun n => n + 1); pure { idx := idx }

def releaseUnreadFields (y : VarId) (mask : Mask) (b : FnBody) : M FnBody :=
mask.size.mfold
  (fun i b =>
    match mask.get i with
    | some _ => pure b -- code took ownership of this field
    | none   => do
      fld ← mkFresh;
      pure (FnBody.vdecl fld IRType.object (Expr.proj i y) (FnBody.dec fld 1 true b)))
  b

def setFields (y : VarId) (zs : Array Arg) (b : FnBody) : FnBody :=
zs.size.fold
  (fun i b => FnBody.set y i (zs.get i) b)
  b

/- Given `set x[i] := y`, return true iff `y := proj[i] x` -/
def isSelfSet (ctx : Context) (x : VarId) (i : Nat) (y : Arg) : Bool :=
match y with
| Arg.var y =>
  match ctx.projMap.find y with
  | some (Expr.proj j w) => j == i && w == x
  | _ => false
| _ => false

/- Given `uset x[i] := y`, return true iff `y := uproj[i] x` -/
def isSelfUSet (ctx : Context) (x : VarId) (i : Nat) (y : VarId) : Bool :=
match ctx.projMap.find y with
| some (Expr.uproj j w) => j == i && w == x
| _                     => false

/- Given `sset x[n, i] := y`, return true iff `y := sproj[n, i] x` -/
def isSelfSSet (ctx : Context) (x : VarId) (n : Nat) (i : Nat) (y : VarId) : Bool :=
match ctx.projMap.find y with
| some (Expr.sproj m j w) => n == m && j == i && w == x
| _                       => false

/- Remove unnecessary `set/uset/sset` operations -/
partial def removeSelfSet (ctx : Context) : FnBody → FnBody
| (FnBody.set x i y b) :=
  if isSelfSet ctx x i y then removeSelfSet b
  else FnBody.set x i y (removeSelfSet b)
| (FnBody.uset x i y b) :=
  if isSelfUSet ctx x i y then removeSelfSet b
  else FnBody.uset x i y (removeSelfSet b)
| (FnBody.sset x n i y t b) :=
  if isSelfSSet ctx x n i y then removeSelfSet b
  else FnBody.sset x n i y t (removeSelfSet b)
| (FnBody.case tid y alts) :=
  let alts := alts.map $ fun alt => alt.modifyBody removeSelfSet;
  FnBody.case tid y alts
| e :=
  if e.isTerminal then e
  else
    let (instr, b) := e.split;
    let b := removeSelfSet b;
    instr.setBody b

partial def reuseToSet (ctx : Context) (x y : VarId) : FnBody → FnBody
| (FnBody.dec z n c b) :=
  if x == z then FnBody.del y b
  else FnBody.dec z n c (reuseToSet b)
| (FnBody.vdecl z t v b) :=
  match v with
  | Expr.reuse w c u zs =>
    if x == w then
      let b := setFields y zs (b.replaceVar z y);
      let b := if u then FnBody.setTag y c.cidx b else b;
      removeSelfSet ctx b
    else FnBody.vdecl z t v (reuseToSet b)
  | _ => FnBody.vdecl z t v (reuseToSet b)
| (FnBody.case tid y alts) :=
  let alts := alts.map $ fun alt => alt.modifyBody reuseToSet;
  FnBody.case tid y alts
| e :=
  if e.isTerminal then e
  else
    let (instr, b) := e.split;
    let b := reuseToSet b;
    instr.setBody b

/-
replace
```
x := reset y; b
```
with
```
let f_i_1 := proj[i_1] y;
...
let f_i_k := proj[i_k] y;
b'
```
where `i_j`s are the field indexes
that the code did not touch immediately before the reset.
That is `mask[j] == none`.
`b'` is `b` where `y` `dec x` is replaced with `del y`,
and `z := reuse x ctor_i ws; F` is replaced with
`set x i ws[i]` operations, and we replace `z` with `x` in `F`
-/
def mkFastPath (x y : VarId) (mask : Mask) (b : FnBody) : M FnBody :=
do
ctx ← read;
let b := reuseToSet ctx x y b;
releaseUnreadFields y mask b

-- Expand `bs; x := reset[n] y; b`
partial def expand (mainFn : FnBody → Array FnBody → M FnBody)
            (bs : Array FnBody) (x : VarId) (n : Nat) (y : VarId) (b : FnBody) : M FnBody :=
do
let bOld := FnBody.vdecl x IRType.object (Expr.reset n y) b;
let (bs, mask) := eraseProjIncFor n y bs;
let bSlow      := mkSlowPath x y mask b;
bFast ← mkFastPath x y mask b;
/- We only optimize recursively the fast. -/
bFast ← mainFn bFast Array.empty;
c ← mkFresh;
let b := FnBody.vdecl c IRType.uint8 (Expr.isShared y) (mkIf c bSlow bFast);
pure $ reshape bs b

partial def searchAndExpand : FnBody → Array FnBody → M FnBody
| d@(FnBody.vdecl x t (Expr.reset n y) b) bs :=
  if consumed x b then do
    expand searchAndExpand bs x n y b
  else
    searchAndExpand b (push bs d)
| (FnBody.jdecl j xs v b) bs := do
  v ← searchAndExpand v Array.empty;
  searchAndExpand b (push bs (FnBody.jdecl j xs v FnBody.nil))
| (FnBody.case tid x alts) bs := do
  alts ← alts.mmap $ fun alt => alt.mmodifyBody $ fun b => searchAndExpand b Array.empty;
  pure $ reshape bs (FnBody.case tid x alts)
| b bs :=
  if b.isTerminal then pure $ reshape bs b
  else searchAndExpand b.body (push bs b)

def main (d : Decl) : Decl :=
let d := d.normalizeIds;
match d with
| (Decl.fdecl f xs t b) =>
  let m := mkProjMap d;
  let nextIdx := d.maxIndex + 1;
  let b := (searchAndExpand b Array.empty { projMap := m }).run' nextIdx;
  Decl.fdecl f xs t b
| d => d

end ExpandResetReuse

/-- (Try to) expand `reset` and `reuse` instructions. -/
def Decl.expandResetReuse (d : Decl) : Decl :=
ExpandResetReuse.main d

end IR
end Lean
