| # Dart 2.X super mixin inference proposal |
| |
| **Owner**: leafp@google.com |
| |
| **Status**: This is now background material. |
| |
| The current version of this document now resides |
| [here](https://github.com/dart-lang/language/blob/master/working/0006.%20Super-invocations%20in%20mixins/0007.%20Mixin%20declarations/mixin-inference.md). |
| |
| This is intended to define a prototype approach to supporting inference of type |
| arguments in super-mixin applications. This is not an official part of Dart |
| 2, but is an experimental feature hidden under flags. When super-mixins are |
| specified and landed, this feature or some variant of it may be included. |
| |
| ## Syntactic conventions |
| |
| The meta-variables `X`, `Y`, and `Z` range over type variables. |
| |
| The meta-variables `T`, and `U` range over types. |
| |
| The meta-variables `M`, `I`, and `S` range over interface types (that is, |
| classes instantiated with zero or more type arguments). |
| |
| The meta-variable `C` ranges over classes. |
| |
| The meta-variable `B` ranges over types used as bounds for type variables. |
| |
| Throughout this document, I assume that bound type variables have been suitably |
| renamed to avoid accidental capture. |
| |
| ## Mixin inference |
| |
| In Dart 2 syntax, a class definition may also have an interpretation as mixin |
| under certain restrictions defined elsewhere. |
| |
| ### Mixins and superclass constraints |
| |
| Given a class of the form: |
| |
| ``` |
| class C<X0, ..., Xn> extends S with M0, ..., Mj implements I0, ..., Ik { ...} |
| ``` |
| |
| we say that the superclass of `C<X0, ..., Xn>` is `S with M0, ..., Mj`. |
| |
| When interpreted as a mixin, we say that the super class constraints for `C<X0, |
| ..., Xn>` are `S`, `M0`, ..., `Mj`. |
| |
| Given a mixin application class of the form: |
| |
| ``` |
| class C<X0, ..., Xn> = S with M0, ..., Mj implements I0, ..., Ik; |
| ``` |
| |
| for we say that the superclass of `C<X0, ..., Xn>` is `S with M0, ..., Mj-1`. |
| |
| When interpreted as a mixin, we say that the super class constraints for `C<X0, |
| ..., Xn>` are `S`, `M0`, ..., `Mj-1`. |
| |
| #### Discussion |
| |
| A class, interpreted as a mixin, is interpreted as a function from its |
| superclass to its own type. That is, the actual class onto which the mixin is |
| applied must be a subtype of the superclass constraint. When the superclass is |
| itself a mixin, we interpret each component of the mixin as a separate |
| constraint, each of which the actual class onto which the mixin is applied must |
| be a subtype of. |
| |
| ### Mixin type inference |
| |
| Given a class of the form: |
| |
| ``` |
| class C<T0, ..., Tn> extends S with M0, ..., Mj implements I0, ..., Ik { ...} |
| ``` |
| |
| or of the form |
| |
| ``` |
| class C<T0, ..., Tn> = S with M0, ..., Mj implements I0, ..., Ik; |
| ``` |
| |
| we say that the superclass of `M0` is `S`, the superclass of `M1` is `S with |
| M0`, etc. |
| |
| For a class with one or more mixins of either of the forms above we allow any |
| or all of the `M0`, ..., `Mj` to have their type arguments inferred. That is, |
| if any of the `M0`, ..., `Mj` are references to generic classes with no type |
| arguments provided, the missing type arguments will be attempted to be |
| reconstructed in accordance with this specification. |
| |
| Type inference for a class is done from the innermost mixin application out. |
| That is, the type arguments for `M0` (if any) are inferred before type arguments |
| for `M1`, and so on. Each successive inference is done with respect to the |
| inferred version of its superclass: so if type arguments `T0, ..., Tn` are |
| inferred for `M0`, `M1` is inferred with respect to `M0<T0, ..., Tn`>, etc. |
| |
| Type inference for the class hierarchy is done from top down. That is, in the |
| example classes above, all mixin inference for the definitions of `S`, the `Mi`, |
| and the `Ii` is done before mixin inference for `C` is done. |
| |
| Let be `M` be a mixin applied to a superclass `S` where `M` is a reference to a |
| generic class with no type arguments provided, and `S` is some class (possibly |
| generic); and where `M` is defined with type parameters `X0, ..., Xj`. Let `S0, |
| ..., Sn` be the superclass constraints for `M` as defined above. Note that the |
| `Xi` may appear free in the `Si`. Let `C0, ..., Cn` be the corresponding |
| classes for the `Si`: that is, each `Si` is of the form `Ci<T0, ..., Tk>` for |
| some `k >= 0`. Note that by assumption, the `Xi` are disjoint from any type |
| parameters in the enclosing scope, since we have assumed that type variables are |
| suitably renamed to avoid capture. |
| |
| For each `Si`, find the unique `Ui` in the super-interface graph of `S` such |
| that the class of `Ui` is `Ci`. Note that if there is not such a `Ui`, then |
| there is no instantiation of `M` that will allow its superclass constraint to be |
| satisfied, and if there is such a `Ui` but it is not unique, then the superclass |
| hierarchy is ill-formed. In either case it is an error. |
| |
| Let *SLN* be the smallest set of pairs of type variables and types `(Z0, T0), |
| ..., (Zl, Tl)` with type variables drawn from `X0, ..., Xj` such that `{T0/Z0, |
| ..., Tl/Zl}Si == Ui`. That is, replacing each free type variable in the `Si` |
| with its corresponding type in *SLN* makes `Si` and `Ui` the same. If no such |
| set exists, then it is an error. Note that for well-formed programs, the only |
| free type variables in the `Ti` must by definition be drawn from the type |
| parameters to the enclosing class of the mixin application. Hence it follows |
| both that the `Ti` are well-formed types in the scope of the mixin application, |
| and that the the `Xi` do not occur free in the `Ti` since we have assumed that |
| classes are suitably renamed to avoid capture. |
| |
| Let `[X0 extends B0, ..., Xj extends Bj]` be a set of type variable bounds such |
| that if `(Xi, Ti)` is in *SLN* then `Bi` is `Ti` and otherwise `Bi` is the |
| declared bound for `Xi` in the definition of `M`. |
| |
| Let `[X0 -> T0', ..., Xj -> Tj']` be the default bounds for this set of type |
| variable bounds as defined in the "instantiate to bounds" specification. |
| |
| The inferred type arguments for `M` are then `<T0', ..., Ti'>`. |
| |
| It is an error if the inferred type arguments are not a valid instantiation of |
| `M` (that is, if they do not satisfy the bounds of `M`). |
| |
| #### Discussion |
| |
| For each superclass constraint, there must be a matching interface in the |
| super-interface hierarchy of the actual superclass. So for each superclass |
| constraint of the form `I0<U0, ..., Uk>` there must be some `I0<U0', ..., Uk'>` |
| in the super-interface hierarchy of the actual superclass `S` (if not, there is |
| an error in the super class hierarchy, or in the mixin application). Note that |
| the `Ui` may have free occurrences of the type variables for which we are |
| solving, but the `Ui'` may not. A simple equality traversal comparing `Ui` and |
| `Ui'` will find all of the type variables which must be equated in order to make |
| the two interfaces equal. Once a type variable is solved via such a traversal, |
| subsequent occurrences must be constrained to an equal type, otherwise there is |
| no solution. Type variables which do not appear in any of the superclass |
| constraints are not constrained by the mixin application. Some or all of the |
| type variables may be unconstrained in this manner. We choose a solution for |
| these type variables using the instantiate to bounds algorithm. We construct a |
| synthetic set of bounds using the chosen constraints for the constrained |
| variables, and use instantiate to bounds to produce the remaining results. |
| Since instantiate to bounds may produce a super-bounded type, we must check that |
| the result satisfies the bounds (or else define a version of instantiate to |
| bounds which issues an error rather than approximates). |
| |
| Note that we do not take into account information from later mixins when solving |
| the constraints: nor from implemented interfaces. The approach specified here |
| may therefore fail to find valid instantiations. We may consider relaxing this |
| in the future. Note however that fully using information from other positions |
| will result in equality constraint queries in which type variables being solved |
| for appear on both sides of the query, hence leading to a full unification |
| problem. |
| |
| The approach specified here is a simplification of the subtype matching |
| algorithm used in expression level type inference. In the case that there is no |
| solution to the declarative specification above, subtype matching may still find |
| a solution which does not satisfy the property that no generic interface may |
| occur twice in the class hierarchy with different type arguments. A valid |
| implementation of the approach specified here should be to run the subtype |
| matching algorithm, and then to subsequently check that no generic interface has |
| been introduced at incompatible type. |
| |
| ## Tests and illustrative examples. |
| |
| Some examples illustrating key points. |
| |
| ### Inference proceeds outward |
| |
| ``` |
| class I<X> {} |
| |
| class M0<T> extends I<T> {} |
| |
| class M1<T> extends I<T> {} |
| |
| // M1 is inferred as M1<int> |
| class A extends M0<int> with M1 {} |
| ``` |
| |
| ``` |
| class I<X> {} |
| |
| class M0<T> extends I<T> {} |
| |
| class M1<T> extends I<T> {} |
| |
| class M2<T> extends I<T> {} |
| |
| // M1 is inferred as M1<int> |
| // M2 is inferred as M1<int> |
| class A extends M0<int> with M1, M2 {} |
| ``` |
| |
| ``` |
| class I<X> {} |
| |
| class M0<T> extends Object implements I<T> {} |
| |
| class M1<T> extends I<T> {} |
| |
| // M0 is inferred as M0<dynamic> |
| // Error since class hierarchy is inconsistent |
| class A extends Object with M0, M1<int> {} |
| ``` |
| |
| ``` |
| class I<X> {} |
| |
| class M0<T> extends Object implements I<T> {} |
| |
| class M1<T> extends I<T> {} |
| |
| // M0 is inferred as M0<dynamic> (unconstrained) |
| // M1 is inferred as M1<dynamic> (constrained by inferred argument to M0) |
| // Error since class hierarchy is inconsistent |
| class A extends Object with M0, M1 implements I<int> {} |
| ``` |
| |
| ### Multiple superclass constraints |
| ``` |
| class I<X> {} |
| |
| class J<X> {} |
| |
| class M0<X, Y> extends I<X> with J<Y> {} |
| |
| class M1 implements I<int> {} |
| class M2 extends M1 implements J<double> {} |
| |
| // M0 is inferred as M0<int, double> |
| class A extends M2 with M0 {} |
| ``` |
| |
| ### Instantiate to bounds |
| ``` |
| class I<X> {} |
| |
| class M0<X, Y extends String> extends I<X> {} |
| |
| class M1 implements I<int> {} |
| |
| // M0 is inferred as M0<int, String> |
| class A extends M1 with M0 {} |
| ``` |
| |
| ``` |
| class I<X> {} |
| |
| class M0<X, Y extends X> extends I<X> {} |
| |
| class M1 implements I<int> {} |
| |
| // M0 is inferred as M0<int, int> |
| class A extends M1 with M0 {} |
| ``` |
| |
| ``` |
| class I<X> {} |
| |
| class M0<X, Y extends Comparable<Y>> extends I<X> {} |
| |
| class M1 implements I<int> {} |
| |
| // M0 is inferred as M0<int, Comparable<dynamic>> |
| // Error since super-bounded type not allowed |
| class A extends M1 with M0 {} |
| ``` |
| |
| ### Non-trivial constraints |
| |
| ``` |
| class I<X> {} |
| |
| class M0<T> extends I<List<T>> {} |
| |
| class M1<T> extends I<List<T>> {} |
| |
| class M2<T> extends M1<Map<T, T>> {} |
| |
| // M0 is inferred as M0<Map<int, int>> |
| class A extends M2<int> with M0 {} |
| ``` |
| |
| ### Unification |
| These examples are not inferred given the strategy in this proposal, and suggest |
| some tricky cases to consider if we consider a broader approach. |
| |
| |
| ``` |
| class I<X, Y> {} |
| |
| class M0<T> implements I<T, int> {} |
| |
| class M1<T> implements I<String, T> {} |
| |
| // M0 inferred as M0<String> |
| // M1 inferred as M1<int> |
| class A extends Object with M0, M1 {} |
| ``` |
| |
| |
| ``` |
| class I<X, Y> {} |
| |
| class M0<T> implements I<T, List<T>> {} |
| |
| class M1<T> implements I<List<T>, T> {} |
| |
| // No solution, even with unification, since solution |
| // requires that I<List<U0>, U0> == I<U1, List<U1>> |
| // for some U0, U1, and hence that: |
| // U0 = List<U1> |
| // U1 = List<U0> |
| // which has no finite solution |
| class A extends Object with M0, M1 {} |
| ``` |