Skip to content

[Static extensions] How does inference work for constructors defined in static extensions #4053

Open
@leafpetersen

Description

@leafpetersen

Bubbling up this discussion from comments in #3835 , how do we expect type inference to work for constructors defined in static extensions. Ignoring the question of whether we re-use the existing extension syntax or use a new static extension syntax, we have the general problem of going from a constructor call of the form C.name(args) or C<T1, ...., Tn>.name(args) which is defined on an extension E which has type arguments X1.... Xk and on type T_on to a specific choice of type arguments for X1....Xk. As a concrete example to work from, consider:

extension E<S, T> on Map<int, T> {
  factory Map.bar(S x, T y) => {3 : y};
}

How should inference work on calls to this constructor? That is, given

test() {
  var x1 = Map.bar("hello", 3);
  var x2 = Map<int, num>.bar("hello", 3);
  Map<Object, Object> x3 = Map.bar("hello, 3);
  Map<Object, Object> x4 = Map<int, num>.bar("hello, 3);
}

how do we produce the type arguments to turn each implicit invocation into an explicit invocation of the form E<T1, T2>.Map.bar("hello", 3); for some T1 and T2?

My initial thinking is that we can draw intuition by considering the original definition as essentially defining a generic static method equivalent to the following:

Map<int, T> E_Map_bar<S, T>(S x, T y) => E<S, T>.Map.bar(x, y);

This gives an obvious intuition as to how to infer calls to Map.bar without explicit type arguments: we view an invocation of Map.bar(args) in downwards context K as essentially equivalent to an invocation of E_Map_bar(args) in downwards context K. For the above examples with no explicit type arguments, this then results in the following inferred explicit invocations:

test() {
  var x1 = Map.bar("hello", 3); // E<String, int>.Map.bar("hello", 3);
  var x2 = Map<int, num>.bar("hello", 3);
  Map<Object, Object> x3 = Map.bar("hello, 3);  // E<String, Object>.Map.bar("hello", 3);
  Map<Object, Object> x4 = Map<int, num>.bar("hello, 3);
}

Note that in the case of x3, downwards inference pins the type argument T to Object when the subtype match Map<int, T> <# Map<Object, Object> is performed, but leaves S unconstrained. S is then filled in via upwards inference based on the first argument to the constructor.

How then should we treat the cases with explicit type arguments (x2 and x4)? My initial intuition is that we get the desired result by treating the explicit instantiation exactly as above, except replacing the downwards context K by the explicitly instantiated type on which the constructor is called. Note that in the explicit instantiation case, K adds nothing to the inference process. So here, we would treat var x2 = Map<int, num>.bar("hello", 3); the same as if we had the invocationE_Map_bar("hello", 3) in downwards context Map<int, num>, and likewise for Map<Object, Object> x4 = Map<int, num>.bar("hello, 3);. The result would be the following inferred explicit invocations:

test() {
  var x1 = Map.bar("hello", 3); // E<String, int>.Map.bar("hello", 3);
  var x2 = Map<int, num>.bar("hello", 3); // E<String, num>.Map.bar("hello", 3);
  Map<Object, Object> x3 = Map.bar("hello, 3);  // E<String, Object>.Map.bar("hello", 3);
  Map<Object, Object> x4 = Map<int, num>.bar("hello, 3); // E<String, num>.Map.bar("hello", 3);
}

In each case, the downwards context (if any) is replaced by Map<int, num> which in turn fixes T to be num via the subtype match of Map<int, T> <# Map<int, num> solving for T and S is inferred via upwards inference from the first argument to the constructor.

The intuition for replacing the downwards context is essentially that by writing Map<int, num>.bar(...) the user has expressed the requirement that bar produces a Map<int, num>.

Note that I'm not proposing we specify this via the desugaring to a static method E_Map_bar as written above: this just provides the underlying semantic model which motivates my sketch of how inference should work above. The specification itself is essentially just a use of the already existing generic type argument inference process, with a particular choice of downwards context, type arguments, arguments, and type parameters to solve for.

@eernstg @stereotype441 @chloestefantsova @johnniwinther WDYT ? Does this make any sense? Do you see problems with my rough sketch above? Alternative proposals?

cc @dart-lang/language-team

Metadata

Metadata

Assignees

No one assigned

    Labels

    static-extensionsIssues about the static-extensions feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions