Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add simple co-inductive predicates #17

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions src/Lean/Elab/Coinductive.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/-
Copyright (c) 2020 Microsoft Corporation. All rights reserved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have a Lean-FRO copyright?

Released under Apache 2.0 license as described in the file LICENSE.
Authors: William Sørensen
-/
prelude
import Lean.Elab.Command
import Lean.Elab.DeclarationRange
import Lean.Elab.Exception
import Lean.Elab.Inductive
import Lean.Util.Trace

namespace Lean.Elab.Command

open Lean Lean.Elab Lean.Elab.Term Lean.Parser Command

builtin_initialize
registerTraceClass `Elab.CoInductive

structure CoInductiveView.CtorView where

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
structure CoInductiveView.CtorView where
structure CoinductiveView.CtorView where

Very minor nit, but I'd consider coinductive to be one word, and thus not capitalize the I in names

ref : Syntax
modifiers : Modifiers
declName : Name
binders : TSyntaxArray ``Lean.Parser.Term.bracketedBinder
type? : Option Term
deriving Inhabited

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the exact same as the already existing CtorView, do you reckon we could just reuse that struct instead of defining our own?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I defined this is because the original uses Syntax instead of TSyntax at multiple places. I can change over to theirs and add casts and stuff if you'd prefer


structure CoInductiveView where
ref : TSyntax ``Lean.Parser.Command.coinductive
declId : TSyntax ``Parser.Command.declId
modifiers : Modifiers
shortDeclName : Name
declName : Name
levelNames : List Name
binders : TSyntaxArray ``Lean.Parser.Term.bracketedBinder
type : Term
ctors : Array CoInductiveView.CtorView
/- derivingClasses : Array Lean.Elab.DerivingClassView -/
/- computedFields : Array Lean.Elab.Command.ComputedFieldView -/

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/- derivingClasses : Array Lean.Elab.DerivingClassView -/
/- computedFields : Array Lean.Elab.Command.ComputedFieldView -/

Just remove these if we don't want to support them

deriving Inhabited

namespace CoInductiveView

def CtorView.ofStx (declName : Name) (modifiers : Modifiers) (ref : Syntax) : CommandElabM CoInductiveView.CtorView := do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring?

let mut ctorModifiers ← elabModifiers ref[2]
if let some leadingDocComment := ref[0].getOptional? then
if ctorModifiers.docString?.isSome then
logErrorAt leadingDocComment "duplicate doc string"
ctorModifiers := { ctorModifiers with docString? := TSyntax.getDocString ⟨leadingDocComment⟩ }
if ctorModifiers.isPrivate && modifiers.isPrivate then
throwError "invalid 'private' constructor in a 'private' inductive datatype"
if ctorModifiers.isProtected && modifiers.isPrivate then
throwError "invalid 'protected' constructor in a 'private' inductive datatype"
Comment on lines +51 to +54

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ctorModifiers.isPrivate && modifiers.isPrivate then
throwError "invalid 'private' constructor in a 'private' inductive datatype"
if ctorModifiers.isProtected && modifiers.isPrivate then
throwError "invalid 'protected' constructor in a 'private' inductive datatype"
if ctorModifiers.isPrivate && modifiers.isPrivate then
throwError "invalid 'private' constructor in a 'private' coinductive predicate"
if ctorModifiers.isProtected && modifiers.isPrivate then
throwError "invalid 'protected' constructor in a 'private' coinductive predicate"

I assume you copied this code from somewhere, can we not reuse that?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is almost identical to the code to generate an InductiveView but the difference is TSyntax as mentioned above. Once again if its preferred to use Syntax we can just use theirs.


checkValidCtorModifier ctorModifiers
let ctorName := ref.getIdAt 3
let ctorName := declName ++ ctorName
let ctorName ← withRef ref[3] $ applyVisibility ctorModifiers.visibility ctorName
let (binders, type?) := expandOptDeclSig ref[4]
addDocString' ctorName ctorModifiers.docString?
addAuxDeclarationRanges ctorName ref ref[3]

return { ref, modifiers := ctorModifiers, declName := ctorName, binders := binders.getArgs.map (⟨·⟩), type? := type?.map (⟨·⟩) }

def ofModifiersAndStx (modifiers : Modifiers) (decl : Syntax) : CommandElabM CoInductiveView := do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring?

let (binders, type) := expandDeclSig decl[2]!

let binders := binders.getArgs.map (⟨·⟩)

let declId := ⟨decl[1]⟩
let ⟨shortDeclName, declName, levelNames⟩ ← expandDeclId declId.raw modifiers

let ctors ← decl[4].getArgs.mapM $ CtorView.ofStx declName modifiers

addDeclarationRanges declName decl

return {
ref := ⟨decl⟩

declName
shortDeclName

levelNames
type := ⟨type⟩

binders
ctors

declId
modifiers
}

def ofStx (stx : Syntax) := elabModifiers stx[0] >>= (ofModifiersAndStx · stx[1])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring? And I would like to see the type annotation, even if it's trivial to infer


def extractIds : TSyntax ``Parser.Term.bracketedBinder → Array Ident
| ⟨.node _ _ #[.atom _ _, .node _ `null ids, _, _, .atom _ _]⟩ => ids.map (⟨·⟩)
| _ => #[]
Equilibris marked this conversation as resolved.
Show resolved Hide resolved

def toBinderIds (c : CoInductiveView) : Array Ident := (c.binders.map extractIds).flatten

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically every def should have a docstring. It can be short if the name is already quite obvious (but it should reconfirm that indeed it does the obvious thing), or longer if the name is not quite self-explanatory


def toRelType (c : CoInductiveView) : CommandElabM Term :=
c.binders.reverse.foldlM (fun curr acc => `($acc:bracketedBinder → $curr)) c.type

end CoInductiveView

open Parser.Term in
section
abbrev bb := bracketedBinder
/- Since `bb` is an alias for `bracketedBinder`, we can safely coerce syntax of these categories -/
instance : Coe (TSyntax ``bb) (TSyntax ``bracketedBinder) where coe x := ⟨x.raw⟩
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to add a few newlines in between here. Also, add a docstring over bb to explain why we need it.

I don't quite remember what the other alias was called (the one where we stole this trick from), but bb feels like a very short name to me. We should consider making it protected


partial def typeToArgArr (type : Term) : Array Term × Term := Prod.map List.toArray id $ go type.raw
where go
| .node _ ``Parser.Term.arrow #[hd, _, tail] => Prod.map (⟨hd⟩ :: ·) id $ go tail
| rest => ⟨[], ⟨rest⟩⟩

def appsToArgArr (type : Term) : Array Term × Term := match type.raw with
| .node _ ``Parser.Term.app #[v, cont] => ⟨cont.getArgs.map (⟨·⟩), ⟨v⟩⟩
| rest => ⟨#[], ⟨rest⟩⟩

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manipulating Syntax like this feels rather brittle. This was fine for QpfTypes, where I number one priority was to get something that at least kind of works, but if we want to PR to core, we should strongly consider going the proper route and elaborating into Exprs

def extractName : Syntax → Name
| .ident _ _ nm _ => nm
| _ => .anonymous

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this exists already, Ident.getId or something similar


def split : Nat → List α → (List α) × (List α)
| _, [] => ⟨[], []⟩
| 0, arr => ⟨[], arr⟩
| n+1, hd :: tl => Prod.map (hd :: ·) id $ split n tl


-- Coming in these have the form of | name ... : ... Nm topBinders... args...
-- But we want them to have the form | name ... : ... Nm.Shape topBinders... RecName args...
def handleCtor (names : Array Ident) (topView : CoInductiveView) (isTy : Ident) (view : CoInductiveView.CtorView) : CommandElabM CtorView := do
let nm := view.declName.replacePrefix topView.declName (topView.declName ++ `Shape)

let type? ← view.type?.mapM fun type => do
let ⟨args, out⟩ := typeToArgArr type
let ⟨arr, _⟩ := appsToArgArr out

let ⟨pre, post⟩ := (split topView.binders.size arr.toList).map (·.toArray) (·.toArray)

let out ← `($isTy $pre* $names* $post*)

args.reverse.foldlM (fun acc curr => `($curr → $acc)) out

return {
ref := .missing
modifiers := view.modifiers
declName := nm
binders := .node .none `null (view.binders.map (·.raw))
type? := type?
}

def generateIs (vss : Array (CoInductiveView × Array Ident)) (rNameEntries : Array (Ident × Term)) : CommandElabM Unit := do
-- It could be worth making this extract only the names that are required.
let boundRNames ← rNameEntries.mapM (fun ⟨i, v⟩ => do `(bb| ( $i : $v) ))
let coRecArgs ← vss.mapM (fun ⟨v, _⟩ => do `(bb| ( $(mkIdent $ v.shortDeclName) : $(←v.toRelType))))
let names := vss.map (mkIdent ·.fst.shortDeclName)

for ⟨idx, topView, argArr⟩ in vss.toList.enum.toArray do
let shortDeclName := topView.shortDeclName ++ `Shape

let view := {
ref := .missing
declId := ←`(declId| $(mkIdent shortDeclName))
modifiers := topView.modifiers
shortDeclName
declName := topView.declName ++ `Shape
levelNames := topView.levelNames
binders := .node .none `null $ topView.binders.append coRecArgs
type? := some topView.type
ctors := ←topView.ctors.mapM $ handleCtor names topView $ mkIdent shortDeclName
derivingClasses := #[]
computedFields := #[]
}

trace[Elab.CoInductive] s!"{repr topView.binders}"
trace[Elab.CoInductive] s!"{topView.toBinderIds}"

let boundNames := rNameEntries.map Prod.fst
let i := boundNames[idx]! -- OK since these come from the same source

let stx ← `(command|
abbrev $(mkIdent $ topView.shortDeclName ++ `Is) $(topView.binders)* $boundRNames* : Prop :=
∀ { $argArr* }, $i $(topView.toBinderIds)* $argArr* → $(mkIdent shortDeclName) $(topView.toBinderIds)* $boundNames* $argArr*)

trace[Elab.CoInductive] "Generating Is check:"
trace[Elab.CoInductive] stx

elabInductiveViews #[view]
elabCommand stx

def elabCoInductiveViews (views : Array CoInductiveView) : CommandElabM Unit := do
let viewCheck ← views.mapM fun view => do
let ⟨tyArr, out⟩ := typeToArgArr view.type
let argArr := (← tyArr.mapM (fun _ => mkFreshBinderName)).map mkIdent

-- In theory we could make this handle types by simply changing the existential quantification but this would yield some pretty funny results
let .node _ ``Parser.Term.prop _ := out.raw | throwErrorAt out "Expected return type to be a Prop"
return Prod.mk view argArr

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you change the existential? The reason this works is because Prop is impredicative (which is a funny way of saying that Prop -> Prop is of type Prop). Once you start looking at types, you run into the issue that Type -> Type lives in Type 1, causing nasty universe bumps.

If you meant "pull in QpfTypes", sure, but that is very explicitly not something that core wants

Suggested change
-- In theory we could make this handle types by simply changing the existential quantification but this would yield some pretty funny results
let .node _ ``Parser.Term.prop _ := out.raw | throwErrorAt out "Expected return type to be a Prop"
return Prod.mk view argArr
let .node _ ``Parser.Term.prop _ := out.raw | throwErrorAt out "Expected return type to be a Prop"
return Prod.mk view argArr


let rNameEntries ← viewCheck.mapM (fun ⟨v, _⟩ => return Prod.mk (mkIdent $ ←mkFreshBinderName) (←v.toRelType))

generateIs viewCheck rNameEntries
for ⟨idx, view, argArr⟩ in viewCheck.toList.enum.toArray do
let boundNames := rNameEntries.map Prod.fst
let i := boundNames[idx]! -- OK since these come from the same source

let stx ← `(def $(mkIdent view.shortDeclName) $(view.binders)* : $(view.type) := fun $argArr* =>
∃ $[$boundNames:ident]*, @$(mkIdent $ view.shortDeclName ++ `Is) $(view.toBinderIds)* $boundNames* ∧ $i $(view.toBinderIds)* $argArr*)

trace[Elab.CoInductive] "Generating co-inductive:"
trace[Elab.CoInductive] stx
elabCommand stx

9 changes: 9 additions & 0 deletions src/Lean/Elab/Declaration.lean
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Lean.Util.CollectLevelParams
import Lean.Elab.DeclUtil
import Lean.Elab.DefView
import Lean.Elab.Inductive
import Lean.Elab.Coinductive
import Lean.Elab.Structure
import Lean.Elab.MutualDef
import Lean.Elab.DeclarationRange
Expand Down Expand Up @@ -47,6 +48,7 @@ private def isNamedDef (stx : Syntax) : Bool :=
k == ``Lean.Parser.Command.opaque ||
k == ``Lean.Parser.Command.axiom ||
k == ``Lean.Parser.Command.inductive ||
k == ``Lean.Parser.Command.coinductive ||
k == ``Lean.Parser.Command.classInductive ||
k == ``Lean.Parser.Command.structure

Expand Down Expand Up @@ -183,6 +185,10 @@ def elabInductive (modifiers : Modifiers) (stx : Syntax) : CommandElabM Unit :=
let v ← inductiveSyntaxToView modifiers stx
elabInductiveViews #[v]

def elabCoInductive (modifiers : Modifiers) (stx : Syntax) : CommandElabM Unit := do
let v ← CoInductiveView.ofModifiersAndStx modifiers stx
elabCoInductiveViews #[v]

def elabClassInductive (modifiers : Modifiers) (stx : Syntax) : CommandElabM Unit := do
let modifiers := modifiers.addAttr { name := `class }
let v ← classInductiveSyntaxToView modifiers stx
Expand Down Expand Up @@ -216,6 +222,9 @@ def elabDeclaration : CommandElab := fun stx => do
else if declKind == ``Lean.Parser.Command.«inductive» then
let modifiers ← elabModifiers stx[0]
elabInductive modifiers decl
else if declKind == ``Lean.Parser.Command.«coinductive» then
let modifiers ← elabModifiers stx[0]
elabCoInductive modifiers decl
else if declKind == ``Lean.Parser.Command.classInductive then
let modifiers ← elabModifiers stx[0]
elabClassInductive modifiers decl
Expand Down
5 changes: 4 additions & 1 deletion src/Lean/Parser/Command.lean
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ For more information about [inductive types](https://lean-lang.org/theorem_provi
def «inductive» := leading_parser
"inductive " >> recover declId skipUntilWsOrDelim >> ppIndent optDeclSig >> optional (symbol " :=" <|> " where") >>
many ctor >> optional (ppDedent ppLine >> computedFields) >> optDeriving
def «coinductive» := leading_parser
"coinductive " >> recover declId skipUntilWsOrDelim >> ppIndent declSig >> optional (symbol " :=" <|> " where") >>
many ctor >> optional (ppDedent ppLine >> computedFields) -- >> optDeriving --TODO: Handle deriving
def classInductive := leading_parser
atomic (group (symbol "class " >> "inductive ")) >>
recover declId skipUntilWsOrDelim >> ppIndent optDeclSig >>
Expand Down Expand Up @@ -236,7 +239,7 @@ def «structure» := leading_parser
@[builtin_command_parser] def declaration := leading_parser
declModifiers false >>
(«abbrev» <|> definition <|> «theorem» <|> «opaque» <|> «instance» <|> «axiom» <|> «example» <|>
«inductive» <|> classInductive <|> «structure»)
«coinductive» <|> «inductive» <|> classInductive <|> «structure»)
@[builtin_command_parser] def «deriving» := leading_parser
"deriving " >> "instance " >> derivingClasses >> " for " >> sepBy1 (recover ident skip) ", "
@[builtin_command_parser] def noncomputableSection := leading_parser
Expand Down
87 changes: 87 additions & 0 deletions tests/lean/run/coindBisim.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
structure FSM where
S : Type
d : S → Nat → S
A : S → Prop

-- Example of a coinductive predicate defined over FSMs

coinductive Bisim (fsm : FSM) : fsm.S → fsm.S → Prop :=
| step {s t : fsm.S} :
(fsm.A s ↔ fsm.A t)
→ (∀ c, Bisim fsm (fsm.d s c) (fsm.d t c))
→ Bisim fsm s t

/--
info: inductive Bisim.Shape : (fsm : FSM) → ((fsm : FSM) → fsm.S → fsm.S → Prop) → fsm.S → fsm.S → Prop
number of parameters: 4
constructors:
Bisim.Shape.step : ∀ {fsm : FSM} {Bisim : (fsm : FSM) → fsm.S → fsm.S → Prop} {s t : fsm.S},
(fsm.A s ↔ fsm.A t) → (∀ (c : Nat), Bisim fsm (fsm.d s c) (fsm.d t c)) → Bisim.Shape fsm Bisim s t
-/
Comment on lines +15 to +20

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't feel right. FSM is a fixed parameter to Bisim (it is given as a binder, before the :), and should not show up in the motive. That is, I would've expected this to have type
(fsm : FSM) → (fsm.S → fsm.S → Prop) → fsm.S → fsm.S → Prop

#guard_msgs in
#print Bisim.Shape

/--
info: @[reducible] def Bisim.Is : FSM → ((fsm : FSM) → fsm.S → fsm.S → Prop) → Prop :=
fun fsm x => ∀ {x_1 x_2 : fsm.S}, x fsm x_1 x_2 → Bisim.Shape fsm x x_1 x_2
-/
#guard_msgs in
#print Bisim.Is

/--
info: def Bisim : (fsm : FSM) → fsm.S → fsm.S → Prop :=
fun fsm x x_1 => ∃ x_2, Bisim.Is fsm x_2 ∧ x_2 fsm x x_1
-/
#guard_msgs in
#print Bisim

/-- info: 'Bisim' does not depend on any axioms -/
#guard_msgs in
#print axioms Bisim

theorem bisim_refl : Bisim f a a := by
exists fun _ a b => a = b
simp only [and_true]
intro s t seqt
constructor <;> simp_all

theorem bisim_symm (h : Bisim f a b): Bisim f b a := by
rcases h with ⟨rel, relIsBisim, rab⟩
exists fun f a b => rel f b a
simp_all
intro a b holds
specialize relIsBisim holds
rcases relIsBisim with ⟨imp, z⟩
constructor <;> simp_all only [implies_true, and_self]

theorem Bisim.unfold {f} : Bisim.Is f Bisim := by
rintro s t ⟨R, h_is, h_Rst⟩
constructor
· exact (h_is h_Rst).1
· intro c; exact ⟨R, h_is, (h_is h_Rst).2 c⟩

theorem bisim_trans (h_ab : Bisim f a b) (h_bc : Bisim f b c) :
Bisim f a c := by
exists (fun f s t => ∃ u, Bisim f s u ∧ Bisim f u t)
constructor
intro s t h_Rst
· rcases h_Rst with ⟨u, h_su, h_ut⟩
have ⟨h_su₁, h_su₂⟩ := h_su.unfold
have ⟨h_ut₁, h_ut₂⟩ := h_ut.unfold
refine ⟨?_, ?_⟩
· rw [h_su₁, h_ut₁]
· intro c; exact ⟨_, h_su₂ c, h_ut₂ c⟩
· exact ⟨b, h_ab, h_bc⟩

/-- info: 'bisim_refl' depends on axioms: [propext, Quot.sound] -/
#guard_msgs in
#print axioms bisim_refl

/-- info: 'bisim_symm' depends on axioms: [propext, Quot.sound] -/
#guard_msgs in
#print axioms bisim_symm

/-- info: 'bisim_trans' depends on axioms: [propext] -/
#guard_msgs in
#print axioms bisim_trans

7 changes: 7 additions & 0 deletions tests/lean/run/coindTypes.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Coinductive currently does only work on Prop

/-- error: Expected return type to be a Prop -/
#guard_msgs in
coinductive S (α : Type) : Type :=
| cons (hd : α) (tl : S α)

Loading