/-
Copyright (c) 2022 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
import Lean.ProjFns
import Lean.ToExpr
import Lean.Util.Recognizers
import Lean.Meta.Match.MatcherInfo
import Lean.Meta.Transform
import Lean.Compiler.InlineAttrs
import Lean.Compiler.InferType
import Lean.Compiler.Util

namespace Lean.Compiler
/-!
# Lean Compiler Normal Form (LCNF)

It is based on the [A-normal form](https://en.wikipedia.org/wiki/A-normal_form),
and the approach described in the paper
[Compiling  without continuations](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/11/compiling-without-continuations.pdf).

Remark, in LCNF, the terminal expression in a `let`-declaration block is never a lambda.
The idea is to make sure we can easily compute the "true arity" of a join point.
For example, consider the following two join points
```
let _jp.1 := fun x y =>
  x + y
let _jp.2 := fun x =>
  let f := fun y => x + y
  f
```
`_jp.1` is a join point of arity 2, and `_jp.2` is a join point of arity 1.

-/

namespace ToLCNF

structure Context where
  root : Bool

structure State where
  /-- Local context containing the original Lean types (not LCNF ones). -/
  lctx : LocalContext := {}
  /-- Local context containing Lean LCNF types. -/
  lctx' : LocalContext := {}
  letFVars : Array Expr := #[]
  /-- Next auxiliary variable suffix -/
  nextIdx : Nat := 1
  /-- Cache from Lean regular expression to LCNF expression. -/
  cache : Std.PHashMap (Expr × Bool) Expr := {}

abbrev M := ReaderT Context $ StateRefT State CoreM

instance : MonadInferType M where
  inferType e := do InferType.inferType e { lctx := (← get).lctx' }

@[inline] def liftMetaM (x : MetaM α) : M α := do
  x.run' { lctx := (← get).lctx }

@[inline] def withRoot (flag : Bool) (x : M α) : M α :=
  withReader (fun _ => { root := flag }) x

def withNewRootScope (x : M α) : M α := do
  let saved ← get
  modify fun s => { s with letFVars := #[] }
  try
    withRoot true x
  finally
    let saved := { saved with nextIdx := (← get).nextIdx }
    set saved

/-- Create a new local declaration using a Lean regular type. -/
def mkLocalDecl (binderName : Name) (type : Expr) (bi := BinderInfo.default) : M Expr := do
  let fvarId ← mkFreshFVarId
  let type' ← liftMetaM <| toLCNFType type
  modify fun s => { s with
    lctx  := s.lctx.mkLocalDecl fvarId binderName type bi
    lctx' := s.lctx'.mkLocalDecl fvarId binderName type' bi
  }
  return .fvar fvarId

def mkFreshBinderName (binderName := `_x): M Name := do
  let declName := .num binderName (← get).nextIdx
  modify fun s => { s with nextIdx := s.nextIdx + 1 }
  return declName

def mkLetDecl (binderName : Name) (type : Expr) (value : Expr) (type' : Expr) (value' : Expr) : M Expr := do
  let fvarId ← mkFreshFVarId
  let binderName ← if binderName.eraseMacroScopes.isInternal then mkFreshBinderName binderName.eraseMacroScopes else pure binderName
  let x := .fvar fvarId
  modify fun s => { s with
    lctx     := s.lctx.mkLetDecl fvarId binderName type value false
    lctx'    := s.lctx'.mkLetDecl fvarId binderName type' value' true
    letFVars := s.letFVars.push x
  }
  return x

/-- Create an auxiliary `let`-declaration for the given LCNF expression. -/
def mkAuxLetDecl (e : Expr) : M Expr := do
  if (← read).root then
    return e
  else
    let fvarId ← mkFreshFVarId
    let binderName ← mkFreshBinderName
    let type ← inferType e
    let x := .fvar fvarId
    modify fun s => { s with
      lctx'    := s.lctx'.mkLetDecl fvarId binderName type e true
      letFVars := s.letFVars.push x
    }
    return x

def visitLambda (e : Expr) : M (Array Expr × Expr) :=
  go e #[]
where
  go (e : Expr) (fvars : Array Expr) := do
    if let .lam binderName type body binderInfo := e then
      let type := type.instantiateRev fvars
      let fvar ← mkLocalDecl binderName type binderInfo
      go body (fvars.push fvar)
    else
      return (fvars, e.instantiateRev fvars)

def visitBoundedLambda (e : Expr) (n : Nat) : M (Array Expr × Expr) :=
  go e n #[]
where
  go (e : Expr) (n : Nat) (fvars : Array Expr) := do
    if n == 0 then
      return (fvars, e.instantiateRev fvars)
    else if let .lam binderName type body binderInfo := e then
      let type := type.instantiateRev fvars
      let fvar ← mkLocalDecl binderName type binderInfo
      go body (n-1) (fvars.push fvar)
    else
      return (fvars, e.instantiateRev fvars)

def mkLetUsingScope (e : Expr) : M Expr := do
  let e ← if e.isLambda then
    /-
    In LCNF, terminal expression in a `let`-block must not be a lambda.
    -/
    withRoot false <| mkAuxLetDecl e
  else
    pure e
  return (← get).lctx'.mkLambda (← get).letFVars e

def mkLambda (xs : Array Expr) (e : Expr) : M Expr :=
  return (← get).lctx'.mkLambda xs e

/--
Eta-expand with `n` lambdas.
-/
def etaExpandN (e : Expr) (n : Nat) : M Expr := do
  if n == 0 then
    return e
  else liftMetaM do
    Meta.forallBoundedTelescope (← Meta.inferType e) n fun xs _ =>
      Meta.mkLambdaFVars xs (mkAppN e xs)

/--
Put the given expression in `LCNF`.

- Nested proofs are replaced with `lcProof`-applications.
- Eta-expand applications of declarations that satisfy `shouldEtaExpand`.
- Put computationally relevant expressions in A-normal form.
-/
partial def toLCNF (e : Expr) : CoreM Expr := do
  let (e, s) ← visit e |>.run { root := true } |>.run {}
  return s.lctx'.mkLambda s.letFVars e
where
  visitCore (e : Expr) : M Expr := withIncRecDepth do
    let key := (e, (← read).root)
    if let some e := (← get).cache.find? key then
      return e
    let r ← match e with
      | .app ..     => visitApp e
      | .const ..   => visitApp e
      | .proj s i e => visitProj s i e
      | .mdata d e  => visitMData d e
      | .lam ..     => visitLambda e
      | .letE ..    => visitLet e #[]
      | .lit ..     => mkAuxLetDecl e
      | .forallE .. => unreachable!
      | .mvar .. => throwError "unexpected occurrence of metavariable in code generator{indentExpr e}"
      | .bvar .. => unreachable!
      | _           => pure e
    modify fun s => { s with cache := s.cache.insert key r }
    return r

  visit (e : Expr) : M Expr := withIncRecDepth do
    if isLCProof e then
      return mkConst ``lcErased
    let type ← liftMetaM <| Meta.inferType e
    if (← liftMetaM <| Meta.isProp type) then
      /- We erase proofs. -/
      return mkConst ``lcErased
    if (← liftMetaM <| Meta.isTypeFormerType type) then
      /-
      We erase type formers unless they occur as application arguments.
      Recall that we usually do not generate code for functions that return type,
      by this branch can be reachable if we cannot establish whether the function produces a type or not.

      See `visitAppArg`
      -/
      return mkConst ``lcErased
    visitCore e

  visitChild (e : Expr) : M Expr :=
    withRoot false <| visit e

  visitAppArg (e : Expr) : M Expr := do
    if isLCProof e then
      return mkConst ``lcErased
    let type ← liftMetaM <| Meta.inferType e
    if (← liftMetaM <| Meta.isProp type) then
      /- We erase proofs. -/
      return mkConst ``lcErased
    if (← liftMetaM <| Meta.isTypeFormerType type) then
      /- Types and Type formers are not put into A-normal form -/
      return (← liftMetaM <| toLCNFType e)
    else
      withRoot false <| visitCore e

  /-- Visit args, and return `f args` -/
  visitAppDefault (f : Expr) (args : Array Expr) : M Expr := do
    if f.isConstOf ``lcErased then
      return f
    else
      let args ← args.mapM visitAppArg
      mkAuxLetDecl <| mkAppN f args

  /-- Eta expand if under applied, otherwise apply k -/
  etaIfUnderApplied (e : Expr) (arity : Nat) (k : M Expr) : M Expr := do
    let numArgs := e.getAppNumArgs
    if numArgs < arity then
      visit (← etaExpandN e (arity - numArgs))
    else
      k

  /--
  If `args.size == arity`, then just return `app`.
  Otherwise return
  ```
  let k := app
  k args[arity:]
  ```
  -/
  mkOverApplication (app : Expr) (args : Array Expr) (arity : Nat) : M Expr := do
    if args.size == arity then
      mkAuxLetDecl app
    else
    let k ← withRoot false <| mkAuxLetDecl app
    let mut args := args
    for i in [arity : args.size] do
      args ← args.modifyM i visitAppArg
    mkAuxLetDecl (mkAppN k args[arity:])

  /--
  Visit a `matcher`/`casesOn` alternative.
  -/
  visitAlt (e : Expr) (numParams : Nat) : M (Expr × Expr) := do
    withNewRootScope do
      let mut (as, e) ← visitBoundedLambda e numParams
      if as.size < numParams then
        e ← etaExpandN e (numParams - as.size)
        let (as', e') ← ToLCNF.visitLambda e
        as := as ++ as'
        e := e'
      e ← mkLetUsingScope (← visit e)
      let eType ← inferType e
      return (eType, (← mkLambda as e))

  visitCases (casesInfo : CasesInfo) (e : Expr) : M Expr :=
    etaIfUnderApplied e casesInfo.arity do
      let mut args := e.getAppArgs
      let mut resultType ← liftMetaM do toLCNFType (← Meta.inferType (mkAppN e.getAppFn args[:casesInfo.arity]))
      let mut discrTypes := #[]
      for i in [:casesInfo.numParams] do
        -- `cases` and `match` parameters are irrelevant at LCNF
        args := args.modify i fun _ => mkConst ``lcErased
      for i in casesInfo.discrsRange do
        let discrType ← inferType args[i]!
        args ← args.modifyM i visitAppArg
        discrTypes := discrTypes.push discrType
      for i in casesInfo.altsRange, numParams in casesInfo.altNumParams do
        let (altType, alt) ← visitAlt args[i]! numParams
        unless compatibleTypes altType resultType do
          resultType := anyTypeExpr
        args := args.set! i alt
      let motive ← discrTypes.foldrM (init := resultType) fun discrType resultType =>
        return .lam (← mkFreshUserName `x) discrType resultType .default
      args := args.set! casesInfo.motivePos motive
      let result := mkAppN e.getAppFn args[:casesInfo.arity]
      if args.size == casesInfo.arity then
        mkAuxLetDecl result
      else
        mkOverApplication result args casesInfo.arity

  visitCtor (arity : Nat) (e : Expr) : M Expr :=
    etaIfUnderApplied e arity do
      visitAppDefault e.getAppFn e.getAppArgs

  visitQuotLift (e : Expr) : M Expr := do
    let arity := 6
    etaIfUnderApplied e arity do
      let mut args := e.getAppArgs
      let α := args[0]!
      let r := args[1]!
      let f ← visitAppArg args[3]!
      let q ← visitAppArg args[5]!
      let .const _ [u, _] := e.getAppFn | unreachable!
      let invq ← withRoot false <| mkAuxLetDecl (mkApp3 (.const ``Quot.lcInv [u]) α r q)
      let r := mkApp f invq
      mkOverApplication r args arity

  visitEqRec (e : Expr) : M Expr :=
    let arity := 6
    etaIfUnderApplied e arity do
      let args := e.getAppArgs
      let f := e.getAppFn
      let recType ← liftMetaM do toLCNFType (← Meta.inferType (mkAppN f args[:arity]))
      let minor := if e.isAppOf ``Eq.rec || e.isAppOf ``Eq.ndrec then args[3]! else args[5]!
      let minor ← visit minor
      let minorType ← inferType minor
      let cast ← if compatibleTypes minorType recType then
        -- Recall that many types become compatible after LCNF conversion
        -- Example: `Fin 10` and `Fin n`
        pure minor
      else
        mkLcCast (← withRoot false <| mkAuxLetDecl minor) recType
      mkOverApplication cast args arity

  visitFalseRec (e : Expr) : M Expr :=
    let arity := 2
    etaIfUnderApplied e arity do
      let type ← liftMetaM do toLCNFType (← Meta.inferType e)
      mkAuxLetDecl (← mkLcUnreachable type)

  visitAndRec (e : Expr) : M Expr :=
    let arity := 5
    etaIfUnderApplied e arity do
      let args := e.getAppArgs
      let ha := mkLcProof args[0]! -- We should not use `lcErased` here since we use it to create a pre-LCNF Expr.
      let hb := mkLcProof args[1]!
      let minor := if e.isAppOf ``And.rec then args[3]! else args[4]!
      let minor := minor.beta #[ha, hb]
      visit (mkAppN minor args[arity:])

  visitNoConfusion (e : Expr) : M Expr := do
    let .const declName _ := e.getAppFn | unreachable!
    let typeName := declName.getPrefix
    let .inductInfo inductVal ← getConstInfo typeName | unreachable!
    let arity := inductVal.numParams + inductVal.numIndices + 1 /- motive -/ + 2 /- lhs/rhs-/ + 1 /- equality -/
    etaIfUnderApplied e arity do
      let args := e.getAppArgs
      let lhs ← liftMetaM do Meta.whnf args[inductVal.numParams + inductVal.numIndices + 1]!
      let rhs ← liftMetaM do Meta.whnf args[inductVal.numParams + inductVal.numIndices + 2]!
      let lhs := lhs.toCtorIfLit
      let rhs := rhs.toCtorIfLit
      match lhs.isConstructorApp? (← getEnv), rhs.isConstructorApp? (← getEnv) with
      | some lhsCtorVal, some rhsCtorVal =>
        if lhsCtorVal.name == rhsCtorVal.name then
          etaIfUnderApplied e (arity+1) do
            let major := args[arity]!
            let major ← expandNoConfusionMajor major lhsCtorVal.numFields
            let major := mkAppN major args[arity+1:]
            visit major
        else
          mkAuxLetDecl (← mkLcUnreachable (← inferType e))
      | _, _ =>
        throwError "code generator failed, unsupported occurrence of `{declName}`"

  expandNoConfusionMajor (major : Expr) (numFields : Nat) : M Expr := do
    match numFields with
    | 0 => return major
    | n+1 =>
      if let .lam _ d b _ := major then
        let proof := mkLcProof d
        expandNoConfusionMajor (b.instantiate1 proof) n
      else
        expandNoConfusionMajor (← etaExpandN major (n+1)) (n+1)

  visitProjFn (projInfo : ProjectionFunctionInfo) (e : Expr) : M Expr := do
    let typeName := projInfo.ctorName.getPrefix
    if isRuntimeBultinType typeName then
      let numArgs := e.getAppNumArgs
      let arity := projInfo.numParams + 1
      if numArgs < arity then
        visit (← etaExpandN e (arity - numArgs))
      else
        visitAppDefault e.getAppFn e.getAppArgs
    else
      let .const declName us := e.getAppFn | unreachable!
      let info ← getConstInfo declName
      let f ← Core.instantiateValueLevelParams info us
      visit (f.beta e.getAppArgs)

  visitApp (e : Expr) : M Expr := do
    if let .const declName _ := e.getAppFn then
      if declName == ``Quot.lift then
        visitQuotLift e
      else if declName == ``Quot.mk then
        visitCtor 3 e
      else if declName == ``Eq.casesOn || declName == ``Eq.rec || declName == ``Eq.ndrec then
        visitEqRec e
      else if declName == ``And.rec || declName == ``And.casesOn then
        visitAndRec e
      else if declName == ``False.rec || declName == ``Empty.rec || declName == ``False.casesOn || declName == ``Empty.casesOn then
        visitFalseRec e
      else if let some casesInfo ← getCasesInfo? declName then
        visitCases casesInfo e
      else if let some arity ← getCtorArity? declName then
        visitCtor arity e
      else if isNoConfusion (← getEnv) declName then
        visitNoConfusion e
      else if let some projInfo ← getProjectionFnInfo? declName then
        visitProjFn projInfo e
      else
        e.withApp visitAppDefault
    else
      e.withApp fun f args => do visitAppDefault (← visitChild f) args

  visitLambda (e : Expr) : M Expr := do
    let r ← withNewRootScope do
      let (as, e) ← ToLCNF.visitLambda e
      let e ← mkLetUsingScope (← visit e)
      mkLambda as e
    mkAuxLetDecl r

  visitMData (mdata : MData) (e : Expr) : M Expr := do
    if isCompilerRelevantMData mdata then
      mkAuxLetDecl <| .mdata mdata (← visitChild e)
    else
      visit e

  visitProj (s : Name) (i : Nat) (e : Expr) : M Expr := do
    trace[Meta.debug] "visitProj: {s}, {i}, {e}"
    mkAuxLetDecl <| .proj s i (← visitChild e)

  visitLet (e : Expr) (xs : Array Expr) : M Expr := do
    match e with
    | .letE binderName type value body _ =>
      let type := type.instantiateRev xs
      let value := value.instantiateRev xs
      if (← liftMetaM <| Meta.isProp type <||> Meta.isTypeFormerType type) then
        visitLet body (xs.push value)
      else
        let type' ← liftMetaM <| toLCNFType type
        let value' ← withRoot true <| visit value
        let x ← mkLetDecl binderName type value type' value'
        visitLet body (xs.push x)
    | _ =>
      let e := e.instantiateRev xs
      visit e

end ToLCNF

export ToLCNF (toLCNF)

end Lean.Compiler
