553 lines
18 KiB
Text
553 lines
18 KiB
Text
/-
|
||
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.Compiler.CompilerM
|
||
import Lean.Compiler.Decl
|
||
import Lean.Compiler.Stage1
|
||
import Lean.Compiler.InlineAttrs
|
||
|
||
namespace Lean.Compiler
|
||
namespace Simp
|
||
|
||
partial def findLambda? (e : Expr) : CompilerM (Option LocalDecl) := do
|
||
match e with
|
||
| .fvar fvarId =>
|
||
let some d@(.ldecl (value := v) ..) ← findDecl? fvarId | return none
|
||
if v.isLambda then return some d else findLambda? v
|
||
| .mdata _ e => findLambda? e
|
||
| _ => return none
|
||
|
||
partial def findExpr (e : Expr) (skipMData := true): CompilerM Expr := do
|
||
match e with
|
||
| .fvar fvarId =>
|
||
let some (.ldecl (value := v) ..) ← findDecl? fvarId | return e
|
||
findExpr v
|
||
| .mdata _ e' => if skipMData then findExpr e' else return e
|
||
| _ => return e
|
||
|
||
inductive Occ where
|
||
| once
|
||
| many
|
||
deriving Repr, Inhabited
|
||
|
||
/--
|
||
Local function declaration statistics.
|
||
|
||
Remark: we use the `userName` as the key. Thus, `ensureUniqueLetVarNames`
|
||
must be used before collectin stastistics.
|
||
-/
|
||
structure OccInfo where
|
||
/--
|
||
Mapping from local function name to occurrence information.
|
||
-/
|
||
map : Std.HashMap Name Occ := {}
|
||
deriving Inhabited
|
||
|
||
def OccInfo.format (s : OccInfo) : Format := Id.run do
|
||
let mut result := Format.nil
|
||
for (k, n) in s.map.toList do
|
||
result := result ++ "\n" ++ f!"{k} ↦ {repr n}"
|
||
return result
|
||
|
||
instance : ToFormat OccInfo where
|
||
format := OccInfo.format
|
||
|
||
def OccInfo.add (s : OccInfo) (key : Name) : OccInfo :=
|
||
match s with
|
||
| { map } =>
|
||
match map.find? key with
|
||
| some .once => { map := map.insert key .many }
|
||
| none => { map := map.insert key .once }
|
||
| _ => { map }
|
||
|
||
structure Config where
|
||
smallThreshold : Nat := 1
|
||
|
||
structure Context where
|
||
config : Config := {}
|
||
|
||
structure State where
|
||
/--
|
||
(Approximate) occurence information for local function declarations.
|
||
-/
|
||
occInfo : OccInfo := {}
|
||
simplified : Bool := false
|
||
deriving Inhabited
|
||
|
||
abbrev SimpM := ReaderT Context $ StateRefT State CompilerM
|
||
|
||
/-- Ensure binder names are unique, and update occurrence information -/
|
||
partial def internalize (e : Expr) : SimpM Expr := do
|
||
visitLambda e
|
||
where
|
||
visitLambda (e : Expr) : SimpM Expr := do
|
||
withNewScope do
|
||
let (as, e) ← Compiler.visitLambdaCore e
|
||
let e ← mkLetUsingScope (← visitLet e as)
|
||
mkLambda as e
|
||
|
||
visitCases (casesInfo : CasesInfo) (cases : Expr) : SimpM Expr := do
|
||
let mut args := cases.getAppArgs
|
||
for i in casesInfo.altsRange do
|
||
args ← args.modifyM i visitLambda
|
||
return mkAppN cases.getAppFn args
|
||
|
||
visitValue (e : Expr) : SimpM Unit := do
|
||
if e.isApp then
|
||
match (← findLambda? e.getAppFn) with
|
||
| some localDecl =>
|
||
if localDecl.value.isLambda then
|
||
let key := localDecl.userName
|
||
modify fun s => { s with occInfo := s.occInfo.add key }
|
||
| _ => pure ()
|
||
|
||
visitLet (e : Expr) (xs : Array Expr) : SimpM Expr := do
|
||
match e with
|
||
| .letE binderName type value body nonDep =>
|
||
let idx ← mkFreshLetVarIdx
|
||
let binderName' := match binderName with
|
||
| .num p _ => .num p idx
|
||
| _ => .num binderName idx
|
||
let type := type.instantiateRev xs
|
||
let mut value := value.instantiateRev xs
|
||
if value.isLambda then
|
||
value ← visitLambda value
|
||
else
|
||
visitValue value
|
||
let x ← mkLetDecl binderName' type value nonDep
|
||
visitLet body (xs.push x)
|
||
| _ =>
|
||
let e := e.instantiateRev xs
|
||
if let some casesInfo ← isCasesApp? e then
|
||
visitCases casesInfo e
|
||
else
|
||
visitValue e
|
||
return e
|
||
|
||
def markSimplified : SimpM Unit :=
|
||
modify fun s => { s with simplified := true }
|
||
|
||
def findCtor (e : Expr) : SimpM Expr := do
|
||
-- TODO: add support for mapping discriminants to constructors in branches
|
||
findExpr e
|
||
|
||
/--
|
||
Try to simplify projections `.proj _ i s` where `s` is constructor.
|
||
-/
|
||
def simpProj? (e : Expr) : OptionT SimpM Expr := do
|
||
let .proj _ i s := e | failure
|
||
let s ← findCtor s
|
||
let some (ctorVal, args) := s.constructorApp? (← getEnv) | failure
|
||
markSimplified
|
||
return args[ctorVal.numParams + i]!
|
||
|
||
/--
|
||
Application over application.
|
||
```
|
||
let _x.i := f a
|
||
_x.i b
|
||
```
|
||
is simplified to `f a b`.
|
||
-/
|
||
def simpAppApp? (e : Expr) : OptionT SimpM Expr := do
|
||
guard e.isApp
|
||
let f := e.getAppFn
|
||
guard f.isFVar
|
||
let f ← findExpr f
|
||
guard <| f.isApp || f.isConst
|
||
markSimplified
|
||
return mkAppN f e.getAppArgs
|
||
|
||
def shouldInlineLocal (localDecl : LocalDecl) : SimpM Bool := do
|
||
match (← get).occInfo.map.find? localDecl.userName with
|
||
| some .once => return true
|
||
| _ => lcnfSizeLe localDecl.value (← read).config.smallThreshold
|
||
|
||
structure InlineCandidateInfo where
|
||
isLocal : Bool
|
||
arity : Nat
|
||
/-- Value (lambda expression) of the function to be inlined. -/
|
||
value : Expr
|
||
|
||
def inlineCandidate? (e : Expr) : SimpM (Option InlineCandidateInfo) := do
|
||
let f := e.getAppFn
|
||
if let .const declName us ← findExpr f then
|
||
unless hasInlineAttribute (← getEnv) declName do return none
|
||
-- TODO: check whether function is recursive or not.
|
||
-- We can skip the test and store function inline so far.
|
||
let some decl ← getStage1Decl? declName | return none
|
||
let numArgs := e.getAppNumArgs
|
||
let arity := decl.getArity
|
||
if numArgs < arity then return none
|
||
/-
|
||
Recall that we use binder names to build `InlineStats`.
|
||
Thus, we use `ensureUniqueLetVarNames` to make sure there is no name collision.
|
||
-/
|
||
let value ← ensureUniqueLetVarNames (decl.value.instantiateLevelParams decl.levelParams us)
|
||
return some {
|
||
arity, value
|
||
isLocal := false
|
||
}
|
||
else if let some localDecl ← findLambda? f then
|
||
unless (← shouldInlineLocal localDecl) do return none
|
||
let numArgs := e.getAppNumArgs
|
||
let arity := getLambdaArity localDecl.value
|
||
if numArgs < arity then return none
|
||
let value ← ensureUniqueLetVarNames localDecl.value
|
||
return some {
|
||
arity, value
|
||
isLocal := true
|
||
}
|
||
else
|
||
return none
|
||
|
||
/--
|
||
If `e` if a free variable that expands to a valid LCNF terminal `let`-block expression `e'`,
|
||
return `e'`.
|
||
-/
|
||
def expandTrivialExpr (e : Expr) : SimpM Expr := do
|
||
if e.isFVar then
|
||
let e' ← findExpr e
|
||
unless e'.isLambda do
|
||
if e != e' then
|
||
markSimplified
|
||
return e'
|
||
return e
|
||
|
||
/--
|
||
Given `value` of the form `let x_1 := v_1; ...; let x_n := v_n; e`,
|
||
return `let x_1; ...; let x_n := v_n; let y : type := e; body`.
|
||
|
||
This methods assumes `type` and `value` do not have loose bound variables.
|
||
|
||
Remark: `body` may have many loose bound variables, and the loose bound variables > 0
|
||
must be lifted by `n`.
|
||
-/
|
||
def mkFlatLet (y : Name) (type : Expr) (value : Expr) (body : Expr) (nonDep : Bool := false) : Expr :=
|
||
go value 0
|
||
where
|
||
go (value : Expr) (i : Nat) : Expr :=
|
||
match value with
|
||
| .letE n t v b d => .letE n t v (go b (i+1)) d
|
||
| _ => .letE y type value (body.liftLooseBVars 1 i) nonDep
|
||
|
||
/--
|
||
Auxiliary function for projecting "type class dictionary access".
|
||
That is, we are trying to extract one of the type class instance elements.
|
||
Remark: We do not consider parent instances to be elements.
|
||
For example, suppose `e` is `_x_4.1`, and we have
|
||
```
|
||
_x_2 : Monad (ReaderT Bool (ExceptT String Id)) := @ReaderT.Monad Bool (ExceptT String Id) _x_1
|
||
_x_3 : Applicative (ReaderT Bool (ExceptT String Id)) := _x_2.1
|
||
_x_4 : Functor (ReaderT Bool (ExceptT String Id)) := _x_3.1
|
||
```
|
||
Then, we will expand `_x_4.1` since it corresponds to the `Functor` `map` element,
|
||
and its type is not a type class, but is of the form
|
||
```
|
||
{α β : Type u} → (α → β) → ...
|
||
```
|
||
In the example above, the compiler should not expand `_x_3.1` or `_x_2.1` because they are
|
||
type class applications: `Functor` and `Applicative` respectively.
|
||
By eagerly expanding them, we may produce inefficient and bloated code.
|
||
For example, we may be using `_x_3.1` to invoke a function that expects a `Functor` instance.
|
||
By expanding `_x_3.1` we will be just expanding the code that creates this instance.
|
||
-/
|
||
partial def inlineProjInst? (e : Expr) : OptionT SimpM Expr := do
|
||
let .proj _ _ s := e | failure
|
||
let sType ← inferType s
|
||
guard (← isClass? sType).isSome
|
||
let eType ← inferType e
|
||
guard (← isClass? eType).isNone
|
||
/-
|
||
We use `withNewScope` + `mkLetUsingScope` to filter the relevant let-declarations.
|
||
Recall that we are extracting only one of the type class elements.
|
||
-/
|
||
let value ← withNewScope do mkLetUsingScope (← visitProj e)
|
||
markSimplified
|
||
internalize value
|
||
where
|
||
visitProj (e : Expr) : OptionT SimpM Expr := do
|
||
let .proj _ i s := e | unreachable!
|
||
let s ← visit s
|
||
if let some (ctorVal, ctorArgs) := s.constructorApp? (← getEnv) then
|
||
return ctorArgs[ctorVal.numParams + i]!
|
||
else
|
||
failure
|
||
|
||
visit (e : Expr) : OptionT SimpM Expr := do
|
||
let e ← findExpr e
|
||
if e.isConstructorApp (← getEnv) then
|
||
return e
|
||
else if e.isProj then
|
||
/- We may have nested projections as we traverse parent classes. -/
|
||
visit (← visitProj e)
|
||
else
|
||
let .const declName us := e.getAppFn | failure
|
||
let some decl ← getStage1Decl? declName | failure
|
||
guard <| decl.getArity == e.getAppNumArgs
|
||
let value := decl.value.instantiateLevelParams decl.levelParams us
|
||
let value := value.beta e.getAppArgs
|
||
/-
|
||
Here, we just go inside of the let-declaration block without trying to simplify it.
|
||
Reason: a type class instannce may have many elements, and it does not make sense to simplify
|
||
all of them when we are extracting only one of them.
|
||
-/
|
||
let value ← Compiler.visitLet (m := SimpM) value fun _ value => return value
|
||
visit value
|
||
|
||
def betaReduce (e : Expr) (args : Array Expr) : SimpM Expr := do
|
||
-- TODO: add necessary casts
|
||
internalize (e.beta args)
|
||
|
||
/--
|
||
Try "cases on cases" simplification.
|
||
If `casesFn args` is of the form
|
||
```
|
||
casesOn _x.i
|
||
(... let _x.j₁ := ctorⱼ₁ ...; _jp.k _x.j₁)
|
||
...
|
||
(... let _x.jₙ := ctorⱼₙ ...; _jp.k _x.jₙ)
|
||
```
|
||
where `_jp.k` is a join point of the form
|
||
```
|
||
let _jp.k := fun y =>
|
||
casesOn y ...
|
||
```
|
||
Then, inline `_jp.k`. The idea is to force the `casesOn` application in the join point to
|
||
reduce after the inlining step.
|
||
Example: consider the following declarations
|
||
```
|
||
@[inline] def pred? (x : Nat) : Option Nat :=
|
||
match x with
|
||
| 0 => none
|
||
| x+1 => some x
|
||
|
||
def isZero (x : Nat) :=
|
||
match pred? x with
|
||
| some _ => false
|
||
| none => true
|
||
```
|
||
After inlining `pred?` in `isZero`, we have
|
||
```
|
||
let _jp.1 := fun y : Option Nat =>
|
||
casesOn y true (fun y => false)
|
||
casesOn x
|
||
(let _x.1 := none; _jp.1 _x.1)
|
||
(fun n => let _x.2 := some n; _jp.1 _x.2)
|
||
```
|
||
and this simplification is applicable, producing
|
||
```
|
||
casesOn x true (fun n => false)
|
||
```
|
||
-/
|
||
def simpCasesOnCases? (casesInfo : CasesInfo) (casesFn : Expr) (args : Array Expr) : OptionT SimpM Expr := do
|
||
let mut jpFirst? := none
|
||
for i in casesInfo.altsRange do
|
||
let alt := args[i]!
|
||
let jp ← isJpCtor? alt
|
||
if let some jpFirst := jpFirst? then
|
||
guard <| jp == jpFirst
|
||
else
|
||
let some localDecl ← findDecl? jp | failure
|
||
let .lam _ _ jpBody _ := localDecl.value | failure
|
||
guard (← isCasesApp? jpBody).isSome
|
||
jpFirst? := jp
|
||
let some jpFVarId := jpFirst? | failure
|
||
let some localDecl ← findDecl? jpFVarId | failure
|
||
let .lam _ _ jpBody _ := localDecl.value | failure
|
||
let mut args := args
|
||
for i in casesInfo.altsRange do
|
||
args := args.modify i (inlineJp · jpBody)
|
||
return mkAppN casesFn args
|
||
where
|
||
isJpCtor? (alt : Expr) : OptionT SimpM FVarId := do
|
||
match alt with
|
||
| .lam _ _ b _ => isJpCtor? b
|
||
| .letE _ _ v b _ => match b with
|
||
| .letE .. => isJpCtor? b
|
||
| .app (.fvar fvarId) (.bvar 0) =>
|
||
let some localDecl ← findDecl? fvarId | failure
|
||
guard localDecl.isJp
|
||
guard <| v.isConstructorApp (← getEnv)
|
||
return fvarId
|
||
| _ => failure
|
||
| _ => failure
|
||
|
||
inlineJp (alt : Expr) (jpBody : Expr) : Expr :=
|
||
match alt with
|
||
| .lam n d b bi => .lam n d (inlineJp b jpBody) bi
|
||
| .letE n t v b nd => .letE n t v (inlineJp b jpBody) nd
|
||
| _ => jpBody
|
||
|
||
mutual
|
||
/--
|
||
Simplify the given lambda expression.
|
||
If `checkEmptyTypes := true`, then return `fun a_i : t_i => lcUnreachable` if
|
||
`t_i` is the `Empty` type.
|
||
-/
|
||
partial def visitLambda (e : Expr) (checkEmptyTypes := false): SimpM Expr :=
|
||
withNewScope do
|
||
let (as, e) ← Compiler.visitLambdaCore e
|
||
if checkEmptyTypes then
|
||
for a in as do
|
||
if (← isEmptyType (← inferType a)) then
|
||
let e := e.instantiateRev as
|
||
let unreach ← mkLcUnreachable (← inferType e)
|
||
let r ← mkLambda as unreach
|
||
return r
|
||
let e ← mkLetUsingScope (← visitLet e as)
|
||
mkLambda as e
|
||
|
||
partial def visitCases (casesInfo : CasesInfo) (e : Expr) : SimpM Expr := do
|
||
let f := e.getAppFn
|
||
let mut args := e.getAppArgs
|
||
let major := args[casesInfo.discrsRange.stop - 1]!
|
||
let major ← findExpr major
|
||
if let some (ctorVal, ctorArgs) := major.constructorApp? (← getEnv) then
|
||
/- Simplify `casesOn` constructor -/
|
||
let ctorIdx := ctorVal.cidx
|
||
let alt := args[casesInfo.altsRange.start + ctorIdx]!
|
||
let ctorFields := ctorArgs[ctorVal.numParams:]
|
||
let alt := alt.beta ctorFields
|
||
assert! !alt.isLambda
|
||
markSimplified
|
||
visitLet alt
|
||
else if let some e ← simpCasesOnCases? casesInfo f args then
|
||
visitCases casesInfo e
|
||
else
|
||
for i in casesInfo.altsRange do
|
||
args ← args.modifyM i (visitLambda · (checkEmptyTypes := true))
|
||
return mkAppN f args
|
||
|
||
/--
|
||
If `e` is an application that can be inlined, inline it.
|
||
|
||
`k?` is the optional "continuation" for `e`, and it may contain loose bound variables
|
||
that need to instantiated with `xs`. That is, if `k? = some k`, then `k.instantiateRev xs`
|
||
is an expression without loose bound variables.
|
||
-/
|
||
partial def inlineApp? (e : Expr) (xs : Array Expr) (k? : Option Expr) : SimpM (Option Expr) := do
|
||
let some info ← inlineCandidate? e | return none
|
||
let args := e.getAppArgs
|
||
let numArgs := args.size
|
||
trace[Compiler.simp.inline] "inlining {e}"
|
||
markSimplified
|
||
if k?.isNone && numArgs == info.arity then
|
||
/- Easy case, there is no continuation and `e` is not over applied -/
|
||
visitLet (← betaReduce info.value args)
|
||
else if (← onlyOneExitPoint info.value) then
|
||
/- If `info.value` has only one exit point, we don't need to create a new auxiliary join point -/
|
||
let mut value ← betaReduce info.value args[:info.arity]
|
||
if numArgs > info.arity then
|
||
let type ← inferType (mkAppN e.getAppFn args[:info.arity])
|
||
value := mkFlatLet (← mkAuxLetDeclName) type value (mkAppN (.bvar 0) args[info.arity:])
|
||
if let some k := k? then
|
||
let type ← inferType e
|
||
value := mkFlatLet (← mkAuxLetDeclName) type value k
|
||
visitLet value xs
|
||
else
|
||
/-
|
||
There is a continuation `k` or `e` is over applied.
|
||
If `e` is over applied, the extra arguments act as a continuation.
|
||
|
||
We create a new join point
|
||
```
|
||
let jp := fun y =>
|
||
let x := y <extra-arguments> -- if `e` is over applied
|
||
k
|
||
```
|
||
Recall that `visitLet` incorporates the current continuation
|
||
to the new join point `jp`.
|
||
-/
|
||
let jpDomain ← inferType (mkAppN e.getAppFn args[:info.arity])
|
||
let binderName ← mkFreshUserName `_y
|
||
let jp ← withNewScope do
|
||
let y ← mkLocalDecl binderName jpDomain
|
||
let body ← if numArgs == info.arity then
|
||
visitLet k?.get! (xs.push y)
|
||
else
|
||
let x ← mkAuxLetDecl (mkAppN y args[info.arity:])
|
||
if let some k := k? then
|
||
visitLet k (xs.push x)
|
||
else
|
||
visitLet x (xs.push x)
|
||
let body ← mkLetUsingScope body
|
||
mkLambda #[y] body
|
||
let jp ← mkJpDeclIfNotSimple jp
|
||
let value ← betaReduce info.value args[:info.arity]
|
||
let value ← attachJp value jp
|
||
visitLet value
|
||
|
||
/-- Try to apply simple simplifications. -/
|
||
partial def simpValue? (e : Expr) : SimpM (Option Expr) :=
|
||
simpProj? e <|> simpAppApp? e <|> inlineProjInst? e
|
||
|
||
/--
|
||
Let-declaration basic block visitor. `e` may contain loose bound variables that
|
||
still have to be instantiated with `xs`.
|
||
-/
|
||
partial def visitLet (e : Expr) (xs : Array Expr := #[]): SimpM Expr := do
|
||
match e with
|
||
| .letE binderName type value body nonDep =>
|
||
let mut value := value.instantiateRev xs
|
||
if value.isLambda then
|
||
value ← visitLambda value
|
||
else if let some value' ← simpValue? value then
|
||
if value'.isLet then
|
||
let e := mkFlatLet binderName type value' body nonDep
|
||
let e ← visitLet e xs
|
||
return e
|
||
value := value'
|
||
if value.isFVar then
|
||
/- Eliminate `let _x_i := _x_j;` -/
|
||
markSimplified
|
||
visitLet body (xs.push value)
|
||
else if let some e ← inlineApp? value xs body then
|
||
return e
|
||
else
|
||
let type := type.instantiateRev xs
|
||
let x ← mkLetDecl binderName type value nonDep
|
||
visitLet body (xs.push x)
|
||
| _ =>
|
||
let e := e.instantiateRev xs
|
||
if let some value ← simpValue? e then
|
||
visitLet value
|
||
else if let some casesInfo ← isCasesApp? e then
|
||
visitCases casesInfo e
|
||
else if let some e ← inlineApp? e #[] none then
|
||
return e
|
||
else
|
||
expandTrivialExpr e
|
||
end
|
||
|
||
end Simp
|
||
|
||
def Decl.simp? (decl : Decl) : Simp.SimpM (Option Decl) := do
|
||
let value ← Simp.internalize decl.value
|
||
trace[Compiler.simp.inline.occs] "{decl.name}:{Format.nest 2 (format (← get).occInfo)}"
|
||
trace[Compiler.simp.step] "{decl.name} :=\n{decl.value}"
|
||
let value ← Simp.visitLambda value
|
||
trace[Compiler.simp.step.new] "{decl.name} :=\n{value}"
|
||
trace[Compiler.simp.stat] "{decl.name}: {← getLCNFSize decl.value}"
|
||
if (← get).simplified then
|
||
return some { decl with value }
|
||
else
|
||
return none
|
||
|
||
partial def Decl.simp (decl : Decl) : CoreM Decl := do
|
||
if let some decl ← decl.simp? |>.run {} |>.run' {} |>.run' {} then
|
||
-- TODO: bound number of steps?
|
||
decl.simp
|
||
else
|
||
return decl
|
||
|
||
builtin_initialize
|
||
registerTraceClass `Compiler.simp.inline
|
||
registerTraceClass `Compiler.simp.stat
|
||
registerTraceClass `Compiler.simp.step
|
||
registerTraceClass `Compiler.simp.step.new
|
||
registerTraceClass `Compiler.simp.inline.occs
|
||
|
||
end Lean.Compiler
|