Collect #
Preliminaries #
We use the term "instance transformers" (or simply "transformers") to refer to instances which map
one or more instances to another instance. For example, CommGroup.toGroup is an instance
transformer. An "instance transformation" (or simply "transformation"), meanwhile, will refer to an
application of an instance transformer, e.g. @CommGroup.toGroup G inst. Note that both instance
transformers and instance transformations are themselves instances; the former are declared as such,
while the latter are instance-valued expressions. To disambiguate, we will refer to instances that
aren't instance transformers or instance transformations as "root instances".
Oftentimes, transformations may be nested:
Weak.toWeakest α (Strong.toWeak α (Strongest.toStrong α ⟨inst of Strongest on α⟩))
└──────── root ────────┘
These nested transformations often involve "junctions": transformations that have ≥2 class-typed
args. Sometimes, these junctions are bona-fide confluences, as is the case for e.g.
Prod.instMonoid {M : Type u} {N : Type v} [Monoid M] [Monoid N] : Monoid (M × N), where the
conclusion isn't just a "projection" of one of the class-typed args. In many other cases, the
junctions are such projections, however (if not technically then at least in spirit). We call
junctions of this second kind tributary junctions, borrowing from hydrology again, and
distinguish two sub-categories:
Unconditional tributary junctions (UTJ) (projections): When one (and only one) of the class-typed args contains all other args in its type, then that single class-typed arg "pins" (or "fixes") the values of all other arguments (no matter their annotation) through unification, i.e., it's the sole degree of freedom. As such, when we have, for example, an instance of
IsStrictOrderedRing R— or, written more explicitly, an instanceinst₃ : @IsStrictOrderedRing.{u} R inst₁ inst₂, whereinst₁ : Semiring.{u} Randinst₂ : PartialOrder.{u} R— then that instance alone, through its type, pins all the other arguments (including universe levels) of, for example, the transformationIsStrictOrderedRing.toIsOrderedRing:instance IsStrictOrderedRing.toIsOrderedRing.{u} {R : Type u} [Semiring R] -- [inst₁ : Semiring.{u} R] [PartialOrder R] -- [inst₂ : PartialOrder.{u} R] [IsStrictOrderedRing R] : -- [inst₃ : @IsStrictOrderedRing.{u} R inst₁ inst₂], IsOrderedRing R -- @IsOrderedRing.{u} R inst₁ inst₂Hence, we treat
IsStrictOrderedRing.toIsOrderedRingas a mere projection ofIsStrictOrderedRing R: hand someoneinst₃, and they can takeIsStrictOrderedRing.toIsOrderedRing, fill out all other argument slots, and give you an instance ofIsOrderedRing R.We call
inst₃the "source" of the transformation (it's what the projection is "forgetting from"). We call class-typed arguments other than the source "tributaries".- NB: In auto-generated projections (
IsStrictOrderedRing.toIsOrderedRingis not one),inst₃would be calledself. See e.g.IsStrictOrderedRing.toPosMulStrictMono.
- NB: In auto-generated projections (
Conditional tributary junctions (CTJ): When we add one or more
Propmixins to what would otherwise have been an unconditional tributary junction, we get a conditional tributary junction. Here, we have that, under the assumption that the mixins are satisfied, this junction is also essentially a projection in spirit. Nonetheless, CTJ's are out of scope for now. Examples of CTJ's includeLeftCancelMonoid.groupOfFiniteCTJs would need tie-breakers. For example:
IsNoetherianRing.wfDvdMonoid.{u} : forall {R : Type.{u}} [inst₁ : CommSemiring.{u} R] [inst₂ : IsDomain.{u} R (CommSemiring.toSemiring.{u} R inst₁)] [h : IsNoetherianRing.{u} R (CommSemiring.toSemiring.{u} R inst₁)], WfDvdMonoid.{u} R (CommSemiring.toCommMonoidWithZero.{u} R inst₁)here, if we removed either the
IsDomain RorIsNoetherianRing Rmixins, we'd have a UTJ (as well as an impossible-to-prove signature, but that's beside the point) withhorinst₂as sources, respectively. However, the signature per se doesn't tell us whether we should treatIsDomain Ras the mixin orIsNoetherianRing R. One sensible heuristic would be to check the class names of the types against the namespace of the transformation, which in this case would pickhas the source and designateinst₂as the mixin.CTJs can be tricky to detect reliably. Take the following non-example:
LeftCancelMonoid.groupOfFinite.{u} : forall {G : Type.{u}} [inst₁ : LeftCancelMonoid.{u} G] [inst₂ : Finite.{succ u} G], Group.{u} GHere, if we adopted the namespace tie-breaker we suggested before,
inst₁would be chosen as the source, andinst₂would be chosen as the mixin. But to callLeftCancelMonoid.groupOfFinitea projection fromLeftCancelMonoid GtoGroup Gconditioned onFinite Gwould flow in the opposite direction of strength, asGroup Gis strictly stronger thanLeftCancelMonoid G(ifinst : Group G, then@CancelMonoid.toLeftCancelMonoid G (@Group.toCancelMonoid G inst)yields an instance ofLeftCancelMonoid G).Whether this would inherently be a problem further down the road is hard to say, but it contradicts our intent and hence motivates us to shelf this feature for now.
This module #
This module concerns itself with finding out what classes a declaration actually uses each of its targeted binders as (which we'll use to set the requirements that any weakening of that binder must still satisfy).
We use the following artificial declaration and proof as a guiding example:
theorem thm {R M : Type*} [Ring R] [AddCommGroup M] [Module R M] (r : R) (m : M) : r • (m + m) = r • m + r • m := smul_add r m mNow, let
sigbe theExprcorresponding to{R M : Type*} [Ring R] [AddCommGroup M] [Module R M] (r : R) (m : M) : r • (m + m) = r • m + r • m, and letproofbe theExprcorresponding tosmul_add r m m.
For a given pair sig proof : Expr, where proof is the proof of sig, we begin by calling
getTargetedBinders sig, which will assign each targeted (class) binder of sig an fvar, which
we'll treat as the targeted binder's "canonical" fvar. To encode this "canonicity", we create the
wrapper type BinderId around FVarId. These fvars are returned within an array of
TargetedBinders, each containing a BinderId. The array obeys the order in which the targeted
binders appear in sig. Call that array tbinders.
In our example,
tbinders = #[inst₁, inst₂, inst₃], whereinst₁is the "canonical" fvar of theRing Rinstance,inst₂that of theAddCommGroup Minstance, andinst₃that of theModule R Minstance.
Next, we call getMIChains tbinders sig proof. Now, getMIChains will telescope sig once more,
generating fresh fvars for all binders. However, given that it's the same sig as was passed to
getTargetedBinders, we can define a bijection between the targeted binder fvars from
getTargetedBinders and the corresponding fvars generated by the telescope in getMIChains by
using the fvars' positional indices within sig. We store this bijection as binderIdOf.
In our example, say that
getMIChains'sforallTelescope sigproduces the binder fvarsR' M' inst₁' inst₂' inst₃' r' m'. ThenbinderIdOfis the bijection{inst₁' ↦ inst₁, inst₂' ↦ inst₂, inst₃' ↦ inst₃}fromgetMIChains's targeted binder fvarsinst₁' inst₂' inst₃'to their matchingBinderIds.
After this bijection is established, getMIChains then walks
- every binder's type,
- the
sig's conclusion, and - the proof term "syntactically applied" to
getMIChains's telescope and β-reduced (the β-reduction is what transforms the syntactic application@proof R' M' inst₁' inst₂' inst₃' r' m'into anExprofproofwhere all its binder bvars are replaced with the corresponding fvars fromgetMIChains's telescope).
Note: An targeted binder only appearing within another targeted binder's type, and nowhere else, does not mean that it is superfluous.
In our example, the things
getMIChainswill walk look like this:
- "every binder's type":
Type*,Type*,Ring R',AddCommGroup M',@_root_.Module R' M' (@Ring.toSemiring R' inst₁') (@AddCommGroup.toAddCommMonoid M' inst₂'),R, andM.- "the
sig's conclusion":@Eq M' (...) (...)(very long, with plenty of instance chains).- "the proof term applied to
getMIChains's telescope and β-reduced":@smul_add R' M' (@AddMonoid.toAddZeroClass M' (@SubNegMonoid.toAddMonoid M' (@AddGroup.toSubNegMonoid M' (@AddCommGroup.toAddGroup M' inst₂')))) (@DistribMulAction.toDistribSMul R' M' (@Semiring.toMonoid R' (@Ring.toSemiring R' inst₁')) (@SubNegMonoid.toAddMonoid M' (@AddGroup.toSubNegMonoid M' (@AddCommGroup.toAddGroup M' inst₂'))) (@Module.toDistribMulAction R' M' (@Ring.toSemiring R' inst₁') (@AddCommGroup.toAddCommMonoid M' inst₂') inst₃')) r' m' m'
walk is defined inductively over every possible Expr constructor. Its job is to find any spot
where the elaborator put an instance transformation or root instance. It recurses through function
applications, projections, lets, and fun and ∀ abstractions. In the case of fun and ∀
abstractions, it walks the domain type first, and then the body, with the abstracted bvar within it
instantiated to a fresh fvar of the right type, since walk can't query the type of anything that
contains a loose bvar.
As it recurses, there are exactly two spots at which walk will pass a subterm to route:
- The subterm is an argument in an application.
- The subterm is the base of a projection (
.proj).
Once walk passes a subterm e to route, route decides what to do next based on the type of
the subterm. If it's class-typed, it runs collect e none, starting a new chain. If it's not
class-typed, route hands e back to walk to keep recursing on.
Assuming that the subterm is an instance chain, collect will record what class the chain is an
instance of (i.e., the head of the subterm's outermost class application) and carry it with it as
chainHead? as it descends the chain. At every instance transformation (e.g.
Module.toDistribMulAction ...), collect will call walk on all non-class-typed arguments to
ensure any instances that might have been involved in the construction of the non-class-typed
argument are still taken into account. With regards to class-typed arguments, we differentiate
between two main cases:
Only 1 class-typed argument:
collectcontinues the current chain by descending into the class-typed argument.≥2 class-typed arguments (junction):
collecttries to figure out what kind of junction this is by callingsourceArg?:sourceArg?returnssome ...:collectcontinues the current chain by descending into the source argument, and starts new chains at every other class-typed argument. This happens when the junction is an unconditional tributary junction.sourceArg?returnsnone:collectdrops the current chain and starts new chains at each class-typed argument. This happens when the junction is a confluence, and currently also when it is a conditional tributary junction.
Once the MIChains have been collected, getMIChains returns them. We then pass the chains to
getReqs, which associates each chain to the corresponding TargetedBinder. It then converts each
chain to a Requirement, and returns the requirements as an array.
In our example, the collected
MIChainsare
{ head := Monoid R', inst := inst₁' }(4 times){ head := Semiring R', inst := inst₁' }(5 times){ head := Add M', inst := inst₂' }(2 times){ head := AddCommMonoid M', inst := inst₂' }(5 times){ head := AddMonoid M', inst := inst₂' }(4 times){ head := AddZeroClass M', inst := inst₂' }(4 times){ head := Zero M', inst := inst₂' }(3 times){ head := DistribSMul R' M', inst := inst₃' }(1 time){ head := SMul R' M', inst := inst₃' }(3 times)
Structure to help differentiate between the FVarId assigned to a binder by the telescope inside
getTargetedBinders and any other FVarIds that other telescopes (specifically, that of getMIChains)
may assign to the same binder.
- fvar : Lean.FVarId
The
FVarIdassigned to the binder by the telescope insidegetTargetedBinders.
Instances For
Equations
Equations
- GeneralizationLinter.instBEqBinderId.beq { fvar := a } { fvar := b } = (a == b)
- GeneralizationLinter.instBEqBinderId.beq x✝¹ x✝ = false
Instances For
Instances For
Equations
Targeted binder in a declaration.
- fvar : BinderId
fvar ID generated for this binder when
getTargetedBinderstelescopes the declaration. - idx : Nat
Binder's 0-indexed position among the declaration's targeted binders. This allows
ReSynth.rebuildWeakenedto locate the binder despite re-telescoping the declaration with fresh fvars. - binderInfo : Lean.BinderInfo
Binder's annotation.
Instances For
Get Name of class head of a TargetedBinder's type.
Instances For
Maximal instance chain.
Example
Suppose we have [inst : Group α] and a maximal instance chain @DivInvMonoid.toMonoid α (@Group.toDivInvMonoid α inst). Then the corresponding MIChain record for this chain would be:
{
inst := ⟨`_uniq.123⟩, -- TargetedBinder.fvar of `inst`
head := {
name := `Monoid,
pattern := #[.bvar 0],
levels := .polymorphic, -- or `.concrete #[…]`
subst := #[.fvar ⟨`_uniq.124⟩] -- array containing fvar `α`
}
}
- head : Key
The "resulting" class application of the chain.
- inst : BinderId
The
TargetedBinder.fvarcorresponding to the instance at the root of the chain.
Instances For
Equations
A minimal requirement that a proof or statement imposes on a specific targeted binder of the statement.
Implementation notes
- For any given declaration, there's a simple bijection between the
MIChains andRequirements associated with the declaration, and the two structures contain essentially the same data. The structures are kept separate to aid modularity and future extensibility of the binders-and-requirements collection pipeline. - The fact that each
Requirementis associated with a specificTargetedBinderis technically an unnecessary restriction on the kinds of weakenings the linter can suggest, but it simplifies the search for weakenings from general abduction to computing some meets, and we find that it doesn't reduce the quality of the suggestions too much.
Example
Suppose we have a theorem theorem thm {α} [inst : Group α] ... : ... := .... Suppose that b : TargetedBinder corresponds to the instance-implicit binder [Group α], and that the proof term of
thm includes the maximal instance chain @DivInvMonoid.toMonoid α (@Group.toDivInvMonoid α inst).
Then the corresponding Requirement record for this chain would be:
{
binder := { … }, -- b.fvar
name := `Monoid,
pattern := #[.bvar 0],
levels := .polymorphic, -- or `.concrete #[…]`
subst := #[.fvar ⟨`_uniq.124⟩] -- array containing fvar `α`
}
Intuitively, this Requirement record is expressing that the instance-implicit binder b must be
able to provide an instance of Mul α.
- binder : TargetedBinder
Instances For
Returns true if the local declaration is an instance-implicit class binder, and false otherwise.
Implementation notes
Instance-implicit binders are almost always classes, and this is enforced by the default-on
checkBinderAnnotations, but they're not technically required to be (see e.g.
Mathlib.CategoryTheory.Bundled.of), so we check out of an abundance of caution.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Given type of the form forall xs, A, extract the targeted binders of xs as local declarations
lds and run k lds A.
Example
targetedBinderTelescope "{R} [CommRing R] {K} [Field K] → C" k will run k #["CommRing R", "Field K"] "C", where "{R} [CommRing R] {K} [Field K] → C" and "C" are to be understood as Exprs and
"CommRing R" and "Field K" as LocalDecls.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Get the targeted binders of a declaration.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Get index of source slot for a given declaration. Doing this once for each declaration and memoizing the result makes descent through instance chains a lot faster.
Equations
- One or more equations did not get rendered due to their size.
Instances For
#TODO
Equations
- One or more equations did not get rendered due to their size.
Instances For
#TODO
Equations
- One or more equations did not get rendered due to their size.