Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

With constructors in static extensions, do we resolve them using type arguments? #4049

Open
eernstg opened this issue Aug 21, 2024 · 4 comments
Labels
question Further information is requested static-extensions Issues about the static-extensions feature

Comments

@eernstg
Copy link
Member

eernstg commented Aug 21, 2024

This issue raises the question whether we should resolve the invocation of a constructor for a class C which is declared by several static extensions based on the constructor return type, or should we just report an error as soon as there are multiple extensions declaring constructors with the requested name?

Background: #3835 provides a proposal for generalizing extension declarations such that they can contribute static members and constructors to an existing declaration (e.g., a class, an extension type, etc.), in a way that may resemble extension instance members (that is, "normal" members of the extension declarations that we have had in the language for several years). The same PR also provides a proposal where a new kind of declaration, static extension, delivers the same affordances, but avoids conflating the semantics of extension with new mechanisms and new syntax. We may prefer one or the other proposal, or something else altogether, but the following question seems relevant to them all (and I'll just use static extension to refer to the kind of declaration that provides this feature):

A static extension can declare a constructor for its so-called on-class, and it has an associated constructor return type (which is similar to the on-type of a regular extension).

The question raised in this issue is whether we should allow the actual type arguments to enable resolution of an instance creation, or we should report a compile-time error as soon as we have detected that there are multiple declarations of constructors with the desired name. Here is an example:

class MyClass<X> {
  final X x;
  MyClass.inClass(this.x);
}

static extension E1<X extends num> on MyClass<X> {
  factory MyClass.inExtension(X x) => MyClass.inClass(x);
}

static extension E2 on MyClass<int> {
  factory MyClass.inExtension(int i) => MyClass.inClass(i);
}

void main() {
  // Currently specified rule.
  MyClass<int>.inExtension(14); // Error, ambiguous: `E1<int>` and `E2` has it.
  MyClass<num>.inExtension(14); // OK, only `E1<num>` has it.
}

A proposal has been made to make this an error even in the second case because both E1 and E2 have the on-class MyClass, and both of them provide a constructor named MyClass.inExtension.

According to the proposal in #3835, it is not an error, because only E1 is capable of receiving type arguments such that its constructor return type is the specified type MyClass<num> (namely: E1<num>).

So @dart-lang/language-team, do you prefer to report an error early (by declaring an ambiguity based on the on-class alone), or do you prefer to take the type parameters into account, and only report an error if there is an ambiguity among the static extensions that are able to match the requested type?

Edit: Corrected the constructor names in the instance creations in main.

@eernstg eernstg added question Further information is requested static-extensions Issues about the static-extensions feature labels Aug 21, 2024
@lrhn
Copy link
Member

lrhn commented Aug 22, 2024

(May also apply to static extension constructor tear-offs! #3876.)

@lrhn
Copy link
Member

lrhn commented Aug 22, 2024

I would prefer to match non-static extensions as much as possible/reasonable.
That means that the static extension constructor is applicable only if the type argument is valid for the extension declaration's on type, and conflicts only occur between applicable extensions.

That would only apply to constructors, since static extension members ignore type arguments. (But they should probably not apply to a type literal with type arguments.)

It is also important that a generative constructor returns a precise type. That means that "valid for the ... on type" means equal to the on type.
A static extension on List<num> constructor should not apply to List<int>.extCostructor(), since it may return a List<num>. (So for conductor constructor selection, the instantiation of the on type, and therefore the return type of constructor, should be a subtype of the static you're of the expression, and the static type of List<int>.extC() should probably still be List<int>... and definitely not a supertype.
That should reduce the conflicts.

So an explicit Foo<Bar>.foo() only considers static extension constructors on Foo<Bar> precisely (or of a mutual subtype of Bar). They means either extension <T> on Foo<T> or extension on Foo<T>.

For explicit type arguments, that's "easy".
For omitted type arguments, we need to infer them. When we have inferred, or instantiated to bounds, then the same rules apply.

We do not want to perform type inference on constructor arguments more than once. Or effectively introduce overloading based on arguments.
So type argument inference for potentially applicable extensions (has constructor with the correct name, is generic) is probably only going to be from context type, or instantiate to bounds, for lack of a context type.

(But may be non-trivial if your parameters are not trivial, fx

static extension <T> on List<List<T>> {
  List.nest() : this.filled(<T>[]);
}

Iterable<Iterable<int>> _ =
    List.nest();    

If we can infer T as int, then the extension applies.)

So, yes. Do use type arguments to static extensions to decide if a constructor applies.

(Tear-offs may or may not be even more problematic, since the context type does include parameter types.)

@natebosch
Copy link
Member

If we use type arguments to resolve the static extensions it would allow the rest of #276 which instance extensions already partially allows.

@lrhn
Copy link
Member

lrhn commented Aug 23, 2024

Constructors are the problem.
I have contradicting feelings about how to approach them.

However, if we say that the following should work:

static extension AList<T> on List<T> {
  List.singleton(T value) : this.filled(1, value, growable: true);
}
void main() {
   var list = List.singleton(4);
   list = List.singleton(5);
   var list2 = List<num>.singleton(6);
   list2.add(1.5);
}

then it puts some constraints on what we can disallow.

What's interesting here is that List.singleton is a member access on a raw type.
For this to work properly, we need to special-case constructor invocations in every possible way, to make them behave like if they were declared on the class itself. We need to distinguish invocations on raw types from invocations on instantiated generic types, doing type inference based on arguments after we've decided whether the extension applies. (Because we don't want to do type inference on arguments twice.) , That's treating the type arguments to the class being constructed more as arguments to the constructor than to the extension.
On the other hand, we may want List<num>.singleton(1) to not apply to static extension IntList on List<int>, so if we do have a non-raw invocation, then we want to use the requested type as an applicability filter.

So two different cases for T.m depending on whether T is a raw type or not.
And if it is a raw type, but we have a context type, we'll want to infer type arguments from the context.

So, during type inference of:

  • a static member access, one of T.m, T.m = ..., T.m(...), T.m<...> or T.m<...>(...), where T is something we would currently allow static members to be accessed through, or

  • a constructor access of the form T<...>.m or T<...>.m(...) where we know grammatically that these are constructor accesses, and we know the target is not a raw type, we do the following if the static namespace of T doesn't have any m members itself:

  • For each static extension declaration E in scope (imported into current library, with or without a prefix):

  • If E does not have a static member named m or constructor with base name m, then it does not apply.

  • If the expression is a constructor access and the extension member is not a constructor, then E does not apply.

  • If the expression is one of T.m = ...,, T.m<...> or T.m<....>(...), and the extension member is a constructor, then E does not apply. (Only T.m and T.m(....) can be both a member access and a constructor access.)

  • Otherwise let O be the uninstantiated on type of E. If the static namespace of O is not the static namespace of T, then E does not apply. (Has to denote the same type).

  • If E has a static member named m, then E applies. (No further constraints on static members.)

  • Otherwise E has a constructor with base name m, and the expression is one of R.m or R.m(...), where T may be either a type name or an instantiated type (T or T<...>).

  • If R is not a raw type (an identifier or qualified identifier denoting a generic declaration), let T be the static type it denotes.

    • Try to solve O <: T for the type parameters of E. (A trivial check if E is not generic.)
    • If no solution is found, E does not apply.
    • Otherwise let P be the instantiated on type of E with that binding of the type parameters.
    • If P is not mutual-subtype-equivalent to T, E does not apply.
    • Otherwise E applies with that binding of type parameters.
  • Otherwise the expression is one of T.m or T.m(...) where T is a raw type (an identifier or qualified identifier denoting a generic declaration).

  • If the expression has no context type, or a context type scheme of _, then E applies with no type parameter bindings.

  • Otherwise let C be the context type scheme:

    • If the expression is T.m (a potential constructor tear-off):
      • If C is not a function type or a supertype of Function, E does not apply.
      • If C is a function type, let T be the return type of that function type.
      • Otherwise E applies with no type parameter bindings.
    • If the expression is *T.m(..)`:
      • Let T be C.
    • If E was not accepted or rejected, then T is defined.
    • try solving O <: T for the type parameters of E.
    • If not successful, E does not apply.
    • Otherwise let P be the instantiated on type of E with those type parameter bindings.
    • If P is not a mutual subtype of T, E does not apply.
    • Otherwise E applies with that instantiation of its type parameters.

Then further:

  • If exactly one static extension applies, then the expression applies to that member.
    • If the extension member is a static member, then T.m denotes that member, and further type inference is for access to that static member.
    • If the extension member is a constructor with a static extension type parameter binding B,
      then T.m denotes that constructor as a member of the static extension instantiated with B.
      (We'll have rules for static analysis of directly invoked static extension constructors, on the static extension itself,
      and this invocation then uses those rules.)
    • If the extension member is a constructor without a static extension type parameter binding B (a "raw" invocation)
      and is of the form T.m or T.m(...), then T.m` denotes the static extension constructor declaration as a member
      of an uninstantiated static extension declaration.

Basically, if you write:

static extension AList<T> on List<T> {
  List.singleton(T value) : this.filled(1, value);
}

then you can directly access:

var v1 = AList.singleton(1);
var v2 = AList<int>.singleton(1);
List<num> v3 = AList.singleton(1);
var v4 = AList.singleton;
var v5 = AList<int>.singleton;
List<num> Function() v6 = AList.singleton;

We will need to specify the meaning of that, which is trivial for non-constructors. For constructors, it follows the same rules as for constructors declared on a class, except that the return type of the constructor is the on type.

If you then write

var v1 = List.singleton(1);
var v2 = List<int>.singleton(1);
List<num> v3 = List.singleton(1);
var v4 = List.singleton;
var v5 = List<int>.singleton;
List<num> Function() v6 = List.singleton;

then it work exactly the same, with List.singleton denoting AList.singleton as an unqualified reference, and List<int>.singleton denotes AList<int>.singleton. Further type inference on arguments is performed after choosing which declaration the List.singleton refers to, but that includes type inference of the type argument to List.

Unless there is a context type, then we infer that on the way down, of if the type isn't raw, then we use the resulting instantiated static extension on type as a filter for whether static extension constructor applies.

If we have an instantiation of the created type, before looking at arguments, it's used as a filter.
If not, we consider the extension applicable, and if it's the only one then we do inference for accessing that member, which may come back as an instantiation of the extension itself.

Feels messy. But might just work, if we can solve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested static-extensions Issues about the static-extensions feature
Projects
None yet
Development

No branches or pull requests

3 participants