/-
Copyright (c) 2022 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.Compiler.LCNF.Basic

public section

namespace Lean.Compiler.LCNF

namespace ToExpr

abbrev LevelMap := FVarIdMap Nat

private def _root_.Lean.FVarId.toExpr (offset : Nat) (m : LevelMap) (fvarId : FVarId) : Expr :=
  match m.get? fvarId with
  | some level => .bvar (offset - level - 1)
  | none => .fvar fvarId

private def _root_.Lean.Expr.abstract' (offset : Nat) (m : LevelMap) (e : Expr) : Expr :=
  go offset e
where
  go (o : Nat) (e : Expr) : Expr :=
    match e with
    | .fvar fvarId => fvarId.toExpr o m
    | .lit .. | .const .. | .sort .. | .mvar .. | .bvar .. => e
    | .app f a => .app (go o f) (go o a)
    | .mdata k b => .mdata k (go o b)
    | .proj s i b => .proj s i (go o b)
    | .forallE n d b bi => .forallE n (go o d) (go (o+1) b) bi
    | .lam n d b bi => .lam n (go o d) (go (o+1) b) bi
    | .letE n t v b nd => .letE n (go o t) (go o v) (go (o+1) b) nd

abbrev ToExprM := ReaderT Nat $ StateM LevelMap

@[inline] def mkLambdaM (params : Array Param) (e : Expr) : ToExprM Expr :=
  return go (← read) (← get) params.size e
where
  go (offset : Nat) (m : LevelMap) (i : Nat) (e : Expr) : Expr :=
   if i > 0 then
     let param  := params[i-1]!
     let domain := param.type.abstract' (offset - 1) m
     go (offset - 1) m (i - 1) (.lam param.binderName domain e .default)
   else
     e

private abbrev _root_.Lean.FVarId.toExprM (fvarId : FVarId) : ToExprM Expr :=
  return fvarId.toExpr (← read) (← get)

@[inline] def abstractM (e : Expr) : ToExprM Expr :=
  return e.abstract' (← read) (← get)

@[inline] def withFVar (fvarId : FVarId) (k : ToExprM α) : ToExprM α := do
  let offset ← read
  modify fun s => s.insert fvarId offset
  withReader (·+1) k

@[inline] partial def withParams (params : Array Param) (k : ToExprM α) : ToExprM α :=
  go 0
where
  @[specialize] go (i : Nat) : ToExprM α := do
    if h : i < params.size then
      withFVar params[i].fvarId (go (i+1))
    else
      k

@[inline] def run (x : ToExprM α) (offset : Nat := 0) (levelMap : LevelMap := {}) : α :=
  x |>.run offset |>.run' levelMap

@[inline] def run' (x : ToExprM α) (xs : Array FVarId) : α :=
  let map := xs.foldl (init := {}) fun map x => map.insert x map.size
  run x xs.size map

end ToExpr

open ToExpr

private def Arg.toExprM (arg : Arg) : ToExprM Expr :=
  return arg.toExpr.abstract' (← read) (← get)

mutual
partial def FunDecl.toExprM (decl : FunDecl) : ToExprM Expr :=
  withParams decl.params do mkLambdaM decl.params (← decl.value.toExprM)

partial def Code.toExprM (code : Code) : ToExprM Expr := do
  match code with
  | .let decl k =>
    let type ← abstractM decl.type
    let value ← abstractM decl.value.toExpr
    let body ← withFVar decl.fvarId k.toExprM
    return .letE decl.binderName type value body true
  | .fun decl k | .jp decl k =>
    let type ← abstractM decl.type
    let value ← decl.toExprM
    let body ← withFVar decl.fvarId k.toExprM
    return .letE decl.binderName type value body true
  | .return fvarId => fvarId.toExprM
  | .jmp fvarId args => return mkAppN (← fvarId.toExprM) (← args.mapM Arg.toExprM)
  | .unreach type => return mkApp (mkConst ``lcUnreachable) (← abstractM type)
  | .cases c =>
    let alts ← c.alts.mapM fun
      | .alt ctorName params k => do
        let body ← withParams params do mkLambdaM params (← k.toExprM)
        return mkApp (mkConst ctorName) body
      | .default k => k.toExprM
    return mkAppN (mkConst `cases) (#[← c.discr.toExprM] ++ alts)
end

public def Code.toExpr (code : Code) (xs : Array FVarId := #[]) : Expr :=
  run' code.toExprM xs

public def FunDecl.toExpr (decl : FunDecl) (xs : Array FVarId := #[]) : Expr :=
  run' decl.toExprM xs

end Lean.Compiler.LCNF
