Documentation

GeneralizationLinter.Analysis.Collect

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:

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 m

Now, let sig be the Expr corresponding to {R M : Type*} [Ring R] [AddCommGroup M] [Module R M] (r : R) (m : M) : r • (m + m) = r • m + r • m, and let proof be the Expr corresponding to smul_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₃], where inst₁ is the "canonical" fvar of the Ring R instance, inst₂ that of the AddCommGroup M instance, and inst₃ that of the Module R M instance.

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's forallTelescope sig produces the binder fvars R' M' inst₁' inst₂' inst₃' r' m'. Then binderIdOf is the bijection {inst₁' ↦ inst₁, inst₂' ↦ inst₂, inst₃' ↦ inst₃} from getMIChains's targeted binder fvars inst₁' inst₂' inst₃' to their matching BinderIds.

After this bijection is established, getMIChains then walks

Note: A 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 getMIChains will 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', and M'.
  • "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:

  1. The subterm is an argument in an application.
  2. 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. collect then asks whether this application is a tributary junction (i.e., whether one class-typed argument pins all the others) by consulting sourceArg?:

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 MIChains are

  • { head := Monoid R', root := inst₁ } (4 times)
  • { head := Semiring R', root := inst₁ } (5 times)
  • { head := Add M', root := inst₂ } (2 times)
  • { head := AddCommMonoid M', root := inst₂ } (5 times)
  • { head := AddMonoid M', root := inst₂ } (4 times)
  • { head := AddZeroClass M', root := inst₂ } (4 times)
  • { head := Zero M', root := inst₂ } (3 times)
  • { head := DistribSMul R' M', root := inst₃ } (1 time)
  • { head := SMul R' M', root := 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.

Instances For

    Targeted binder in a declaration.

    Instances For

      Get the Name of the class head of a TargetedBinder's type (δ-reduced by whnf).

      Equations
      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:

        {
          root := ⟨`_uniq.123⟩, -- TargetedBinder.id 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.

        • root : BinderId

          The TargetedBinder.id corresponding to the instance at the root of the chain.

        Instances For

          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 and Requirements 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 Requirement is associated with a specific TargetedBinder is 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 minimal upper bounds, 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
            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 Monoid α.

          Instances For

            Returns true if binder is class-typed and instance-implicit. If generalizeTypeclasses.targetImplicit is true (which it is by default), then isTargetedBinder also returns true if binder is class-typed and implicit or strict implicit.


            Implementation notes

            Implicit and strict-implicit binders are usually not class-typed, so the isClass? check is what keeps them out when generalizeTypeclasses.targetImplicit is true. For instance-implicit binders the check is mostly redundant, since instance-implicit arguments are pretty much always classes (which is "enforced" by the default-on checkBinderAnnotations linter), but they're not technically required to be (Mathlib has one example, CategoryTheory.Bundled.of, though there the intention is still that the binder should be class-typed, so it's not even really a bona fide counter-example).

            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.

                  Note: If any of the conclusion's universe parameters are absent from the source slot's type and present in the type of one of the other arguments, none is returned.


                  Examples

                  -- instance CommGroup.toGroup {G : Type u} [self : CommGroup G] : Group G
                  sourceSlot? `CommGroup.toGroup = some 1
                  -- instance IsStrictOrderedRing.toIsOrderedRing {R} [Semiring R] [PartialOrder R]
                  --   [IsStrictOrderedRing R] : IsOrderedRing R
                  sourceSlot? `IsStrictOrderedRing.toIsOrderedRing = some 3
                  sourceSlot? `map_one = none -- conclusion isn't class-typed
                  -- instance instAddNat : Add Nat
                  sourceSlot? `instAddNat = none -- no arguments
                  -- instance Prod.instMonoid {M N} [Monoid M] [Monoid N] : Monoid (M × N)
                  sourceSlot? `Prod.instMonoid = none -- `Monoid N` doesn't contain `M`
                  -- instance Pi.monoid {I : Type u} {f : I → Type v} [∀ i, Monoid (f i)] : Monoid (∀ i, f i)
                  sourceSlot? `Pi.monoid = none -- `u` is absent from the source's type but present in `I`'s
                  
                  Equations
                  • One or more equations did not get rendered due to their size.
                  Instances For

                    Collect all MIChains from decl and proof.

                    Equations
                    • One or more equations did not get rendered due to their size.
                    Instances For

                      Convert an array of MIChains into an array of Requirements, filtering out MIChains that aren't rooted at one of the targeted binders.

                      Equations
                      • One or more equations did not get rendered due to their size.
                      Instances For