Documentation

GeneralizationLinter.Core.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: 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 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. With regards to class-typed arguments, we differentiate between two main cases:

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', 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.

Instances For

    Targeted binder in a declaration.

    Instances For

      Get Name of class head of a TargetedBinder's type.

      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:

        {
          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.fvar 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 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 α.

          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.
                      Instances For