Skip to content

feat: Generalize type of step indices with metaprogramming and typeclasses - #576

Open
markusdemedeiros wants to merge 42 commits into
masterfrom
sidx-defaults
Open

feat: Generalize type of step indices with metaprogramming and typeclasses #576
markusdemedeiros wants to merge 42 commits into
masterfrom
sidx-defaults

Conversation

@markusdemedeiros

@markusdemedeiros markusdemedeiros commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

This PR builds off of Alvin's work in #550, as well as Mario's work and my own attempt. I believe it solves every single problem reported with the prior attempts at this feature. The text here is long because I will try to explain how.

TL;DR:

  • A strictly isolated typeclass restores the old outParam behaviors around local class inference.
  • It does not violate the global typeclass rules for our unbundled algebraic hierarchy.
  • A lightweight command elab guards against common errors.
  • No duplicate or scoped notation needed and essentially no step index type annotations (the single exception is OFE.eq_dist: see limitations below).
  • No bundling, automation is unchanged.
  • Fully opt-in, hierarchy behaves normally when turned off. Working without default indices enabled allows multiple (non-overlapping) SIdx classes even in the same theorem.

In PR #550 I mentioned that having local scoped instances that could depend on section variables would solve all of our problems. This PR started by trying to emulate that, and morphed into something I think might make everybody 99%-100% happy.

Technique

The trick is to have a special typeclasss for default SIdx implementations, and let that provide the default instance for SIdx. Concretely, we start with the normal implicit index version of OFE (similar for COFE, OFunctor, etc)

@[rocq_alias ofe]
class OFE {SI : Type _} [SIdx SI] (α : Type _) where
  Dist : SI → α → α → Prop
  -- ... 

however, we add a second class for declaring default step index instances with an outParam index, and let it be the default instance for the step index type

class DefaultSI (SI : outParam (Type u)) where
  private mk ::
  sidx : SIdx SI

@[default_instance, reducible]
def dfltSIdx {SI : Type u} [d : DefaultSI SI] : SIdx SI := d.sidx

Initially I did this to put different defaults in different scopes (so we could emulate scoped default instances) but I ended up deciding that an elab was a better way to control it. To that end, the stepindex command puts an instance of the DefaultSI class in scope and does some sanity checks to ensure that it is the only one at elab time:

scoped stepindex Nat

Ordinal works in IrisMath too. Unlike hypothetical scoped default instances, the default index type can depend on section variables, so your type of step indices can be defined from any other Lean machinery. Here is how to generalize a section to use a generic type of step indices, such that the SIdx constraint on the type obeys normal typeclass synthesis rules:

variable {SI : Type _} [SIdx SI]
local stepindex SI

Hierarchy

Once a stepindex is declared, a DefaultSI instance is put in scope, so plain typeclass synthesis will handle filling in the SI type and the [SIdx SI] instances almost exactly like it would have in the outParam approach. However there are big improvements related to how stable this is. Synthesis of this arbitrary instance happens once, at the time the local stepindex command is elab'd, and in the happy path this instance will be synthesized using normal synthesis rules (ie. it will pick the [SIdx SI] instance for SI in the snippet above, not an arbitrary instance at every call site). Even if you get it wrong, and break the hierarchy in some other way, because synthesis happens only once this choice is guaranteed to be self-consistent for the rest of the section.

I've also used the module system to make declaring DefaultSI instances out of band difficult. It's not impossible, I don't think, but I think it is very hard to do by accident. Compare this to accidentally copy-pasting an open statement that includes a scope you didn't realize had a SIdx instance in it: the latter is much easier to get wrong. Setting a global step index type is also disabled.

Basic hierarchy discipline like avoiding non-definitional diamonds still applies of course, but it is no longer possible for an unrelated SIdx typeclass instance to break unrelated OFE synthesis. The stepindex command also has an option to provide an explicit instance name: I suspect this is unnecessary, but it gives you to turn off even the command elab-time nondeterminism if such a need arises. The final hierarchy thing I'll mention is that, unlike the outParam appraoches, the default step indices are fully opt-in. I can use OFE/COFE/OFunctor/... without interacting setting default step indices at all, and it behaves like any other two-parameter Lean typeclass. You can use multiple index types in the same theorem if you want. The stability of our algebraic hierarchy is preserved with or without default step indices set.

Notations

Because of the outParam, you get nearly every nicety of Alvin's original approaches, and adapting old code is nearly zero work. OFE does not need to specify its SI parameter, and step indices (even 0) do not need to specify their type. I also reused the stepindex% term elab from my other attempt: this elaborates to the current default step index type or a hole if none is set. However, unlike my prior approach, this is not used for any odd optParam stuff: it's used a total of three times so that we can have reuse the old notation and make it fill in the SI type at its elab site:

scoped notation:40 x " ≡{" n "}≡ " y:41 => OFE.Dist n x y (SI := stepindex%)

Doing it this way avoids the need for the duplicate scoped notations as in Mario's version. I didn't even define notation for adding in the type SI because I think at this point it is not necessary.

Limitation

The only edge case I could not resolve was for eq_dist (and a few one-offs like it): this is an annoying combination of both not being a notation, and not referencing the step index type, so neither typeclass inference nor an optParam can be used to fill the type. Admittedly, the outParam version (nondetermistically) fills this in at every call site where mind doesn't.

In this PR I explicitly fixed each SI instance be Nat or SI as at each call site so that the Lean code looks normal. I could have also defined eq_dist' to be eq_dist fixing (SI := stepindex%) with an optParam and it would be uniform across the repo. I'm open to any solutions to this problem.

Conclusion

I hope this version meets everybody's requirements! I think this solution is more than the sum of its parts, and cards on the table, I'm very pleased with it. To me it feels quite inline with other Lean features. I look forward to your feedback :)

cc: @alvinylt @MackieLoeffel @Kaptch @digama0

alvinylt added 30 commits July 28, 2026 11:51
Replace all proofs in section `Fixpoint`, all requires rewrite
Parametrisation of `CMRA` to be done in a future PR
@markusdemedeiros

Copy link
Copy Markdown
Collaborator Author

Not for nothing: a similar design might be capable of removing the \vdash@{IProp GF} annotations as well. Happy to prototype this if people find the idea acceptable.

@MackieLoeffel MackieLoeffel left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for working on this! I think this design looks great and solves all the problems. I like the idea of the DefaultSIdx typeclass with the default instance. We can indeed explore if we want to use a similar design for other cases. I left some small comments (partly also for @alvinylt, who wrote the original version). I also hope to have a look that is a bit deeper later to see if we can clean up things a bit more, but overall this looks great.
Thanks again for all your work on this! It took a bit of time, but in the end we now have a great solution that addresses all problems, which I think is very nice.

Comment thread Iris/Iris/Algebra/OFE.lean Outdated
Comment thread Iris/Iris/Algebra/OFE.lean Outdated
Comment thread Iris/Iris/Algebra/OFE.lean
Comment thread Iris/Iris/Algebra/StepIndex.lean
@Kaptch

Kaptch commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Nice! I've taken a look, and for me it was a matter of just rebasing and fixing a few annotations on top of this PR.


namespace DFracAgree

local stepindex Nat

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It is a bit unclear for me where to put these local stepindex Nat commands in a file. It seems the clearest version for me would be to put the local stepindex Nat command at the top of the file directly after the imports. Then it is clear that it applies to the whole file and not just some namespace or so. What do you think about this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The stepindex command follows the scoping rules (in the literal sense: the local translates to a local instance of the DefaultSIdx typeclass). So, as written here it does just apply until end DFracAgree, but it also works to move it right after the imports and it will apply to the whole file.

Which would you prefer? The only hard constraint is that for generic SI, the stepindex annotation has to come after the section variable.

Comment thread Iris/Iris/Algebra/COFESolver.lean Outdated
compl c := by
refine ⟨fun k => compl ⟨fun i => c.1 i k, fun h => c.cauchy h k⟩, ?_⟩
refine OFE.eq_dist.mpr (fun n => ?_)
refine (OFE.eq_dist (SI := Nat)).mpr (fun n => ?_)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I was not able to figure out, why eq_dist needs a type annotation, but basically nothing else needs it. Does anyone have an idea why?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It seems to be because SIdx is not an instance parameter for eq_distandDist`. Not sure how to fix this. Maybe something like the following helps?

@[rocq_alias ofe]
class OFE {SI : Type _} [SIdx SI] (α : Type _) where
  Dist' : SI → α → α → Prop
  dist_eqv : Equivalence (Dist' n)
  eq_dist' : x = y ↔ ∀ n, Dist' n x y
  dist_lt : Dist' n x y → m < n → Dist' m x y

/-- Thin abbreviation for `OFE (SI := SI) α`. -/
class abbrev IOFE (SI : Type _) [SIdx SI] (α : Type _) := OFE (SI := SI) α

abbrev OFE.Dist {SI} [SIdx SI] {α} [self : IOFE SI α] : SI → α → α → Prop :=
  OFE.Dist'

abbrev OFE.eq_dist {SI} [SIdx SI] {α} [self : IOFE SI α] {x y : α} :
  x = y ↔ ∀ n, OFE.Dist (SI:=SI) n x y := OFE.eq_dist'

Not sure if there is another way to change the status of implicit parameters.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This looks to be promising! I will propagate it throughout the rest of the PR and remove those annotations :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hm. It doesn't work in all cases, sadly. I can get the annotation to be OFE.eq_dist _ using an autoParam (this version at least doesn't have to change when the local stepindex type gets generalized) but the underscore can't be removed without implicit autoparams, which are not planned.

I'll commit this work once my agent finishes propagating the changes but I could take it or leave it.

/-- Ordered family of equivalences -/
@[rocq_alias ofe]
class OFE {SI : Type _} [SIdx SI] (α : Type _) where
  Dist : SI → α → α → Prop
  dist_eqv : Equivalence (Dist n)
  eq_dist' : x = y ↔ ∀ n, Dist n x y
  dist_lt : Dist n x y → m < n → Dist m x y

/-- Thin abbreviation for `OFE (SI := SI) α`. -/
class abbrev IOFE (SI : Type _) [SIdx SI] (α : Type _) := OFE (SI := SI) α

open OFE

scoped notation:40 x " ≡{" n "}≡ " y:41 => OFE.Dist n x y (SI := stepindex%)

abbrev OFE.eq_dist (SI := by infer_stepindex) [SIdx SI] {α} [self : IOFE SI α] {x y : α} :
  x = y ↔ ∀ n, OFE.Dist (SI:=SI) n x y := OFE.eq_dist'

@markusdemedeiros markusdemedeiros Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Even this does't get rid of all annotations, sadly. Latest commit still fixes a couple of them to Nat.

This version is still less characters (it is clear form the type that you must supply the step index type somehow, as it is not marked implicit) but if we wanted to disable the autoparam altogether for consistency I think that would be fine too.

@MackieLoeffel

Copy link
Copy Markdown
Collaborator

!bench

@leanprover-radar

leanprover-radar commented Aug 10, 2026

Copy link
Copy Markdown

Benchmark results for f68356e against 69dee49 are in. There are significant results. @MackieLoeffel

  • 🟥 build//instructions: +106.8G (+6.23%)

Large changes (5🟥)

  • 🟥 build/module/Iris.Algebra.CMRA//instructions: +6.2G (+29.84%)
  • 🟥 build/module/Iris.Algebra.COFESolver//instructions: +12.8G (+100.28%)
  • 🟥 build/module/Iris.Algebra.OFE//instructions: +24.1G (+125.32%)
  • 🟥 build/module/Iris.Examples.Fix//instructions: +5.6G (+121.03%)
  • and 1 hidden

Medium changes (17🟥)

  • 🟥 build/module/Iris.Algebra.Agree//instructions: +1.9G (+24.16%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.Algebra.Csum//instructions: +2.6G (+12.67%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.Algebra.Excl//instructions: +1.1G (+25.61%)
  • 🟥 build/module/Iris.Algebra.GenMap//instructions: +1.1G (+13.00%)
  • 🟥 build/module/Iris.Algebra.Heap//instructions: +2.5G (+18.42%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.Algebra.HeapView//instructions: +1.5G (+8.34%)
  • 🟥 build/module/Iris.Algebra.IProp//instructions: +1.3G (+37.48%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.Algebra.View//instructions: +2.5G (+11.04%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.BI.InternalEq//instructions: +1.9G (+28.66%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.BI.Lib.FixpointBanach//instructions: +1.4G (+44.52%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.BI.MonPred//instructions: +1.2G (+8.48%)
  • 🟥 build/module/Iris.Examples.IProp//instructions: +1.4G (+28.72%)
  • 🟥 build/module/Iris.Instances.IProp.Instance//instructions: +2.7G (+16.79%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.Instances.Lib.Boxes//instructions: +2.8G (+13.81%)
  • 🟥 build/module/Iris.Instances.Lib.GhostMap//instructions: +3.3G (+7.68%)
  • 🟥 build/module/Iris.Instances.Lib.SavedProp//instructions: +2.8G (+42.45%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.Instances.Lib.WSat//instructions: +1.4G (+17.22%)

Small changes (2✅, 32🟥)

  • 🟥 build/module/Iris.Algebra.Auth//instructions: +714.3M (+9.26%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.Algebra.BigOp//instructions: +677.9M (+6.10%)
  • 🟥 build/module/Iris.Algebra.DFrac//instructions: +352.9M (+2.13%)
  • 🟥 build/module/Iris.Algebra.DynReservationMap//instructions: +513.5M (+5.49%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.Algebra.Functions//instructions: +351.1M (+11.13%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.Algebra.Lib.DFracAgree//instructions: +270.2M (+8.89%)
  • 🟥 build/module/Iris.Algebra.Lib.ExclAuth//instructions: +296.3M (+9.06%)
  • 🟥 build/module/Iris.Algebra.Lib.FracAuth//instructions: +226.7M (+3.63%)
  • 🟥 build/module/Iris.Algebra.Lib.UFracAuth//instructions: +253.5M (+4.70%)
  • 🟥 build/module/Iris.Algebra.LocalUpdates//instructions: +151.5M (+3.51%)
  • 🟥 build/module/Iris.Algebra.Monoid//instructions: +98.6M (+4.58%)
  • 🟥 build/module/Iris.Algebra.Numbers//instructions: +337.4M (+9.14%)
  • 🟥 build/module/Iris.Algebra.ReservationMap//instructions: +571.0M (+7.27%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.Algebra.StepIndex//instructions: +1.9G (+52.60%) (reduced significance based on *//lines)
  • 🟥 build/module/Iris.Algebra.UPred//instructions: +394.7M (+12.62%) (reduced significance based on *//lines)
  • 🟥 build/module/Iris.BI.BI//instructions: +193.2M (+5.55%)
  • 🟥 build/module/Iris.BI.BigOp.BigSepList//instructions: +397.9M (+2.36%)
  • 🟥 build/module/Iris.BI.Cmra//instructions: +157.5M (+4.36%)
  • 🟥 build/module/Iris.BI.DerivedLaws//instructions: +820.7M (+3.83%) (reduced significance based on absolute threshold)
  • 🟥 build/module/Iris.BI.DerivedLawsLater//instructions: +304.3M (+3.47%)
  • and 14 more

@MackieLoeffel

Copy link
Copy Markdown
Collaborator

Seems like there is some performance hit from this change. This was expected since Rocq had a similar performance hit (if not worse) for the corresponding PR. Not sure if there is something we can do about this.

@markusdemedeiros

markusdemedeiros commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

My best guess as to any unnecessary negative perf would be the additional typeclasses not being private to the modules. Let me try making them private (forcing all stepindex annotations to be local instead of scoped) so there are fewer typeclass instances added. I recall trying this at some point but I can't quite remember what went wrong: let me try this again to see if it works/if we can measure it.

@markusdemedeiros

Copy link
Copy Markdown
Collaborator Author

Ah right, that prevents OFE from being used in public signatures, so the instances of DefaultSI can't be made module private.

@markusdemedeiros

Copy link
Copy Markdown
Collaborator Author

@MackieLoeffel Do you think the performance should be a blocker for this?

@markusdemedeiros markusdemedeiros mentioned this pull request Aug 11, 2026
2 tasks
@MackieLoeffel

Copy link
Copy Markdown
Collaborator

No, I don't think the performance should be a blocker for this. If there is some easy win, we can go for it, but otherwise I think the performance regression is fine. We don't focus much on performance at the moment and if someone has a more efficient implementation of this in the future, we can still improve it.

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.

5 participants