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

[Static extensions] Should we allow static extensions to be accessed on any named type? #4055

Open
leafpetersen opened this issue Aug 23, 2024 · 1 comment
Labels
static-extensions Issues about the static-extensions feature

Comments

@leafpetersen
Copy link
Member

The current proposal (#3835) for static extensions only allows static members and constructors to be accessed only on "class-like" types (that is classes, mixin classes, mixins, enums, and extension types). In principle, we could relax this restriction, either in the case that the on type is a typedef (see #4052) or in the case that the receiver is a typedef name. For example, we could allow:

typedef Pair<T> = (T, T);

extension E<T> on Pair<T> {
   factory Pair.twice(T x) => (x, x);
}
void test() {
   var (x, y) = Pair.twice(3);
   assert(3 == y);
   assert(3 == x);
}

If so, do we attach the extension to the name Pair or to the underlying type? That is, does the following work?

typedef Pair<T> = (T, T);

extension E<T> on (T, T) {
   factory Pair.twice(T x) => (x, x);
}
void test() {
   var (x, y) = Pair.twice(3);
   assert(3 == y);
   assert(3 == x);
}

cc @dart-lang/language-team

@leafpetersen leafpetersen added the static-extensions Issues about the static-extensions feature label Aug 23, 2024
@lrhn
Copy link
Member

lrhn commented Aug 24, 2024

No. I'd only allow adding extension static members to existing static namespaces.

If we say "any type can have static members", we'll have to explain why List<int> and List<String> do not have different static namespaces, but List<int> Function() and List<String> Function () do. (Or say how to collapse those function type namespaces into one.)

The static namespaces are introduced by class, mixin, enum, extension type and extension declarations. There are no other static namespaces than those.

Some type aliases have a shape which we say denotes a static namespace, and we let them also alias that existing static namespace. (Which cannot be an extension namespace, since those do not introduce a type.)

With this feature, some extensions will also act as extensions of the static namespaces of their on type, if the on type clause has a shape that denotes a static namespace.
When we would otherwise do a static namespace lookup on that namespace, and we find no member with the needed name, we check if any extension extends that static namespace with something of the same base name, and if a single one is applicable, the static reference denotes that static member definition.

(And if we did, we would not attach anything to type alias names.)


To be a little more formal:

An identifier or qualified identifier T denotes a static namespace, N, if and only if:

  • T denotes a class, mixin, enum or extension type declaration, then N is the static namespace of that declaration.
  • T denotes an extension declaration, then N is the static namespace of that declaration.
  • T denotes a type alias declaration, and the uninstantiated aliased type clause of that declaration denotes a static namespace N.

A type clause T, which must denote a type, also denotes a static namespace, N, if and only if:

  • T is an identifier or qualified identifier, and T denotes the static namespace N, or
  • T has the form R<...> and R denotes the static namespace N. (Which, grammatically, means that R must be an identifier or qualified identifier.)

A static access on T, where T is an identifier or qualified identifier which denotes a static namespace, has one of the forms:

  • A static member access T.id, T.id = ..., T.id<...>, T.id(...) or T.id<...>(...).
  • A constructor(-only) access of the form T<...>.id, T.new, T<...>.new, T(...), T<...>(...), T<...>.id(...), T.new(...), or T<...>..new(...).

In each case, the static namespace is searched for members with base name id, or for an unnamed constructor for the new or no-name (constructor-only) cases.
If one is found, that is the member denoted by the T.id/T.new/T (as constructor).

  • For constructor-only accesses, it's a compile-time error if a non-constructor member is found.
  • For assignment, T.id = ..., it's an error if a setter is not found. (Might find a getter too, the only valid cases where there can be two static namespace members with the same base name.)
  • For the rest, a method, getter or constructor must be found, and it's invocation or tear-off (possibly instantiated) as normal, depending on which one it is. Further failures will be a type errors.

With static extensions, let's go with the variant without static, a declaration of:

extension NumList<T extends num> on List<T> {
  factory List.sorted(Iterable<T> values) => [...values]..sort();
}

will be a static extension on the static namespace denoted by List<T>, if any. (In this case, the static namespace introduced by the class List declaration).

When doing type inference for a static member access like var x = List.sorted([3, 2, 1]);:

  • We perform type inference for List.sorted([3, 2, 1]) with context type scheme _.
  • First we recognize that the expression List denotes a type declaration or extension declaration, so this is a static access.
  • The static namespace denoted by List is the static namespace of the List class declaration(s), so the static namespace definition introduced by a class declaration and possibly a number of augmenting class declarations.
  • That static namespace is checked for a member with base name sorted. None is found, so we fall back to checking static extensions.
  • For each available extension (imported into current import scope). Oooh, this is important for enhanced parts!
  • If the extension declaration's uninstantiated on type clause does not denote the same static namespace, then the extension does not apply.
  • If the extension does not declare a static member or constructor with base name sorted, it does not apply.
  • Otherwise the extension applies. (Let's go with the simple rule, and not check types.)
  • There are not other applicable extensions, so we continue with NumList.
  • Then List.sorted denotes the raw extension constructor reference NumList.sorted (because List is a generic type, and List.sorted is a raw/uninstantiated reference, we do not try to infer type parameters for NumList from that).
  • Type inference proceeds inferring types for a function type List<T> Function(List<T>) with T still unbound, context type scheme _ and argument list ([3, 2, 1]). Since the scheme is empty, we don't infer T during downwards inference.
  • Type inference on the argument list with context type scheme List<T> infers (<int>[3, 2, 1]) and int <: T`
  • then upwards type inference infers that the invoked method is NumList<int>.sorted(<int>[3, 2, 1]), which has return type List<int>, so that is the static type of the invocation. (And int <: num, so the invocation is well-bounded.)

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

No branches or pull requests

2 participants