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

Conversation

Equilibris
Copy link

@Equilibris Equilibris commented Aug 21, 2024

Add basic implementation of co-inductive predicates using cofix points expressed as existential quantifications. This is done by first generating a Shape inductive datatype then quantifying over this. For example, if this were to be hand implemented in the context of a bisimulation it looks something like this:

import Matlib.Data.Set.Basic

structure FSM where
  S : Type
  d : S -> Set S
  A : Set S

def IsBisim (fsm : FSM) (R : fsm.S → fsm.S → Prop) : Prop :=
  ∀ s t, R s t →
    (s ∈ fsm.A ↔ t ∈ fsm.A)
    ∧ (∀ s' ∈ fsm.d s, ∃ t' ∈ fsm.d t, R s' t')
    ∧ (∀ t' ∈ fsm.d t, ∃ s' ∈ fsm.d s, R s' t')

def Bisim (fsm : FSM) : fsm.S → fsm.S → Prop :=
  fun s t => ∃ R, IsBisim fsm R ∧ R s t

(example by @alexkeizer )

This is writable if hard, rather, a new syntax is added in this PR that looks like the following:

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

An example of what this yields is generated in the test coindBisim.lean.

Currently, this does only hold for co-inductive predicates (i.e. the ones that reside in Prop).

The generation of shape types is done by taking the original written syntax and simply adding another parameter with the name and signature of the entire expression. In the case of Bisim this shape looks like this:

inductive Bisim.Shape (fsm : FSM) (Bisim : (fsm : FSM) → fsm.S → fsm.S → Prop) : fsm.S → fsm.S → Prop
  | step {s t : fsm.S} :
    (s ∈ fsm.A ↔ t ∈ fsm.A)
    → (∀ s' ∈ fsm.d s, ∃ t' ∈ fsm.d t, Bisim fsm s' t')
    → (∀ t' ∈ fsm.d t, ∃ s' ∈ fsm.d s, Bisim fsm s' t')
    → Bisim.Shape fsm Bisim s t

The fixed point is then found with the same means as listed above.


TODOs:

  • Tests
    • Base tests
    • Mutual tests
  • Mutual coinductives
    • These could be added by allowing the type to contain multiple parameters as inputs which are then quantified over separately in the code
  • toBinderViews alternatives
  • Alternative to how quoting bracketed binders works currently
  • Fixing imports

Comment on lines 143 to 144
/- #check Lean.Elab.Binders.to -/

Choose a reason for hiding this comment

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

Suggested change
/- #check Lean.Elab.Binders.to -/

Comment on lines 211 to 212
we can safely coerce syntax of these categories -/
instance : Coe (TSyntax ``bb) (TSyntax ``bracketedBinder) where coe x := ⟨x.raw⟩

Choose a reason for hiding this comment

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

Suggested change
we can safely coerce syntax of these categories -/
instance : Coe (TSyntax ``bb) (TSyntax ``bracketedBinder) where coe x := ⟨x.raw⟩
we can safely coerce syntax of these categories. -/
instance : Coe (TSyntax ``bb) (TSyntax ``bracketedBinder) where coe x := ⟨x.raw⟩

@alexkeizer
Copy link

Mutual coinductives
These could be added by allowing the type to contain multiple parameters as inputs which are then quantified over seperately > in the code

Let's not worry about mutuals at all in this first PR

Copy link

@alexkeizer alexkeizer left a comment

Choose a reason for hiding this comment

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

I gave it a look over. @Equilibris it would help a lot if you did a pass and document all the various functions that you've defined

This doesn't feel ready to be PRd to core yet. I think we should look into how hard it would be to go the proper route and elaborate into Exprs

Comment on lines 38 to 39
/- 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

Comment on lines 20 to 26
structure CoInductiveView.CtorView where
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

Comment on lines +50 to +53
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"

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.


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?


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?

Comment on lines +15 to +20
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
-/

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

Comment on lines 200 to 202
-- 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

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

Comment on lines 123 to 125
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

Comment on lines 114 to 122
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

@@ -0,0 +1,253 @@
/-
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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants