lean4-htt/src/Lean/Compiler/Simp.lean
2022-08-20 08:36:27 -07:00

553 lines
18 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
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