Skip to content

Commit

Permalink
Many improvements, based on reviews of both documents
Browse files Browse the repository at this point in the history
  • Loading branch information
eernstg committed Sep 2, 2024
1 parent 227b140 commit edfefb4
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 124 deletions.
250 changes: 157 additions & 93 deletions working/0723-static-extensions/feature-specification-variant1.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,27 @@ constructors in the static extension must return an object whose type is
For example:

```dart
// Omit extension type parameters when the static extension only has static
// members. Type parameters are just noise for static members, anyway.
// Omit extension type parameters when the static extension only
// has static members. Type parameters are just noise for static
// members, anyway.
static extension E2 on Map {
static Map<K2, V> castFromKey<K, V, K2>(Map<K, V> source) =>
Map.castFrom<K, V, K2, V>(source);
}
// Declare extension type parameters, to be used by constructors. The
// type parameters can have stronger bounds than the on-declaration.
// Declare extension type parameters, to be used by constructors.
// The type parameters can have stronger bounds than the
// on-declaration.
static extension E3<K extends String, V> on Map<K, V> {
factory Map.fromJson(Map<String, Object?> source) => Map.from(source);
factory Map.fromJson(Map<String, Object?> source) =>
Map.from(source);
}
var jsonMap = <String, Object?>{"key": 42};
var typedMap = Map<String, int>.fromJson(jsonMap);
// `Map<int, int>.fromJson(...)` is an error: Violates the bound of `K`.
// But `Map<int, int>.fromJson(...)` is an error: It violates the
// bound of `K`.
```

Another motivation for this mechanism is that it supports constructors of
Expand Down Expand Up @@ -120,14 +125,14 @@ static extension E4<X> on Map<X, List<X>> {
factory Map.listValue(X x) => {x: [x]};
}
var int2intList = Map.listValue(1); // Inferred as `Map<int, List<int>>`.
var int2intList = Map.listValue(1); // `Map<int, List<int>>`.
// `Map<int, double>.listValue(...)` is an error.
static extension E6<Y> on Map<String, Y> {
factory Map.fromString(Y y) => {y.toString(): y};
}
var string2bool = Map.fromString(true); // Inferred as `Map<String, bool>`.
var string2bool = Map.fromString(true); // `Map<String, bool>`.
Map<String, List<bool>> string2listOfBool = Map.fromString([]);
```

Expand All @@ -147,7 +152,8 @@ The grammar is modified as follows:
...;
<staticExtensionDeclaration> ::= // New rule.
'static' 'extension' <identifier>? <typeParameters>? 'on' <type>
'static' 'extension' <identifier>? <typeParameters>?
'on' <type>
'{' (<metadata> <staticExtensionMemberDeclaration>)* '}';
<staticExtensionMemberDeclaration> ::= // New rule.
Expand All @@ -168,9 +174,6 @@ The grammar is modified as follows:
('final' | 'const') <type>? <staticFinalDeclarationList> |
'late' 'final' <type>? <initializedIdentifierList> |
'late'? <varOrType> <initializedIdentifierList>;
<constructorNameList> ::=
<constructorName> (',' <constructorName>)*;
```

In a static extension of the form `static extension E on C {...}` where `C`
Expand Down Expand Up @@ -257,7 +260,9 @@ constructor or a static member with the same basename as a constructor or a
static member in the on-declaration of _D_.

*In other words, a static extension should not have name clashes with its
on-declaration.*
on-declaration. The warning above is aimed at static members and
constructors, but a similar warning would probably be useful for name
clashes with instance members as well.*

#### Static extension scopes

Expand All @@ -278,10 +283,10 @@ type parameter scope, if any, and otherwise the library scope.
Static members in a static extension are subject to the same static
analysis as static members in other declarations.

A factory constructor in a static extension introduces scopes in the same
way as other factory constructor declarations. The return type of the
factory constructor is the constructor return type of the static
extension *(that is, the type in the `on` clause)*.
A constructor in a static extension introduces scopes in the same way as
other constructor declarations. The return type of the constructor is the
constructor return type of the static extension *(that is, the type in the
`on` clause)*.

Type variables of a static extension `E` are in scope in static member
declarations in `E`, but any reference to such type variables in a static
Expand All @@ -295,46 +300,95 @@ the current library, or if _D_ is imported and not hidden.

*In particular, it is accessible even in the case where there is a name
clash with another locally declared or imported declaration with the same
name.*

#### Invocation of a static member of a static extension

An _explicitly resolved invocation_ of a static member of a static
extension named `E` is an expression of the form `E.m()` (or any other
member access, *e.g., `E.m`, `E.m = e`, etc*), where `m` is a static member
declared by `E`.
name. This is also true if _D_ is imported with a prefix. Similarly, it is
accessible even in the case where _D_ does not have a name, if it is
declared in the current library.*

#### Invocation of a static member

*The language specification defines the notion of a _member invocation_ in
the section [Member Invocations][], which is used below. This concept
includes method invocations like `e.aMethod<int>(24)`, property extractions
like `e.aGetter` or `e.aMethod` (tear-offs), operator invocations like
`e1 + e2` or `aListOrNull?[1] = e`, function invocations like `f()`. Each
of these expressions has a _syntactic receiver_ and an _associated member
name_. With `e.aMethod<int>(24)`, the receiver is `e` and the associated
member name is `aMethod`, with `e1 + e2` the receiver is `e1` and the
member name is `+`, and with `f()` the receiver is `f` and the member name
is `call`. Note that the syntactic receiver is a type literal in the case
where the member invocation invokes a static member. In the following we
will specify invocations of static members using this concept.*

[Member Invocations]: https://github.com/dart-lang/language/blob/94194cee07d7deadf098b1f1e0475cb424f3d4be/specification/dartLangSpec.tex#L13903

Consider an expression `e` which is a member invocation with syntactic
receiver `E` and associated member name `m`, where `E` denotes a static
extension and `m` is a static member declared by `E`. We say that `e` is an
_explicitly resolved invocation_ of said static member of `E`.

*This can be used to invoke a static member of a specific static extension
in order to manually resolve a name clash.*

A static member invocation on a class `C`, of the form `C.m()` (or any
other member access), is resolved by looking up static members in `C` named
`m` and looking up static members of every accessible static extension with
on-declaration `C` and a member named `m`.

If `C` contains such a declaration then the expression is an invocation of
that static member of `C`, with the same static analysis and dynamic
semantics as before the introduction of this feature.

Otherwise, an error occurs if no declarations named `m` or more than one
declaration named `m` were found. *They would necessarily be declared in
static extensions.*

Otherwise, the invocation is resolved to the given static member
declaration in a static extension named `Ej`, and the invocation is treated
as `Ej.m()` *(this is an explicitly resolved invocation, which is specified
above)*.
in order to manually resolve a name clash.

Consider an expression `e` which is a member invocation with syntactic
receiver `C` and an associated member name `m`, where `C` denotes a class
and `m` is a static member declared by `C`. The static analysis and dynamic
semantics of this expression is the same as in Dart before the introduction
of this feature.

When `C` declares a static member whose basename is the basename of `m`,
but `C` does not declare a static member named `m` or a constructor named
`C.m`, a compile-time error occurs. *This is the same behavior as in
pre-feature Dart. It's about "near name clashes" involving a setter.*

In the case where `C` does not declare any static members whose basename is
the basename of `m`, and `C` does not declare any constructors named `C.m2`
where `m2` is the basename of `m`, let _M_ be the set containing each
accessible extension whose on-declaration is `C`, and whose static members
include one with the name `m`, or which declares a constructor named `C.m`.

*If `C` does declare a constructor with such a name `C.m2` then the given
expression is not a static member invocation. This case is described in a
section below.*

Otherwise *(when `C` does not declare such a constructor)*, an error occurs
if _M_ is empty or _M_ contains more than one member.

Otherwise *(when no error occurred)*, assume that _M_ contains exactly one
element which is an extension `E` that declares a static member named
`m`. The invocation is then treated as `E.m()` *(this is an explicitly
resolved invocation, which is specified above)*.

Otherwise *(when `E` does not declare such a static member)*, _M_ will
contain exactly one element which is a constructor named `C.m`. This is not
a static member invocation, and it is specified in a section below.

In addition to these rules for invocations of static members of a static
extension or a class, a corresponding set of rules exist for a static
extension and the following: An enumerated declaration *(`enum ...`)*, a
mixin class, a mixin, and an extension type. They only differ by being
concerned with a different kind of on-declaration.

In addition to the member invocations specified above, it is also possible
to invoke a static member of the enclosing declaration based on lexical
lookup. This case is applicable when an expression in a class, enum, mixin
or extension type resolves to an invocation of a static member of the
enclosing declaration.

*This invocation will never invoke a static member of a static extension
which is not the enclosing declaration. In other words, there is nothing
new in this case.*

#### The instantiated constructor return type of a static extension

We associate a static extension declaration _D_ named `E` with formal type
parameters `X1 extends B1 .. Xs extends Bs` and an actual type argument
list `T1 .. Ts` with a type known as the _instantiated constructor return
type of_ _D_ _with type arguments_ `T1 .. Ts`.
Assume that _D_ is a generic static extension declaration named `E` with
formal type parameters `X1 extends B1, ..., Xs extends Bs` and constructor
return type `C<S1 .. Sk>`. Let `T1, ..., Ts` be a list of types. The
_instantiated constructor return type_ of _D_ _with actual type arguments_
`T1 .. Ts` is then the type `[T1/X1 .. Ts/Xs]C<S1 .. Sk>`.

When a static extension declaration _D_ named `E` has an on-clause which
denotes a non-generic class `C`, the instantiated constructor return type
is `C`, for any list of actual type arguments.
*As a special case, assume that _D_ has an on-type which denotes a
non-generic class `C`. In this case, the instantiated constructor return
type is `C`, for any list of actual type arguments.*

*Note that such type arguments can be useful, in spite of the fact that
they do not occur in the type of the newly created object. For example:*
Expand All @@ -352,25 +406,16 @@ static extension E<X> on A {
void main() {
// We can create an `A` "directly".
A a = A(42);
// We can also use a function to compute the `int`.
a = A.computed('Hello!', (s) => s.length);
}
```

When a static extension declaration _D_ has no formal type parameters, and
it has an on-type `C<S1 .. Sk>`, the instantiated constructor return type
of _D_ is `C<S1 .. Sk>`.

*In this case the on-type is a fixed type (also known as a ground type),
e.g., `List<int>`. This implies that the constructor return type of D is
the same for every call site.*

Finally we have the general case: Consider a static extension declaration
_D_ named `E` with formal type parameters `X1 extends B1 .. Xs extends Bs`
and a constructor return type `C<S1 .. Sk>`. With actual type arguments
`T1 .. Ts`, the instantiated constructor return type of _D_ with type
arguments `T1 .. Ts` is `[T1/X1 .. Ts/Xs]C<S1 .. Sk>`.
*As another special case, assume that _D_ has no formal type parameters,
and it has a constructor return type of the form `C<S1 .. Sk>`. In this
case the instantiated constructor return type of _D_ is `C<S1 .. Sk>`,
which is a ground type, and it is the same for all call sites.*

#### Invocation of a constructor in a static extension

Expand All @@ -391,7 +436,7 @@ information. In particular, the actual type arguments passed to the
extension determines the actual type arguments passed to the class, which
means that the explicitly resolved invocation typically has quite some
redundancy (but it is very easy to check whether it is consistent, and it
is an error if it is inconsistent). Every other form is reduced to the
is an error if it is inconsistent). Every other form is reduced to this
explicitly resolved form.*

A compile-time error occurs if the type arguments passed to `E` violate the
Expand All @@ -415,10 +460,10 @@ normalization occurs. *In other words, the types must be equal, not
just mutual subtypes.*

*Note that explicitly resolved invocations of constructors declared in
static extensions are an exception in real code, usable in the case where a
name clash prevents an implicitly resolved invocation. However, implicitly
resolved invocations are specified in the rest of this section by reducing
them to explicitly resolved ones.*
static extensions are a rare exception in real code, usable in the case
where a name clash prevents an implicitly resolved invocation. However,
implicitly resolved invocations are specified in the rest of this section
by reducing them to explicitly resolved ones.*

A constructor invocation of the form `C<T1 .. Tm>.name(args)` is partially
resolved by looking up a constructor named `C.name` in the class `C` and in
Expand All @@ -437,46 +482,65 @@ Otherwise, the invocation is partially resolved to a set of candidate
constructors found in static extensions. Each of the candidates _kj_ is
vetted as follows:

Assume that _kj_ is a constructor declared by a static extension _D_ named
`E` with type parameters `X1 extends B1 .. Xs extends Bs` and on-type
`C<S1 .. Sm>`. Find actual values `U1 .. Us` for `X1 .. Xs` satisfying the
bounds `B1 .. Bs`, such that `([U1/X1 .. Us/Xs]C<S1 .. Sm>) == C<T1 .. Tm>`.
If this fails then remove _kj_ from the set of candidate constructors.
Otherwise note that _kj_ uses actual type arguments `U1 .. Us`.
If `m` is zero and `E` is an accessible extension with on-declaration `C`
that declares a static member whose basename is `name` then the invocation
is a static member invocation *(which is specified in an earlier section)*.

Otherwise, assume that _kj_ is a constructor declared by a static extension
_D_ named `E` with type parameters `X1 extends B1 .. Xs extends Bs`,
on-declaration `C`, and on-type `C<S1 .. Sm>`. Find actual values
`U1 .. Us` for `X1 .. Xs` satisfying the bounds `B1 .. Bs`, such that
`([U1/X1 .. Us/Xs]C<S1 .. Sm>) == C<T1 .. Tm>`. This may determine the
value of some of the actual type arguments `U1 .. Us`, and others may be
unconstrained (because they do not occur in `C<T1 .. Tm>`). Actual type
arguments corresponding to unconstrained type parameters are given as `_`
(and they are subject to inference later on, where the types of the actual
arguments `args` may influence their value). If this inference fails
then remove _kj_ from the set of candidate constructors. Otherwise note
that _kj_ uses actual type arguments `U1 .. Us`.

If all candidate constructors have been removed, or more than one candidate
remains, a compile-time error occurs. Otherwise, the invocation is
henceforth treated as `E<U1 .. Us>.C<T1 .. Tm>.name(args)` (respectively
`E<U1 .. Us>.C<T1 .. Tm>(args)`).
`E<U1 .. Us>.C<T1 .. Tm>(args)`). *This is an explicitly resolved static
extension constructor invocation, which is specified above.*

A constructor invocation of the form `C.name(args)` (respectively
`C(args)`) where `C` denotes a non-generic class is resolved in the
same manner, with `m == 0`.

*In this case, type parameters declared by `E` may be inferred based on the
constructor signature (similarly to the example with `A.computed` above),
or they will be bound to values selected by instantiation to bound.*
same manner, with `m == 0`.

Consider a constructor invocation of the form `C.name(args)` (and similarly
for `C(args)`) where `C` denotes a generic class. As usual, the
invocation is treated as in the pre-feature language when it denotes a
constructor declared by the class `C`.

In the case where the context type schema for this invocation fully
determines the actual type arguments of `C`, the expression is changed to
receive said actual type arguments, `C<T1 .. Tm>.name(args)`, and treated
as described above.
In the case where the context type schema for this invocation
determines some actual type arguments of `C`, the expression is changed to
receive said actual type arguments, `C<T1 .. Tm>.name(args)` (where the
unconstrained actual type arguments are given as `_` and inferred later).
The expression is then treated as described above.

Next, we construct a set _M_ containing all accessible static extensions
with on-declaration `C` that declare a constructor named `C.name`
(respectively `C`).

In the case where the invocation resolves to exactly one constructor
`C.name` (or `C`) declared by a static extension named `E`, the invocation
is treated as `E.C.name(args)` (respectively `E.C(args)`).
In the case where _M_ contains exactly one extension `E` that declares a
constructor named `C.name` (respectively `C`), the invocation is treated as
`E.C.name(args)` (respectively `E.C(args)`).

Otherwise, when there are two or more candidates from static extensions,
an error occurs. *We do not wish to specify an approach whereby `args` is
Otherwise, when there are two or more candidates from static extensions, an
error occurs. *We do not wish to specify an approach whereby `args` is
subject to type inference multiple times, and hence we do not support type
inference for `C.name(args)` in the case where there are multiple distinct
declarations whose signature could be used during the static analysis of
that expression.*
that expression. The workaround is to specify the actual type arguments
explicitly.*

In addition to these rules for invocations of constructors of a static
extension or a class, a corresponding set of rules exist for a static
extension and the following: An enumerated declaration *(`enum ...`)*, a
mixin class, a mixin, and an extension type. They only differ by being
concerned with a different kind of declaration.

### Dynamic Semantics

Expand All @@ -495,7 +559,7 @@ This fully determines the dynamic semantics of this feature.

### Changelog

1.1 - August 28, 2024
1.1 - August 30, 2024

* Clarify many parts.

Expand Down
Loading

0 comments on commit edfefb4

Please sign in to comment.