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

prelude
public import Lean.Runtime
public import Lean.Compiler.IR.CompilerM
public import Lean.Compiler.IR.LiveVars

public section

namespace Lean.IR.ExplicitRC
/-!
Insert explicit RC instructions. So, it assumes the input code does not contain `inc` nor `dec` instructions.
This transformation is applied before lower level optimizations
that introduce the instructions `release` and `set`
-/

structure VarInfo where
  isPossibleRef : Bool
  isDefiniteRef: Bool
  persistent : Bool
  inheritsBorrowFromParam : Bool
  deriving Inhabited

abbrev VarMap := Std.TreeMap VarId VarInfo (fun x y => compare x.idx y.idx)

structure Context where
  env            : Environment
  decls          : Array Decl
  varMap         : VarMap := {}
  jpLiveVarMap   : JPLiveVarMap := {} -- map: join point => live variables
  localCtx       : LocalContext := {} -- we use it to store the join point declarations

def getDecl (ctx : Context) (fid : FunId) : Decl :=
  findEnvDecl' ctx.env fid ctx.decls |>.get!

def getVarInfo (ctx : Context) (x : VarId) : VarInfo :=
  ctx.varMap.get! x

def getJPParams (ctx : Context) (j : JoinPointId) : Array Param :=
  ctx.localCtx.getJPParams j |>.get!

def getJPLiveVars (ctx : Context) (j : JoinPointId) : LiveVarSet :=
  ctx.jpLiveVarMap.get? j |>.getD {}

def mustConsume (ctx : Context) (x : VarId) : Bool :=
  let info := getVarInfo ctx x
  info.isPossibleRef && !info.inheritsBorrowFromParam

@[inline] def addInc (ctx : Context) (x : VarId) (b : FnBody) (n := 1) : FnBody :=
  let info := getVarInfo ctx x
  if n == 0 then b else .inc x n (!info.isDefiniteRef) info.persistent b

@[inline] def addDec (ctx : Context) (x : VarId) (b : FnBody) : FnBody :=
  let info := getVarInfo ctx x
  .dec x 1 (!info.isDefiniteRef) info.persistent b

private def updateRefUsingCtorInfo (ctx : Context) (x : VarId) (c : CtorInfo) : Context :=
  let m := ctx.varMap
  { ctx with
    varMap := match m.get? x with
    | some info =>
      let isPossibleRef := c.type.isPossibleRef
      let isDefiniteRef := c.type.isDefiniteRef
      m.insert x { info with isPossibleRef, isDefiniteRef }
    | none => m
  }

private def addDecForAlt (ctx : Context) (caseLiveVars altLiveVars : LiveVarSet) (b : FnBody) : FnBody :=
  caseLiveVars.foldl (init := b) fun b x =>
    if !altLiveVars.contains x && mustConsume ctx x then addDec ctx x b else b

/-- `isFirstOcc xs x i = true` if `xs[i]` is the first occurrence of `xs[i]` in `xs` -/
private def isFirstOcc (xs : Array Arg) (i : Nat) : Bool :=
  let x := xs[i]!
  i.all fun j _ => xs[j]! != x

/-- Return true if `x` also occurs in `ys` in a position that is not consumed.
   That is, it is also passed as a borrow reference. -/
private def isBorrowParamAux (x : VarId) (ys : Array Arg) (consumeParamPred : Nat → Bool) : Bool :=
  ys.size.any fun i _ =>
    let y := ys[i]
    match y with
    | .erased => false
    | .var y  => x == y && !consumeParamPred i

private def isBorrowParam (x : VarId) (ys : Array Arg) (ps : Array Param) : Bool :=
  isBorrowParamAux x ys fun i => ! ps[i]!.borrow

/--
Return `n`, the number of times `x` is consumed.
- `ys` is a sequence of instruction parameters where we search for `x`.
- `consumeParamPred i = true` if parameter `i` is consumed.
-/
private def getNumConsumptions (x : VarId) (ys : Array Arg) (consumeParamPred : Nat → Bool) : Nat :=
  ys.size.fold (init := 0) fun i _ n =>
    let y := ys[i]
    match y with
    | .erased => n
    | .var y  => if x == y && consumeParamPred i then n+1 else n

private def addIncBeforeAux (ctx : Context) (xs : Array Arg) (consumeParamPred : Nat → Bool) (b : FnBody) (liveVarsAfter : LiveVarSet) : FnBody :=
  xs.size.fold (init := b) fun i _ b =>
    let x := xs[i]
    match x with
    | .erased => b
    | .var x =>
      let info := getVarInfo ctx x
      if !info.isPossibleRef || !isFirstOcc xs i then b
      else
        let numConsumptions := getNumConsumptions x xs consumeParamPred
        let numIncs :=
          if info.inheritsBorrowFromParam ||
             liveVarsAfter.contains x ||          -- `x` is live after executing instruction
             isBorrowParamAux x xs consumeParamPred  -- `x` is used in a position that is passed as a borrow reference
          then numConsumptions
          else numConsumptions - 1
        addInc ctx x b numIncs

private def addIncBefore (ctx : Context) (xs : Array Arg) (ps : Array Param) (b : FnBody) (liveVarsAfter : LiveVarSet) : FnBody :=
  addIncBeforeAux ctx xs (fun i => ! ps[i]!.borrow) b liveVarsAfter

/-- See `addIncBeforeAux`/`addIncBefore` for the procedure that inserts `inc` operations before an application.  -/
private def addDecAfterFullApp (ctx : Context) (xs : Array Arg) (ps : Array Param) (b : FnBody) (bLiveVars : LiveVarSet) : FnBody :=
  xs.size.fold (init := b) fun i _ b =>
    match xs[i] with
    | .erased => b
    | .var x  =>
      /- We must add a `dec` if `x` must be consumed, it is alive after the application,
        and it has been borrowed by the application.
        Remark: `x` may occur multiple times in the application (e.g., `f x y x`).
        This is why we check whether it is the first occurrence. -/
      if mustConsume ctx x && isFirstOcc xs i && isBorrowParam x xs ps && !bLiveVars.contains x then
        addDec ctx x b
      else b

private def addIncBeforeConsumeAll (ctx : Context) (xs : Array Arg) (b : FnBody) (liveVarsAfter : LiveVarSet) : FnBody :=
  addIncBeforeAux ctx xs (fun _ => true) b liveVarsAfter

/-- Add `dec` instructions for parameters that are references, are not alive in `b`, and are not borrow.
   That is, we must make sure these parameters are consumed. -/
private def addDecForDeadParams (ctx : Context) (ps : Array Param) (b : FnBody) (bLiveVars : LiveVarSet) : FnBody × LiveVarSet :=
  ps.foldl (init := ⟨b, bLiveVars⟩) fun ⟨b, bLiveVars⟩ p =>
    let b :=
      if !p.borrow && p.ty.isPossibleRef && !bLiveVars.contains p.x then
        addDec ctx p.x b
      else b
    let bLiveVars := bLiveVars.erase p.x
    ⟨b, bLiveVars⟩

private def isPersistent : Expr → Bool
  | .fap _ xs => xs.isEmpty -- all global constants are persistent objects
  | _         => false

/-- Return true iff `v` at runtime is a scalar value stored in a tagged pointer.
   We do not need RC operations for this kind of value. -/
private def typeForScalarBoxedInTaggedPtr? (v : Expr) : Option IRType :=
  match v with
  | .ctor c _ =>
    some c.type
  | .lit (.num n) =>
    if n ≤ maxSmallNat then
      some .tagged
    else
      some .tobject
  | _ => none

private def updateVarInfo (ctx : Context) (x : VarId) (t : IRType) (v : Expr) : Context :=
  let inheritsBorrowFromParam :=
    match v with
    | .proj _ x => match ctx.varMap.get? x with
      | some info => info.inheritsBorrowFromParam
      | none => false
    | _ => false
  let type := typeForScalarBoxedInTaggedPtr? v |>.getD t
  let isPossibleRef := type.isPossibleRef
  let isDefiniteRef := type.isDefiniteRef
  { ctx with
    varMap := ctx.varMap.insert x {
        isPossibleRef
        isDefiniteRef
        persistent := isPersistent v,
        inheritsBorrowFromParam
    }
  }

private def addDecIfNeeded (ctx : Context) (x : VarId) (b : FnBody) (bLiveVars : LiveVarSet) : FnBody :=
  if mustConsume ctx x && !bLiveVars.contains x then addDec ctx x b else b

private def processVDecl (ctx : Context) (z : VarId) (t : IRType) (v : Expr) (b : FnBody) (bLiveVars : LiveVarSet) : FnBody × LiveVarSet :=
  let b := match v with
    | .ctor _ ys | .reuse _ _ _ ys | .pap _ ys =>
      addIncBeforeConsumeAll ctx ys (.vdecl z t v b) bLiveVars
    | .proj _ x =>
      let b := addDecIfNeeded ctx x b bLiveVars
      let b := if !(getVarInfo ctx x).inheritsBorrowFromParam then addInc ctx z b else b
      .vdecl z t v b
    | .uproj _ x | .sproj _ _ x | .unbox x =>
      .vdecl z t v (addDecIfNeeded ctx x b bLiveVars)
    | .fap f ys =>
      let ps := (getDecl ctx f).params
      let b  := addDecAfterFullApp ctx ys ps b bLiveVars
      let b  := .vdecl z t v b
      addIncBefore ctx ys ps b bLiveVars
    | .ap x ys =>
      let ysx := ys.push (.var x) -- TODO: avoid temporary array allocation
      addIncBeforeConsumeAll ctx ysx (.vdecl z t v b) bLiveVars
    | .lit _ | .box .. | .reset .. | .isShared _ =>
      .vdecl z t v b
  let liveVars := updateLiveVars v bLiveVars
  let liveVars := liveVars.erase z
  ⟨b, liveVars⟩

def updateVarInfoWithParams (ctx : Context) (ps : Array Param) : Context :=
  let m := ps.foldl (init := ctx.varMap) fun m p =>
    m.insert p.x {
      isPossibleRef := p.ty.isPossibleRef
      isDefiniteRef := p.ty.isDefiniteRef
      persistent := false
      inheritsBorrowFromParam := p.borrow }
  { ctx with varMap := m }

partial def visitFnBody (b : FnBody) (ctx : Context) : FnBody × LiveVarSet :=
  match b with
  | .vdecl x t v b =>
    let ctx := updateVarInfo ctx x t v
    let ⟨b, bLiveVars⟩ := visitFnBody b ctx
    processVDecl ctx x t v b bLiveVars
  | .jdecl j xs v b =>
    let ctxAtV := updateVarInfoWithParams ctx xs
    let ⟨v, vLiveVars⟩ := visitFnBody v ctxAtV
    let ⟨v, vLiveVars⟩ := addDecForDeadParams ctxAtV xs v vLiveVars
    let ctx := { ctx with
      localCtx     := ctx.localCtx.addJP j xs v
      jpLiveVarMap := ctx.jpLiveVarMap.insert j vLiveVars
    }
    let ⟨b, bLiveVars⟩ := visitFnBody b ctx
    ⟨.jdecl j xs v b, bLiveVars⟩
  | .uset x i y b =>
    let ⟨b, s⟩ := visitFnBody b ctx
    -- We don't need to insert `y` since we only need to track live variables that are references at runtime
    let s      := s.insert x
    ⟨.uset x i y b, s⟩
  | .sset x i o y t b =>
    let ⟨b, s⟩ := visitFnBody b ctx
    -- We don't need to insert `y` since we only need to track live variables that are references at runtime
    let s      := s.insert x
    ⟨.sset x i o y t b, s⟩
  | .case tid x xType alts =>
    let alts : Array (Alt × LiveVarSet) := alts.map fun alt => match alt with
      | .ctor c b =>
        let ctx := updateRefUsingCtorInfo ctx x c
        let ⟨b, altLiveVars⟩ := visitFnBody b ctx
        ⟨.ctor c b, altLiveVars⟩
      | .default b =>
        let ⟨b, altLiveVars⟩ := visitFnBody b ctx
        ⟨.default b, altLiveVars⟩
    let caseLiveVars : LiveVarSet := alts.foldl (init := {}) fun liveVars ⟨_, altLiveVars⟩ =>
      liveVars.merge altLiveVars
    let caseLiveVars := caseLiveVars.insert x
    let alts := alts.map fun ⟨alt, altLiveVars⟩ => match alt with
      | .ctor c b =>
        let ctx := updateRefUsingCtorInfo ctx x c
        let b := addDecForAlt ctx caseLiveVars altLiveVars b
        .ctor c b
      | .default b =>
        let b := addDecForAlt ctx caseLiveVars altLiveVars b
        .default b
    ⟨.case tid x xType alts, caseLiveVars⟩
  | .ret x =>
    match x with
    | .var x =>
      let info := getVarInfo ctx x
      let b :=
        if info.isPossibleRef && info.inheritsBorrowFromParam then
          addInc ctx x b
        else b
      ⟨b, mkLiveVarSet x⟩
    | .erased => ⟨b, {}⟩
  | .jmp j xs =>
    let jLiveVars := getJPLiveVars ctx j
    let ps        := getJPParams ctx j
    let b         := addIncBefore ctx xs ps b jLiveVars
    let bLiveVars := collectLiveVars b ctx.jpLiveVarMap
    ⟨b, bLiveVars⟩
  | .unreachable => ⟨.unreachable, {}⟩
  | .set .. | .setTag .. | .inc .. | .dec .. | .del .. => unreachable!

partial def visitDecl (env : Environment) (decls : Array Decl) (d : Decl) : Decl :=
  match d with
  | .fdecl (xs := xs) (body := b) .. =>
    let ctx := updateVarInfoWithParams { env, decls } xs
    let ⟨b, bLiveVars⟩ := visitFnBody b ctx
    let ⟨b, _⟩ := addDecForDeadParams ctx xs b bLiveVars
    d.updateBody! b
  | other => other

end ExplicitRC

def explicitRC (decls : Array Decl) : CompilerM (Array Decl) := do
  let env ← getEnv
  return decls.map (ExplicitRC.visitDecl env decls)

builtin_initialize registerTraceClass `compiler.ir.rc (inherited := true)

end Lean.IR
