From 90a2d9fb225a095d49a55cc5bc944b1dc61c2d05 Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Mon, 24 Mar 2025 17:32:22 +0100 Subject: [PATCH 01/17] Add the feature specification --- .../4200-metaobjects/feature-specification.md | 461 ++++++++++++++++++ 1 file changed, 461 insertions(+) create mode 100644 working/4200-metaobjects/feature-specification.md diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md new file mode 100644 index 000000000..afcb807c5 --- /dev/null +++ b/working/4200-metaobjects/feature-specification.md @@ -0,0 +1,461 @@ +# Metaobjects + +Author: Erik Ernst + +Status: Draft + +Version: 1.1 + +Experiment flag: metaobjects + +This document specifies the metaobjects feature, which is a feature that +allow a type `T` to be mapped into one or more kinds of objects known as +_metaobjects_ of that type. The main purpose of a metaobject is to serve as +a vehicle for late-bound invocations of static members and/or constructors +of the type `T`, but it is also possible to use the mechanism for other +purposes. + +## Introduction + +The _metaobjects_ feature allows programs to map a given type to an object, +known as a _metaobject_, which allows features associated with that type +such as static members and constructors to be supported without depending +on the exact type itself. + +Current Dart only allows static members and constructors to be invoked by +specifying the exact declaration (of a class, mixin, etc) that contains +said static member or constructor declaration. For example, if a class `A` +declares a static method `foo` then we can call it using `A.foo(...)`, but +we can not call it using `X.foo(...)` where `X` is a type variable, even in +the case where the value of `X` at run time is `A`. + +Here is an example: + +```dart +// Define the interface that we wish to support. +abstract class Fooable { + String foo(X x); +} + +// The `static implements` clause specifies that a +// metaobject for `A` supports the given interface. +class A static implements Fooable { + final String name; + A(this.name); + static String foo(A a) => "${a.name} fooing!"; +} + +// Ditto. +class B static implements Fooable { + final int size; + B(this.size); + static String foo(B b) => "B of size ${b.size} fooing!"; +} + +// Does not depend on `A` or `B`, but is still type safe. +void showFoo>(X x) { + X.foo(x); +} + +void main() { + showFoo(A("MyA"); + showFoo(B(42)); +} +``` + +This works because the type variable `X` is evaluated to an object when it +is used as the receiver in an expression like `X.foo(x)`. The +_static bound_ on `X` which is specified as `static extends Fooable` +provides a compile-time guarantee that the actual argument that `X` is +bound to in any given invocation of `showFoo` will be a type such that +when it is evaluated as an expression, the resulting metaobject will have a +type which is a subtype of `Fooable`. In particular, it has a `foo` +method that has a positional parameter of type `X`, so we can safely call +it as `X.foo(x)`. + +In main, it is statically ensured that the actual type argument of the two +invocations of `showFoo` satisfy this requirement: In the first invocation +the inferred type argument is `A`, and this is OK because +`A static implements Fooable`. Similarly for the second invocation with +the actual type argument `B`. + +The semantics that actually makes the invocation of `foo` call the static +method of `A` respectively `B` is established by an implicitly induced +class for each class that has a `static implements` clause. This implicitly +induced class implements all members of the specified interface by +forwarding to a static member or a constructor of the class/mixin/... whose +type it is a metaobject for. + +For example: + +```dart +class MetaobjectForA implements Type, Fooable { + String foo(A a) => A.foo(a); + bool operator ==(Object other) => ...; + int get hashCode => ...; +} + +class MetaobjectForB implements Type, Fooable { + String foo(B b) => B.foo(b); + bool operator ==(Object other) => ...; + int get hashCode => ...; +} +``` + +These classes are implicitly generated by the compiler/analyzer, so we +can't refer to them in user code (the real name is probably something like +`$xBz8_`, and it's certainly a fresh name such that it doesn't coincide +with any name that a developer has written). However, when a type variable +like `X` above is evaluated as an expression, the resulting metaobject will +be an instance of one of these classes. + +These classes implement operator `==` and the getter `hashCode`, such that +they can behave the same as objects of type `Type` do today, when they are +obtained by evaluating a type as an expression. + +The static type of the metaobject will be the implicitly induced class when +the type which is being evaluated as an expression is a compile-time +constant type (for example, `var myMetaObject = A;`). In the case where the +type is a type variable `X` that has a bound of the form `static extends I` +then the metaobject has a static type which is a subtype of `Type` and a +subtype of `I`. (An implementation is free to implicitly generate such +classes when needed, or it may represent this kind of type in some other +way if preferred.) This implies that we can use the members of `I` on +that metaobject, e.g., `X.foo(x)` in the example above. + +As a special case (ensuring that this feature does not break existing +code), the result of evaluating a type that denotes a type introducing +membered declaration except extension types (that is, a class, mixin, mixin +class, or an enum) that does _not_ have a `static implements` clause has +static type `Type` and works exactly the same as today. So does the result +of evaluating a type that does not denote a declaration (that is, a +function type, a record type, special types like `dynamic`, union types +like `T?` and `FutureOr`, etc.) + +The previous example showed how we can use metaobjects to provide access to +static members of a set of classes (or mixins, etc.) without depending on +the exact class (mixin, etc.) declaration. This basically means that we +have made the static members _virtual_, because we're calling them via +normal instance members (that is, virtual members, with all the usual OO +semantics) of the metaobject. + +The next example illustrates how we can use metaobjects to call +constructors in a similar way (yielding 'virtual constructors'): + +```dart +abstract class Creation { + X call(); + X named(int _); +} + +class C static implements Creation> { + C.named(int _): this(); +} + +class D static implements Creation { + factory D() = _DImpl; + D.named(int _); +} + +class _DImpl implements D {...} + +X create>() => X(); + +void main() { + C c = create(); + D d = create(); +} +``` + +This illustrates that we can perform creation of instances of the given +type argument (denoted by `X` in the declaration of `create`), in spite of +the fact that the class `C` is generic (and the type argument `X` has the +value `C`, that is, it carries the actual type arguments with it), and +the constructor in `C` that we're using is the implicitly induced 'default' +constructor whose name is `C` (we could have written this explicitly as +`C();` or `C(): super();`). In contrast, the constructor that we're using +in `D` is a redirecting factory constructor. + +This works because the only requirement for a class `C` to static implement +`Creation` is that it must have a declaration which can be invoked as an +invocation of the type itself (`C()`), which is exactly what we get the +ability to do when the metaobject has a `call` method (that is, we can do +`X()` when `X` denotes an object that has a `call` method). + +The constructors named `C.named` and `D.named` are treated similarly except +that they are named. They can be invoked using expressions like +`X.named(42)` when `X` is a type variable that +`static extends Creation`. + +When a type introducing declaration (that is, a class/mixin/etc) has a +`static implements I` clause, the implicitly generated class which is used +to create metaobjects for said type will have an `implements I` clause, and +it is satisfied by generating forwarding instance members for each of the +members of the interface of `I`. + +It is also possible to use a `static extends T` clause, in which case the +implicitly generated metaobject class will have an `extends T` clause. This +implies that the implicitly generated class can inherit behaviors from `T` +(and possibly implement others as forwarders, with members which are not +implemented otherwise). + +For example: + +```dart +abstract class CallWithTypeArguments { + int get numberOfTypeArguments; + R callWithTypeArgument(int number, R callback()); +} + +class _EStaticHelper implements CallWithTypeArguments { + int get numberOfTypeArguments => 2; + R callWithTypeArgument(int number, R callback()) => switch (number) { + 1 => callback(), + 2 => callback(), + _ => throw ArgumentError("Expected number in {1, 2}, got $number."), + }; +} + +class E static extends _EStaticHelper { + const E(); + void foo(X x, Y y) {} +} + +void main() { + final E e = E(); + + // When we don't know the precise type arguments we can't call + // `e.foo` safely. But `CallWithTypeArguments` can help! + final eType = e.runtimeType; + eType.callWithTypeArgument(1, () { + eType.callWithTypeArgument(2, () { + var potentialArgument1 = 'Hello'; + var potentialArgument2 = 42; + if (potentialArgument1 is X && potentialArgument2 is Y) { + a.foo(potentialArgument1, potentialArgument2); // Safe! + } + }); + }); + + // If we didn't have this feature we could only do this: + try { + e.foo('Hello', 42); // May or may not throw. + } catch (error) { + // Some error recovery. + } +} +``` + +The `static extends` clause allows metaobjects to have arbitrary +user-written code in members (not just forwarding members, as is the case +with `static implements`). + +In this example, we're using it to provide a very basic kind of an +'existential open' operation. That is, we provide support for executing +code in a scope where the actual value of each type parameter can be +denoted. In the example we use this capability to test whether or not the +given arguments have the required types. + +Here is the corresponding implicitly induced metaobject class: + +```dart +class MetoobjectForE extends _EStaticHelper implements Type { + // All member implementations are inherited, except for the support + // for `Type` equality. So we only have the following: + bool operator ==(Object other) => ...; + int get hashCode => ...; +} +``` + +In general, the static clauses and the regular subtype relationships are +independent. It is possible for two classes to have a subtype relationship, +and both of them may have a `static implements` or `static extends` clause, +but it is still possible for those static supertypes to be unrelated. Or +vice versa: the classes in the first example, `A` and `B`, are unrelated +classes, but they have the same static supertype. + +This means that it is meaningful to have a regular bound on a type variable +as well as a static bound, because none of them is a consequence of the +other: `X extends SomeType static extends SomeOtherType`. This just means +that `X` is a subtype of `SomeType`, and a metaobject which is obtained by +evaluating `X` as an expression will be an object whose run-time type is a +subtype of `SomeOtherType`. However, even if we know that `Y extends X`, +we cannot conclude that `Y static extends SomeOtherType`. + +Note that the latter could never be true: If `Y extends X` and +`X static extends SomeOtherType` would actually imply that +`Y static extends SomeOtherType` then the object which is obtained by +evaluating `Never` as an expression would have to have all types +because we can always write `X static extends C` for any class `C`, +so a metaobject for `Never` must, essentially, be an instance of +`Never`, and that _must_ be impossible (`Never` corresponds to the empty +set, so we can't promise that we can deliver an element that belongs to +this set). + +In summary, this feature can be said to introduce support for virtual +static members, virtual constructors, and type related behaviors including +the ones that rely on having explicit access to the actual type arguments +of the given type. + +## Specification + +### Syntax + +The grammar is adjusted as follows: + +```ebnf + ::= // New. + 'static' 'extends' + | 'static' 'implements' + + ::= // Modified. + ( | ) + 'class' ? + ? ? + '{' ( )* '}' + | 'mixin'? 'class' + + ::= // Unchanged, included for readability + '=' ';' + + ::= // Modified. + ? ? + + ::= // Modified. + 'base'? 'mixin' + ('on' )? ? ? + '{' ( )* '}' + + ::= // Modified. + 'enum' ? ? ? + '{' (',' )* (',')? + (';' ( )*)? '}' + + ::= // Modified. + + ('extends' )? + ('static' 'extends' ) +``` + +The modifications are generally that type introducing declarations (except +extension types) are extended to include `?`, and type +parameters are extended to include the corresponding static bound. + +### Static Analysis + +Assume that _D_ is a class declaration named `C` which has a clause of the +form `static implements T` or `static extends T`. + +A compile-time error occurs if `T` is not an interface type. A compile-time +error occurs if `C` does not declare a set of static members and/or +constructors such that every member of `T` can obtain a correctly +overriding implementation by implicitly inducing a forwarding member to a +static member or constructor of `C`. + +A compile-time error occurs if the metaobject class derived from _D_ has +any errors. + +#### Deriving Forwarding Members + +Assume that _D_ is a class declaration named `C` which has a clause of the +form `static implements T` or `static extends T`, and declares the formal +type parameters `X1 extends B1 .. Xk extends Bk`. + +Forwarding members are derived from the static members and as follows: + +If `static R get g ...` is a static member of `C` then the corresponding +forwarding member is `R get g => C.g;`. + +If `static set s(parm) ...` is a static setter of `C` where `parm` is +derived from ``, then the corresponding forwarding +setter is `set s(parm) => C.s = arg;`, where `arg` is the identifier which +is declared as a positional parameter by `parm`. + +*Static variable declarations are covered as getters and/or setters.* + +If `static R m(parms)` is a static method of `C` where `parms` is derived +from ``, then the corresponding forwarding method is +`R m(parms) => C.m(args);`, where `args` is a comma separated list of +identifiers declared as positional parameters in `parms`, followed by +actual arguments of the form `id: id` for the named parameters in `parms`. + +Similarly, if `static R m(parms)` is a static method of `C` +where `typeParms` is derived from ` (',' )*` +and `parms` is derived from ``, then the corresponding +forwarding method is `R m(parms) => C.m(args);`, where +`typeArgs` is a comma separated list of the type variables declared in +`typeParms` in declaration order, and `args` is a comma separated list of +identifiers declared as positional parameters in `parms`, followed by +actual arguments of the form `id: id` for the named parameters in `parms`. + +*Note that the type parameter bounds and optional parameter default values +are the same for the original declaration and the forwarding +declaration. This works because the implicitly induced class is placed +in the same scope as the original declaration.* + +If `C(parms) ...` or `factory C(parms) ...` is a constructor declared by `C` +then the corresponding forwarding declaration is +`C call(parms1) => C(args);`, here `args` are derived from `parms1` +in the same way as with the static method above, and `parms1` is derived +from `parms` by erasing `this.` and `super.` and inserting the inferred +types and default values that are implied by `this.` and `super.` in the +cases where those declared types and default values have been omitted. + +If `C.name(parms) ...` or `factory C.name(parms) ...` is a constructor +declared by `C` then the corresponding forwarding declaration is +`C name(parms1) => C.name(args);`, here `args` are derived from +`parms1` in the same way as with the static method above, and `parms1` is +derived from `parms` in the same way as in the previous case. + +#### Deriving the Metaobject Class + +Assume that _D_ is a class declaration named `C` which has a clause of the +form `static implements T` or `static extends T`, and declares the formal +type parameters `X1 extends B1 .. Xk extends Bk`. + +The _metaobject class_ for _D_ is an implicitly induced class which is +located in the same scope as _D_. + +The name of the metaobject class is a fresh name, say, `MetaC`. The +implicitly induced metaobject class for _D_ is then as follows in the case +where the clause is `static implements T`: + +```dart +class MetaC implements Type, T { + // Implicitly induced member implementations, obtained by + // deriving forwarding declarations from static members and/or + // constructors in `C`, including only the ones needed to + // implement `T`. + ... +} +``` + +In the case where the clause is `static extends T` we have the following: + +```dart +class MetaC extends T implements Type { + // Implicitly induced member implementations, same rules. + ... +} +``` + +### Dynamic Semantics + +In the case where a type `T` is introduced by a declaration that has a +`static implements` or a `static extends` clause, the step whereby a given +type is evaluated as an expression is changed such that the resulting +metaobject is an instance of the implicitly induced metaobject class, +passing the same actual type arguments as the ones which are part of `T`. + +*For example, if we are evaluating a type parameter `X` as an expression, +and the value of `X` is a type `C` that has a metaobject class +`MetaC` then the result will be an instance of `MetaC`.* + +The metaobject has standard semantics, everything follows the normal rules +of Dart based on the metaobject class declaration. + +### Changelog + +1.0 - Mar 28, 2025 + +* First version of this document released. From 51c108cd21c98f4429e16417bf02483679a8eff2 Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Mon, 24 Mar 2025 17:39:59 +0100 Subject: [PATCH 02/17] WIP --- working/4200-metaobjects/feature-specification.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index afcb807c5..48da69712 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -425,7 +425,8 @@ class MetaC implements Type, T { // Implicitly induced member implementations, obtained by // deriving forwarding declarations from static members and/or // constructors in `C`, including only the ones needed to - // implement `T`. + // implement `T` plus operator `==` and `hashCode` to support + // today's behavior of `Type` instances. ... } ``` @@ -433,7 +434,8 @@ class MetaC implements Type, T { In the case where the clause is `static extends T` we have the following: ```dart -class MetaC extends T implements Type { +class MetaC + extends T implements Type { // Implicitly induced member implementations, same rules. ... } From eee335ebda33c6536aa05971b05be86ff396e666 Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Tue, 25 Mar 2025 10:50:04 +0100 Subject: [PATCH 03/17] Small improvements --- .../4200-metaobjects/feature-specification.md | 238 +++++++++++------- 1 file changed, 142 insertions(+), 96 deletions(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index 48da69712..42143fc76 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -23,17 +23,19 @@ such as static members and constructors to be supported without depending on the exact type itself. Current Dart only allows static members and constructors to be invoked by -specifying the exact declaration (of a class, mixin, etc) that contains -said static member or constructor declaration. For example, if a class `A` -declares a static method `foo` then we can call it using `A.foo(...)`, but -we can not call it using `X.foo(...)` where `X` is a type variable, even in -the case where the value of `X` at run time is `A`. +specifying the exact declaration (of a class, mixin, mixin class, extension +type, extension, or enum) that contains said static member or constructor +declaration. For example, if a class `A` declares a static method `foo` +then we can call it using `A.foo(...)`, but we can not call it using +`X.foo(...)` where `X` is a type variable, even in the case where the value +of `X` at run time is `A`. Here is an example: ```dart // Define the interface that we wish to support. abstract class Fooable { + const Fooable(); String foo(X x); } @@ -81,28 +83,30 @@ the actual type argument `B`. The semantics that actually makes the invocation of `foo` call the static method of `A` respectively `B` is established by an implicitly induced -class for each class that has a `static implements` clause. This implicitly -induced class implements all members of the specified interface by -forwarding to a static member or a constructor of the class/mixin/... whose -type it is a metaobject for. +class for each class that has a `static implements` clause, known as the +_metaobject class_. The metaobject class implements all members of the +specified interface by forwarding to a static member or a constructor of +the underlying class, mixin, mixin class, or enum declaration. For example: ```dart class MetaobjectForA implements Type, Fooable { + const MetaobjectForA(); String foo(A a) => A.foo(a); bool operator ==(Object other) => ...; int get hashCode => ...; } class MetaobjectForB implements Type, Fooable { + const MetaobjectForB(); String foo(B b) => B.foo(b); bool operator ==(Object other) => ...; int get hashCode => ...; } ``` -These classes are implicitly generated by the compiler/analyzer, so we +These classes are implicitly generated by the compiler and analyzer, so we can't refer to them in user code (the real name is probably something like `$xBz8_`, and it's certainly a fresh name such that it doesn't coincide with any name that a developer has written). However, when a type variable @@ -113,24 +117,21 @@ These classes implement operator `==` and the getter `hashCode`, such that they can behave the same as objects of type `Type` do today, when they are obtained by evaluating a type as an expression. -The static type of the metaobject will be the implicitly induced class when -the type which is being evaluated as an expression is a compile-time +The static type of the metaobject will be the metaobject class when the +type literal which is being evaluated as an expression is a compile-time constant type (for example, `var myMetaObject = A;`). In the case where the -type is a type variable `X` that has a bound of the form `static extends I` -then the metaobject has a static type which is a subtype of `Type` and a -subtype of `I`. (An implementation is free to implicitly generate such -classes when needed, or it may represent this kind of type in some other -way if preferred.) This implies that we can use the members of `I` on -that metaobject, e.g., `X.foo(x)` in the example above. +type literal is a type variable `X` that has a bound of the form +`static extends I`, the metaobject has a static type which is a subtype of +`Type` and a subtype of `I`. This implies that we can use the members of +`I` on that metaobject. E.g., we can call `X.foo(x)` in the example above. As a special case (ensuring that this feature does not break existing -code), the result of evaluating a type that denotes a type introducing -membered declaration except extension types (that is, a class, mixin, mixin -class, or an enum) that does _not_ have a `static implements` clause has -static type `Type` and works exactly the same as today. So does the result -of evaluating a type that does not denote a declaration (that is, a -function type, a record type, special types like `dynamic`, union types -like `T?` and `FutureOr`, etc.) +code), the result of evaluating a type literal that denotes a class, a +mixin, a mixin class, or an enum declaration that does _not_ have a +`static implements` clause has static type `Type`, and it works exactly the +same as today. So does the result of evaluating a type that does not denote +a declaration (that is, a function type, a record type, special types like +`dynamic`, union types like `T?` and `FutureOr`, etc.) The previous example showed how we can use metaobjects to provide access to static members of a set of classes (or mixins, etc.) without depending on @@ -187,17 +188,15 @@ that they are named. They can be invoked using expressions like `X.named(42)` when `X` is a type variable that `static extends Creation`. -When a type introducing declaration (that is, a class/mixin/etc) has a -`static implements I` clause, the implicitly generated class which is used -to create metaobjects for said type will have an `implements I` clause, and -it is satisfied by generating forwarding instance members for each of the -members of the interface of `I`. +When a class, mixin, mixin class, or enum declaration has a +`static implements I` clause, the metaobject class for said type will have +an `implements I` clause, and it is implemented by generating forwarding +instance members for each of the members of the interface of `I`. It is also possible to use a `static extends T` clause, in which case the -implicitly generated metaobject class will have an `extends T` clause. This -implies that the implicitly generated class can inherit behaviors from `T` -(and possibly implement others as forwarders, with members which are not -implemented otherwise). +metaobject class will have an `extends T` clause. This implies that the +metaobject class can inherit behaviors from `T` (and possibly implement +others as forwarders, with members which are not implemented otherwise). For example: @@ -209,11 +208,13 @@ abstract class CallWithTypeArguments { class _EStaticHelper implements CallWithTypeArguments { int get numberOfTypeArguments => 2; - R callWithTypeArgument(int number, R callback()) => switch (number) { - 1 => callback(), - 2 => callback(), - _ => throw ArgumentError("Expected number in {1, 2}, got $number."), - }; + R callWithTypeArgument(int number, R callback()) { + return switch (number) { + 1 => callback(), + 2 => callback(), + _ => throw ArgumentError("Expected 1 or 2, got $number."), + }; + } } class E static extends _EStaticHelper { @@ -236,7 +237,7 @@ void main() { } }); }); - + // If we didn't have this feature we could only do this: try { e.foo('Hello', 42); // May or may not throw. @@ -248,7 +249,7 @@ void main() { The `static extends` clause allows metaobjects to have arbitrary user-written code in members (not just forwarding members, as is the case -with `static implements`). +with `static implements`). In this example, we're using it to provide a very basic kind of an 'existential open' operation. That is, we provide support for executing @@ -259,9 +260,12 @@ given arguments have the required types. Here is the corresponding implicitly induced metaobject class: ```dart -class MetoobjectForE extends _EStaticHelper implements Type { - // All member implementations are inherited, except for the support - // for `Type` equality. So we only have the following: +class MetaobjectForE extends _EStaticHelper + implements Type { + const MetaobjectForE(); + + // All member implementations are inherited, except for the + // support for `Type` equality. So we only have the following: bool operator ==(Object other) => ...; int get hashCode => ...; } @@ -282,8 +286,8 @@ evaluating `X` as an expression will be an object whose run-time type is a subtype of `SomeOtherType`. However, even if we know that `Y extends X`, we cannot conclude that `Y static extends SomeOtherType`. -Note that the latter could never be true: If `Y extends X` and -`X static extends SomeOtherType` would actually imply that +Note that the latter could never be true: If `Y extends X` and +`X static extends SomeOtherType` would actually imply that `Y static extends SomeOtherType` then the object which is obtained by evaluating `Never` as an expression would have to have all types because we can always write `X static extends C` for any class `C`, @@ -319,20 +323,23 @@ The grammar is adjusted as follows: '=' ';' ::= // Modified. - ? ? + + ? ? ::= // Modified. 'base'? 'mixin' - ('on' )? ? ? + ('on' )? + ? ? '{' ( )* '}' ::= // Modified. - 'enum' ? ? ? + 'enum' ? + ? ? '{' (',' )* (',')? (';' ( )*)? '}' ::= // Modified. - + ('extends' )? ('static' 'extends' ) ``` @@ -346,15 +353,79 @@ parameters are extended to include the corresponding static bound. Assume that _D_ is a class declaration named `C` which has a clause of the form `static implements T` or `static extends T`. -A compile-time error occurs if `T` is not an interface type. A compile-time -error occurs if `C` does not declare a set of static members and/or -constructors such that every member of `T` can obtain a correctly -overriding implementation by implicitly inducing a forwarding member to a -static member or constructor of `C`. +A compile-time error occurs if `T` is not an interface type. + +A member access *(for example, a method, setter, or getter invocation such +as `r.foo()`)* whose receiver is a type variable (`X.foo()`) is treated as +the same member access where the receiver is parenthesized (`(X).foo()`). + +*This implies that the member access is treated as a member access on the +result of evaluating the type literal as an expression, i.e., it is a +member access on the metaobject of the value of that type variable.* + +*Note that this is not a breaking change because every member access on a +type variable is a compile-time error in current Dart.* + +#### Deriving the Metaobject Class + +Assume that _D_ is a class declaration named `C` which declares the formal +type parameters `X1 extends B1 .. Xk extends Bk` and has a clause of the +form `static implements T` or `static extends T`. + +The _metaobject class_ for _D_ is an implicitly induced class which is +located in the same scope as _D_. + +The name of the metaobject class is a fresh name, say, `MetaC`. The +implicitly induced metaobject class for _D_ is then declared as follows in +the case where the clause is `static implements T`: + +```dart +class MetaC implements Type, T { + const MetaC(); + + // Implicitly induced member implementations, obtained by + // deriving forwarding declarations from static members and/or + // constructors in `C`, including only the ones needed to + // implement `T`, plus operator `==` and `hashCode` to support + // today's behavior of `Type` instances: + ... +} +``` + +In the case where the clause is `static extends T`, the metaobject class is +implicitly induced with the following declaration: + +```dart +class MetaC + extends T implements Type { + const MetaC(); + + // Implicitly induced member implementations, same rules: + ... +} +``` + +The derivation of forwarding members is specified in the next section. A compile-time error occurs if the metaobject class derived from _D_ has any errors. +*For example, it is an error on the `static implements I` or +`static extends I` clause if `I` has a member signature `String bar(int)`, +and there is a derived member whose name is `bar`, but it has signature +`int bar()`, which is not a correct override.* + +*Note that with `static extends C`, it is an error if `C` does not have a +constant constructor whose name is `C`. This implies that metaobjects can +be constant. This is possible in every case when the underlying type is +non-generic, and it may be possible in some cases when the underlying type +is generic.* + +A compile-time error occurs if `C` does not declare a set of static members +and/or constructors such that every unimplemented member of `T` can obtain +a correctly overriding implementation by implicitly inducing a forwarding +member to a static member or constructor of `C`. + #### Deriving Forwarding Members Assume that _D_ is a class declaration named `C` which has a clause of the @@ -389,9 +460,9 @@ identifiers declared as positional parameters in `parms`, followed by actual arguments of the form `id: id` for the named parameters in `parms`. *Note that the type parameter bounds and optional parameter default values -are the same for the original declaration and the forwarding -declaration. This works because the implicitly induced class is placed -in the same scope as the original declaration.* +are the same for the original declaration and the forwarding declaration. +This works because the metaobject class is placed in the same scope as the +original declaration.* If `C(parms) ...` or `factory C(parms) ...` is a constructor declared by `C` then the corresponding forwarding declaration is @@ -407,47 +478,22 @@ declared by `C` then the corresponding forwarding declaration is `parms1` in the same way as with the static method above, and `parms1` is derived from `parms` in the same way as in the previous case. -#### Deriving the Metaobject Class - -Assume that _D_ is a class declaration named `C` which has a clause of the -form `static implements T` or `static extends T`, and declares the formal -type parameters `X1 extends B1 .. Xk extends Bk`. - -The _metaobject class_ for _D_ is an implicitly induced class which is -located in the same scope as _D_. - -The name of the metaobject class is a fresh name, say, `MetaC`. The -implicitly induced metaobject class for _D_ is then as follows in the case -where the clause is `static implements T`: - -```dart -class MetaC implements Type, T { - // Implicitly induced member implementations, obtained by - // deriving forwarding declarations from static members and/or - // constructors in `C`, including only the ones needed to - // implement `T` plus operator `==` and `hashCode` to support - // today's behavior of `Type` instances. - ... -} -``` - -In the case where the clause is `static extends T` we have the following: +### Dynamic Semantics -```dart -class MetaC - extends T implements Type { - // Implicitly induced member implementations, same rules. - ... -} -``` +When invoked on an object whose run-time type is `C`, in the case +where `C` has a `static implements` or `static extends` clause, the +implementation of the getter `runtimeType` in `Object` returns an instance +of the metaobject class for `C` with the same type arguments `T1 .. Tk`. -### Dynamic Semantics +*For example, `C().runtimeType` returns `MetaC()` +if `C` has the clause `static implements SomeInterface` and `MetaC` is the +implicitly induced metaobject class for `C`.* -In the case where a type `T` is introduced by a declaration that has a -`static implements` or a `static extends` clause, the step whereby a given -type is evaluated as an expression is changed such that the resulting -metaobject is an instance of the implicitly induced metaobject class, -passing the same actual type arguments as the ones which are part of `T`. +In the case where a type `T` is introduced by a declaration that has a +`static implements` or a `static extends` clause, the step whereby this +type is evaluated as an expression yields the corresponding metaobject, +that is, an instance of the metaobject class for `T`, passing the same +actual type arguments as the ones which are part of `T`. *For example, if we are evaluating a type parameter `X` as an expression, and the value of `X` is a type `C` that has a metaobject class From abdbf4145ef395a3cc754b451735ba8c2750e506 Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Tue, 25 Mar 2025 15:11:04 +0100 Subject: [PATCH 04/17] WIP --- .../4200-metaobjects/feature-specification.md | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index 42143fc76..99bd6c357 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -9,11 +9,11 @@ Version: 1.1 Experiment flag: metaobjects This document specifies the metaobjects feature, which is a feature that -allow a type `T` to be mapped into one or more kinds of objects known as +allows a type `T` to be mapped into one or more kinds of objects known as _metaobjects_ of that type. The main purpose of a metaobject is to serve as -a vehicle for late-bound invocations of static members and/or constructors -of the type `T`, but it is also possible to use the mechanism for other -purposes. +a vehicle for late-bound invocations of selected static members and/or +constructors of the type `T`, but it is also possible to use the mechanism +for other purposes. ## Introduction @@ -35,8 +35,8 @@ Here is an example: ```dart // Define the interface that we wish to support. abstract class Fooable { - const Fooable(); - String foo(X x); + const Fooable(); // A metaobject must be const-able. + String foo(X x); // This is a gateway to a static method. } // The `static implements` clause specifies that a @@ -55,13 +55,13 @@ class B static implements Fooable { } // Does not depend on `A` or `B`, but is still type safe. -void showFoo>(X x) { - X.foo(x); +String showFoo>(X x) { + return X.foo(x); } void main() { - showFoo(A("MyA"); - showFoo(B(42)); + print(showFoo(A("MyA")); + print(showFoo(B(42))); } ``` @@ -115,7 +115,7 @@ be an instance of one of these classes. These classes implement operator `==` and the getter `hashCode`, such that they can behave the same as objects of type `Type` do today, when they are -obtained by evaluating a type as an expression. +obtained by evaluating a type literal as an expression. The static type of the metaobject will be the metaobject class when the type literal which is being evaluated as an expression is a compile-time @@ -128,10 +128,11 @@ type literal is a type variable `X` that has a bound of the form As a special case (ensuring that this feature does not break existing code), the result of evaluating a type literal that denotes a class, a mixin, a mixin class, or an enum declaration that does _not_ have a -`static implements` clause has static type `Type`, and it works exactly the -same as today. So does the result of evaluating a type that does not denote -a declaration (that is, a function type, a record type, special types like -`dynamic`, union types like `T?` and `FutureOr`, etc.) +`static implements` or `static extends` clause has static type `Type`, and +it works exactly the same as today. So does the result of evaluating a type +that does not denote a declaration (that is, a function type, a record +type, special types like `dynamic`, union types like `T?` and +`FutureOr`, etc.) The previous example showed how we can use metaobjects to provide access to static members of a set of classes (or mixins, etc.) without depending on @@ -145,11 +146,13 @@ constructors in a similar way (yielding 'virtual constructors'): ```dart abstract class Creation { + const Creation(); X call(); X named(int _); } class C static implements Creation> { + C(); C.named(int _): this(); } @@ -202,11 +205,13 @@ For example: ```dart abstract class CallWithTypeArguments { + const CallWithTypeArguments(); int get numberOfTypeArguments; R callWithTypeArgument(int number, R callback()); } class _EStaticHelper implements CallWithTypeArguments { + const _EStaticHelper(); int get numberOfTypeArguments => 2; R callWithTypeArgument(int number, R callback()) { return switch (number) { @@ -400,7 +405,7 @@ class MetaC extends T implements Type { const MetaC(); - // Implicitly induced member implementations, same rules: + // Implicitly induced forwarding member implementations, same rules: ... } ``` @@ -432,7 +437,7 @@ Assume that _D_ is a class declaration named `C` which has a clause of the form `static implements T` or `static extends T`, and declares the formal type parameters `X1 extends B1 .. Xk extends Bk`. -Forwarding members are derived from the static members and as follows: +Forwarding members are derived from the static members as follows: If `static R get g ...` is a static member of `C` then the corresponding forwarding member is `R get g => C.g;`. @@ -442,7 +447,7 @@ derived from ``, then the corresponding forwarding setter is `set s(parm) => C.s = arg;`, where `arg` is the identifier which is declared as a positional parameter by `parm`. -*Static variable declarations are covered as getters and/or setters.* +*Static variable declarations are covered as setters and/or getters.* If `static R m(parms)` is a static method of `C` where `parms` is derived from ``, then the corresponding forwarding method is @@ -459,10 +464,11 @@ forwarding method is `R m(parms) => C.m(args);`, where identifiers declared as positional parameters in `parms`, followed by actual arguments of the form `id: id` for the named parameters in `parms`. -*Note that the type parameter bounds and optional parameter default values -are the same for the original declaration and the forwarding declaration. -This works because the metaobject class is placed in the same scope as the -original declaration.* +In the case where a default value resolves to a declaration in `C` and does +not have the prefix `C.`, this prefix is added when the corresponding +default value is specified in the metaobject class. *For example, +`int i = myDefault` in `C` will be `int i = C.myDefault` in the derived +forwarding declaration when `myDefault` is declared in `C`.* If `C(parms) ...` or `factory C(parms) ...` is a constructor declared by `C` then the corresponding forwarding declaration is @@ -480,6 +486,10 @@ derived from `parms` in the same way as in the previous case. ### Dynamic Semantics +It is allowed, but not required, for every metaobject to be constant if +possible. *Hence, programs should not depend on the identity of +metaobjects.* + When invoked on an object whose run-time type is `C`, in the case where `C` has a `static implements` or `static extends` clause, the implementation of the getter `runtimeType` in `Object` returns an instance From 55e2a549660b7dd08e3a7323931a05192202e940 Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Tue, 25 Mar 2025 17:43:09 +0100 Subject: [PATCH 05/17] WIP --- working/4200-metaobjects/feature-specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index 99bd6c357..af7f0f3ac 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -4,7 +4,7 @@ Author: Erik Ernst Status: Draft -Version: 1.1 +Version: 1.0 Experiment flag: metaobjects From d5e1530176c8a2d714243904e2a999b26d929cd4 Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Fri, 28 Mar 2025 11:50:20 +0100 Subject: [PATCH 06/17] Add the ability to invoke `C.foo()` meaning `(C).foo()` --- .../4200-metaobjects/feature-specification.md | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index af7f0f3ac..37762d2bc 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -371,6 +371,29 @@ member access on the metaobject of the value of that type variable.* *Note that this is not a breaking change because every member access on a type variable is a compile-time error in current Dart.* +Consider a member access whose receiver is a possibly qualified identifier +that denotes a class, mixin, mixin class, or enum declaration _D_ (e.g., +`C.foo()` or `prefix.C.foo()`). If the accessed member is not declared as a +static member or constructor in _D_ then the member access is treated as +the same member access where the receiver is parenthesized +(`(C).foo()` respectively `(prefix.C).foo()`). + +*This implies that a member of the statically implemented or extended +interface which isn't shadowed by a static member or a constructor can be +invoked as if it had been a static member or a constructor of D itself. You +could say that the static member or constructor is added to D in a way that +resembles the addition of extension instance members to an object.* + +A member access whose receiver is a parameterized type (e.g., +`prefix.C.foo()`) is treated as the same member access where the +receiver is parenthesized (`(prefix.C).foo()`). + +*This means that members in the interface of the metaobject class can be +invoked as if they were static members or constructors, but passing actual +type arguments. Thoese type arguments are available to the members of the +metaobject, so we could say that this introduces "static members that have +access to the type parameters or the enclosing class".* + #### Deriving the Metaobject Class Assume that _D_ is a class declaration named `C` which declares the formal @@ -466,7 +489,7 @@ actual arguments of the form `id: id` for the named parameters in `parms`. In the case where a default value resolves to a declaration in `C` and does not have the prefix `C.`, this prefix is added when the corresponding -default value is specified in the metaobject class. *For example, +default value is specified in the metaobject class. *For example, `int i = myDefault` in `C` will be `int i = C.myDefault` in the derived forwarding declaration when `myDefault` is declared in `C`.* From a262352c00ac228fdbb24b4e8ef6e418cb408fbf Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Fri, 28 Mar 2025 16:38:42 +0100 Subject: [PATCH 07/17] Review response --- .../4200-metaobjects/feature-specification.md | 491 +++++++++++------- 1 file changed, 309 insertions(+), 182 deletions(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index 37762d2bc..a1480622f 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -10,108 +10,128 @@ Experiment flag: metaobjects This document specifies the metaobjects feature, which is a feature that allows a type `T` to be mapped into one or more kinds of objects known as -_metaobjects_ of that type. The main purpose of a metaobject is to serve as -a vehicle for late-bound invocations of selected static members and/or +_metaobjects_ for that type. The main purpose of a metaobject is to serve +as a vehicle for late-bound invocations of selected static members and/or constructors of the type `T`, but it is also possible to use the mechanism for other purposes. ## Introduction -The _metaobjects_ feature allows programs to map a given type to an object, -known as a _metaobject_, which allows features associated with that type -such as static members and constructors to be supported without depending -on the exact type itself. +The _metaobjects_ feature maps a given type `T` to an object, known as a +_metaobject_, which allows features associated with the type `T` (such as +static members and constructors) to be invoked without depending on the +exact type itself. Current Dart only allows static members and constructors to be invoked by -specifying the exact declaration (of a class, mixin, mixin class, extension +denoting the exact declaration (of a class, mixin, mixin class, extension type, extension, or enum) that contains said static member or constructor declaration. For example, if a class `A` declares a static method `foo` -then we can call it using `A.foo(...)`, but we can not call it using +then we can call it using `A.foo(...)`, but we cannot call it using `X.foo(...)` where `X` is a type variable, even in the case where the value of `X` at run time is `A`. +The metaobjects feature allows this restriction to be lifted. It introduces +support for new clauses on type-introducing membered declarations (e.g., +classes and mixins), of the form `static implements T` or +`static extends T`, where `T` is an interface type (e.g., a class). + +These clauses specify superinterfaces that the metaobject class must +implement respectively extend. That is, it specifies members that we can +call on the metaobject. + Here is an example: ```dart // Define the interface that we wish to support. -abstract class Fooable { - const Fooable(); // A metaobject must be const-able. - String foo(X x); // This is a gateway to a static method. +abstract class PrettyPrintable { + String prettyPrint(X x); } // The `static implements` clause specifies that a // metaobject for `A` supports the given interface. -class A static implements Fooable { +class A static implements PrettyPrintable { final String name; A(this.name); - static String foo(A a) => "${a.name} fooing!"; + static String prettyPrint(A a) => a.name; } // Ditto. -class B static implements Fooable { +class B static implements PrettyPrintable { final int size; B(this.size); - static String foo(B b) => "B of size ${b.size} fooing!"; + static String foo(B b) => "B of size ${b.size}"; } // Does not depend on `A` or `B`, but is still type safe. -String showFoo>(X x) { - return X.foo(x); +String doPrettyprint>(X x) { + return X.prettyPrint(x); } void main() { - print(showFoo(A("MyA")); - print(showFoo(B(42))); + print(doPrettyprint(A("MyA")); + print(doPrettyprint(B(42))); } ``` -This works because the type variable `X` is evaluated to an object when it -is used as the receiver in an expression like `X.foo(x)`. The -_static bound_ on `X` which is specified as `static extends Fooable` -provides a compile-time guarantee that the actual argument that `X` is -bound to in any given invocation of `showFoo` will be a type such that -when it is evaluated as an expression, the resulting metaobject will have a -type which is a subtype of `Fooable`. In particular, it has a `foo` -method that has a positional parameter of type `X`, so we can safely call -it as `X.foo(x)`. +The type variable `X` is evaluated to an object when it is used as the +receiver in an expression like `X.prettyPrint(x)`, that is, it works +exactly like `(X).prettyPrint(x)`. In this expression, `(X)` will evaluate +the type `X` as an expression. In current Dart this yields an instance of +type `Type` that reifies the type which is the value of `X`. With the +metaobjects feature it still evaluates to a reified type object, but it is +now an instance of a class (the metaobject class) that has the specified +superinterfaces. + +When the value of `X` is `A`, this means that `(X)` is a subtype of +`PrettyPrintable`, and similarly for the case where `X` is `B`. + +The _static bound_ on `X` which is specified as +`static extends PrettyPrintable` provides a compile-time guarantee that +the actual argument that `X` is bound to in any given invocation of +`doPrettyprint` will be a type such that when it is evaluated as an +expression, the resulting metaobject will have a type which is a subtype of +`PrettyPrintable`. In particular, it has a `prettyPrint` method that has +a positional parameter of type `X`, so we can safely call it as +`X.prettyPrint(x)`. In main, it is statically ensured that the actual type argument of the two -invocations of `showFoo` satisfy this requirement: In the first invocation -the inferred type argument is `A`, and this is OK because -`A static implements Fooable`. Similarly for the second invocation with -the actual type argument `B`. - -The semantics that actually makes the invocation of `foo` call the static -method of `A` respectively `B` is established by an implicitly induced -class for each class that has a `static implements` clause, known as the -_metaobject class_. The metaobject class implements all members of the -specified interface by forwarding to a static member or a constructor of -the underlying class, mixin, mixin class, or enum declaration. +invocations of `doPrettyPrint` satisfy this requirement: In the first +invocation the inferred type argument is `A`, and this is OK because it is +known that `A static implements PrettyPrintable`. Similarly for the +second invocation with the actual type argument `B`. + +The semantics that actually makes the invocation of `prettyPrint` call the +static method of `A` respectively `B` is established by an implicitly +induced class for each class that has a `static implements` clause, known +as the _metaobject class_. The metaobject class implements all members of +the specified interface by forwarding to a static member or a constructor +of the underlying class, mixin, mixin class, or enum declaration. For example: ```dart -class MetaobjectForA implements Type, Fooable { - const MetaobjectForA(); - String foo(A a) => A.foo(a); +class MetaobjectForA implements Type, PrettyPrintable { + const MetaobjectForA(); // Every metaobject class must be const-able. + String prettyPrint(A a) => A.prettyPrint(a); bool operator ==(Object other) => ...; int get hashCode => ...; } -class MetaobjectForB implements Type, Fooable { - const MetaobjectForB(); - String foo(B b) => B.foo(b); +class MetaobjectForB implements Type, PrettyPrintable { + const MetaobjectForB(); // Must be const-able. + String prettyPrint(B b) => B.prettyPrint(b); bool operator ==(Object other) => ...; int get hashCode => ...; } ``` -These classes are implicitly generated by the compiler and analyzer, so we -can't refer to them in user code (the real name is probably something like -`$xBz8_`, and it's certainly a fresh name such that it doesn't coincide -with any name that a developer has written). However, when a type variable -like `X` above is evaluated as an expression, the resulting metaobject will -be an instance of one of these classes. +These classes are implicitly induced by the compiler and analyzer. We +can't refer to them in user code because the name is a fresh identifier +such that it doesn't coincide with any name that a developer has written. + +However, when a type variable like `X` above is evaluated as an expression, +the resulting metaobject will be an instance of the class which is the +metaobject class for the run-time value of `X`. These classes implement operator `==` and the getter `hashCode`, such that they can behave the same as objects of type `Type` do today, when they are @@ -123,47 +143,48 @@ constant type (for example, `var myMetaObject = A;`). In the case where the type literal is a type variable `X` that has a bound of the form `static extends I`, the metaobject has a static type which is a subtype of `Type` and a subtype of `I`. This implies that we can use the members of -`I` on that metaobject. E.g., we can call `X.foo(x)` in the example above. +`I` on that metaobject. E.g., we can call `X.prettyPrint(x)` in the example +above. As a special case (ensuring that this feature does not break existing code), the result of evaluating a type literal that denotes a class, a mixin, a mixin class, or an enum declaration that does _not_ have a `static implements` or `static extends` clause has static type `Type`, and it works exactly the same as today. So does the result of evaluating a type -that does not denote a declaration (that is, a function type, a record +that isn't introduced by a declaration (that is, a function type, a record type, special types like `dynamic`, union types like `T?` and `FutureOr`, etc.) The previous example showed how we can use metaobjects to provide access to static members of a set of classes (or mixins, etc.) without depending on the exact class (mixin, etc.) declaration. This basically means that we -have made the static members _virtual_, because we're calling them via -normal instance members (that is, virtual members, with all the usual OO -semantics) of the metaobject. +have made the static members _late bound_, because we're calling them via +normal instance members of the metaobject. In contrast, regular invocations +of static members and constructors are bound to a specific call target at +compile time, which means that the call site depends on that declaration. The next example illustrates how we can use metaobjects to call -constructors in a similar way (yielding 'virtual constructors'): +constructors in a similar way (yielding 'late-bound constructors'): ```dart -abstract class Creation { - const Creation(); +abstract class SimpleCreation { X call(); X named(int _); } -class C static implements Creation> { +class C static implements SimpleCreation> { C(); C.named(int _): this(); } -class D static implements Creation { +class D static implements SimpleCreation { factory D() = _DImpl; D.named(int _); } class _DImpl implements D {...} -X create>() => X(); +X create>() => X(); void main() { C c = create(); @@ -174,22 +195,21 @@ void main() { This illustrates that we can perform creation of instances of the given type argument (denoted by `X` in the declaration of `create`), in spite of the fact that the class `C` is generic (and the type argument `X` has the -value `C`, that is, it carries the actual type arguments with it), and -the constructor in `C` that we're using is the implicitly induced 'default' -constructor whose name is `C` (we could have written this explicitly as -`C();` or `C(): super();`). In contrast, the constructor that we're using -in `D` is a redirecting factory constructor. - -This works because the only requirement for a class `C` to static implement -`Creation` is that it must have a declaration which can be invoked as an -invocation of the type itself (`C()`), which is exactly what we get the -ability to do when the metaobject has a `call` method (that is, we can do -`X()` when `X` denotes an object that has a `call` method). +value `C`, that is, it carries the actual type argument with it), and +the constructor in `C` that we're using is the generative constructor whose +name is `C`. In contrast, the constructor that we're using in `D` is a +redirecting factory constructor. + +The only requirement for a class `C` to static implement +`SimpleCreation` is that it must have a declaration which can be invoked +as an invocation of the type itself (`C()`), which is exactly what we get +the ability to do when the metaobject has a `call` method (that is, we can +do `X()` when `X` denotes an object that has a `call` method). The constructors named `C.named` and `D.named` are treated similarly except that they are named. They can be invoked using expressions like -`X.named(42)` when `X` is a type variable that -`static extends Creation`. +`X.named(42)` when `X` is a type variable which is declared with the static +bound `static extends SimpleCreation`. When a class, mixin, mixin class, or enum declaration has a `static implements I` clause, the metaobject class for said type will have @@ -203,6 +223,69 @@ others as forwarders, with members which are not implemented otherwise). For example: +```dart +sealed class Animal {} + +class Fish extends Animal static extends AnimalStatics { + static bool get swims => true; +} + +class Bird extends Animal static extends AnimalStatics { + static bool get flies => true; + static bool get walks => true; +} + +class Mammal extends Animal static extends AnimalStatics { + static bool get walks => true; + static bool get swims => true; +} + +abstract class AnimalStatics { + const AnimalStatics(); // Must allow const-able subclasses. + bool get swims => false; + bool get flies => false; + bool get walks => false; +} + +void showCapabilities(X x) { + var capabilities = [ + if (X.walks) "walk", + if (X.swims) "swim", + if (X.flies) "fly", + ]; + print("$x can do the following: $capabilities"); +} + +void main() { + showCapabilities(Mammal()); +} +``` + +In this case the `static extends` feature is used to provide a default +implementation of a set of static members (`swims`, `flies`, `walks`). +The metaobject classes for the subclasses of `Animal` can declare any +subset of these static members if it wants to override their behavior, and +the rest are inherited from `AnimalStatics`. + +`Mammal.walks` can be invoked according to today's rules about static +members (nothing new here). However, `Mammal.flies` can be invoked because +this means `(Mammal).flies`. In other words, even though the invocation +includes a metaobject, clients can consider `Mammal` to be a class that has +all of these static members even though only some of them are actually +declared as static members in `Mammal`. The rest are "inherited" from the +metaobject class. + +In general, `static extends` offers developers greater expressive power +than `static implements` because it is possible for the metaobject to +inherit code that developers have written to do whatever they want. +Methods in a metaobject class which was induced by a `static implements` +clause, on the other hand, will only have methods whose implementation is a +forwarding invocation of a static member or constructor of the underlying +type. + +Here is an example where the metaobject is used to provide access to the +actual type arguments of a given object or type: + ```dart abstract class CallWithTypeArguments { const CallWithTypeArguments(); @@ -249,19 +332,38 @@ void main() { } catch (error) { // Some error recovery. } + + // We can also traverse the structure of a given type. + List> createSets() { + final result = >[]; + result.add({}); + final metaX = X; // Enable promotions. + if (metaX is CallWithTypeArguments) { + final maxNumber = metaX.numberOfTypeArguments; + for (int number = 1; number <= maxNumber; ++number) { + metaX.callWithTypeParameter(number, () { + result.addAll(createSets()); + }); + } + } + } + + createSets, Symbol>, double>>(); } ``` -The `static extends` clause allows metaobjects to have arbitrary -user-written code in members (not just forwarding members, as is the case -with `static implements`). - In this example, we're using it to provide a very basic kind of an 'existential open' operation. That is, we provide support for executing code in a scope where the actual value of each type parameter can be denoted. In the example we use this capability to test whether or not the given arguments have the required types. +In the first part of `main` this is used to get access to the actual type +arguments of an existing object. In the last part, it is used to get access +to the type arguments of a given _type_. The first part can be expressed +today by adding an instance member to the class, but the last part is not +expressible in current Dart. + Here is the corresponding implicitly induced metaobject class: ```dart @@ -301,21 +403,25 @@ so a metaobject for `Never` must, essentially, be an instance of set, so we can't promise that we can deliver an element that belongs to this set). -In summary, this feature can be said to introduce support for virtual -static members, virtual constructors, and type related behaviors including -the ones that rely on having explicit access to the actual type arguments -of the given type. +In summary, this feature can be said to introduce support for late-bound +static members, late-bound constructors, a kind of inheritance of static +members, plus type related behaviors including the ones that rely on having +explicit access to the actual type arguments of the given type. ## Specification ### Syntax -The grammar is adjusted as follows: +The grammar is adjusted as follows. The modifications extend some +type-introducing declarations (the exception is extension types) such that +they include `?`. Moreover, type parameters are extended +to include the corresponding static bound. ```ebnf ::= // New. - 'static' 'extends' - | 'static' 'implements' + ('static' 'extends' + ('with' )?)? + ('static' 'implements' )? ::= // Modified. ( | ) @@ -349,27 +455,30 @@ The grammar is adjusted as follows: ('static' 'extends' ) ``` -The modifications are generally that type introducing declarations (except -extension types) are extended to include `?`, and type -parameters are extended to include the corresponding static bound. - ### Static Analysis -Assume that _D_ is a class declaration named `C` which has a clause of the -form `static implements T` or `static extends T`. +A _metaobject capable_ declaration is a class, mixin, or enum declaration. + +Assume that _D_ is a metaobject capable declaration which has a clause +of the form `static implements T1 .. Tk`. In this case we say that each of +`T1 .. Tk` is a direct static superinterface and a declared static +superinterface of _D_. + +Assume that _D_ is a metaobject capable declaration which has a clause of +the form `static extends T with M1 .. Mk`. In this case we say that each of +`T` and `M1 .. Mk` is a declared static superinterface, and the class +denoted by `T with M1 .. Mk` is the direct static superclass of _D_. -A compile-time error occurs if `T` is not an interface type. +A compile-time error occurs if a metaobject capable declaration _D_ has a +declared static superinterface that denotes a type which is not an +interface type. A member access *(for example, a method, setter, or getter invocation such as `r.foo()`)* whose receiver is a type variable (`X.foo()`) is treated as the same member access where the receiver is parenthesized (`(X).foo()`). *This implies that the member access is treated as a member access on the -result of evaluating the type literal as an expression, i.e., it is a -member access on the metaobject of the value of that type variable.* - -*Note that this is not a breaking change because every member access on a -type variable is a compile-time error in current Dart.* +result of evaluating the type literal as an expression.* Consider a member access whose receiver is a possibly qualified identifier that denotes a class, mixin, mixin class, or enum declaration _D_ (e.g., @@ -396,47 +505,40 @@ access to the type parameters or the enclosing class".* #### Deriving the Metaobject Class -Assume that _D_ is a class declaration named `C` which declares the formal -type parameters `X1 extends B1 .. Xk extends Bk` and has a clause of the -form `static implements T` or `static extends T`. +Assume that _D_ is a metaobject capable declaration named `C` which +declares the formal type parameters `X1 extends B1 .. Xk extends Bk` and +has static superinterfaces *(that is, it has a clause of the form +`static implements ...` and/or a clause of the form `static extends ...`)*. -The _metaobject class_ for _D_ is an implicitly induced class which is -located in the same scope as _D_. +The metaobject class for _D_ is a class _M_ with a fresh name _N_. The +class _M_ has the same type parameter list as _D_, if any. -The name of the metaobject class is a fresh name, say, `MetaC`. The -implicitly induced metaobject class for _D_ is then declared as follows in -the case where the clause is `static implements T`: +If _T_ is a declared static interface of _D_, then the interface type +denoted by _T_, with type parameters of _D_ replaced by the corresponding +type parameters of _M_, is an immediate super-interface of _M_. -```dart -class MetaC implements Type, T { - const MetaC(); - - // Implicitly induced member implementations, obtained by - // deriving forwarding declarations from static members and/or - // constructors in `C`, including only the ones needed to - // implement `T`, plus operator `==` and `hashCode` to support - // today's behavior of `Type` instances: - ... -} -``` +The interface of the `Type` class is also a super-interface of _M_. If _T_ +is a declared static superclass of _D_ then the class denoted by _T_, with +type parameters of _D_ replaced by the corresponding type parameters of +_M_, is the superclass of _M_. -In the case where the clause is `static extends T`, the metaobject class is -implicitly induced with the following declaration: +The class _M_ has a constant non-redirecting generative constructor +with the name _N_. This constructor declares no formal parameters, has no +initializer list, and uses the superinitializer `super()`. -```dart -class MetaC - extends T implements Type { - const MetaC(); +The class _M_ overrides the getter `hashCode` and the operator `==` such +that an instance of _M_ with actual type arguments `T1 .. Tk` has the same +behavior with respect to those two members as a `Type` instance that +reifies the underlying type `C` in current Dart. - // Implicitly induced forwarding member implementations, same rules: - ... -} -``` +The class _M_ also declares a forwarding implementation of each member +named `m` of the interface of _M_ for which there is a static member of `C` +whose name is `m` or a constructor whose name is `C.m`. The derivation of forwarding members is specified in the next section. A compile-time error occurs if the metaobject class derived from _D_ has -any errors. +any compile-time errors. *For example, it is an error on the `static implements I` or `static extends I` clause if `I` has a member signature `String bar(int)`, @@ -446,87 +548,112 @@ and there is a derived member whose name is `bar`, but it has signature *Note that with `static extends C`, it is an error if `C` does not have a constant constructor whose name is `C`. This implies that metaobjects can be constant. This is possible in every case when the underlying type is -non-generic, and it may be possible in some cases when the underlying type -is generic.* +non-generic, and it is possible in some cases when the underlying type +is generic. In particular, it is possible in all cases where a type literal +is used as a constant expression because all type arguments are then +constant type expressions as well.* A compile-time error occurs if `C` does not declare a set of static members and/or constructors such that every unimplemented member of `T` can obtain a correctly overriding implementation by implicitly inducing a forwarding member to a static member or constructor of `C`. -#### Deriving Forwarding Members +#### Deriving Metaobject Class Members -Assume that _D_ is a class declaration named `C` which has a clause of the -form `static implements T` or `static extends T`, and declares the formal -type parameters `X1 extends B1 .. Xk extends Bk`. +Assume that _D_ is a class declaration named `C` which declares the type +parameters `X1 extends B1 .. Xk extends Bk` and has declared static +superinterfaces `T1 .. Tn`. -Forwarding members are derived from the static members as follows: +The implicitly induced members of the metaobject class _M_ of _D_ are +derived from the static members of _D_ as follows: -If `static R get g ...` is a static member of `C` then the corresponding -forwarding member is `R get g => C.g;`. +If `static R get g ...` is a static getter of _D_ then _M_ has an instance +getter named `g` whose return type is the type denoted by `R`. +When invoked, the getter will invoke the static getter named `g` of _D_ +and return the result of that invocation. -If `static set s(parm) ...` is a static setter of `C` where `parm` is -derived from ``, then the corresponding forwarding -setter is `set s(parm) => C.s = arg;`, where `arg` is the identifier which -is declared as a positional parameter by `parm`. +If `static set s(T id) ...` is a static setter of _D_ where `id` is an +identifier, then _M_ has an instance setter named `s=` whose parameter type +is the type denoted by `T`. When invoked, the setter will invoke the static +setter named `s=` of _D_ with the actual argument `id`. + +*Note that the setter in _M_ may be covariant if it overrides a setter with +the same name in a superinterface whose parameter has the `covariant` +modifier. In this case, each invocation of this setter will give rise to +a run-time type check on the actual argument.* *Static variable declarations are covered as setters and/or getters.* If `static R m(parms)` is a static method of `C` where `parms` is derived -from ``, then the corresponding forwarding method is -`R m(parms) => C.m(args);`, where `args` is a comma separated list of -identifiers declared as positional parameters in `parms`, followed by -actual arguments of the form `id: id` for the named parameters in `parms`. +from ``, then _M_ declares an instance method named +`m` with a return type which is denoted by `R` and a formal parameter list +with the same shape and names, and type annotations denoting the same types +as the corresponding type annotation of the static method `C.m`. For each +parameter in `C.m` that has a default value, the corresponding parameter +has the same default value in the `m` which is declared by _M_. When +invoked, the `m` in _M_ calls `C.m` with its positional parameters in +declaration order as positional arguments, plus named arguments of the form +`id: id` for each of its named parameters. Similarly, if `static R m(parms)` is a static method of `C` where `typeParms` is derived from ` (',' )*` -and `parms` is derived from ``, then the corresponding -forwarding method is `R m(parms) => C.m(args);`, where -`typeArgs` is a comma separated list of the type variables declared in -`typeParms` in declaration order, and `args` is a comma separated list of -identifiers declared as positional parameters in `parms`, followed by -actual arguments of the form `id: id` for the named parameters in `parms`. - -In the case where a default value resolves to a declaration in `C` and does -not have the prefix `C.`, this prefix is added when the corresponding -default value is specified in the metaobject class. *For example, -`int i = myDefault` in `C` will be `int i = C.myDefault` in the derived -forwarding declaration when `myDefault` is declared in `C`.* +and `parms` is derived from ``, then _M_ declares an +instance method named `m` with a return type which is denoted by `R`, type +parameters with the same names and in the same order as in `typeParms` and +with bounds denoting the same type assuming that the type variables have +the same binding, and a formal parameter list with the same shape and +names, and type annotations denoting the same types as the corresponding +type annotation of the static method `C.m`. For each parameter in `C.m` +that has a default value, the corresponding parameter has the same default +value in the `m` which is declared by _M_. When invoked, the `m` in _M_ +calls `C.m` with its type parameters in declaration order, with its +positional parameters in declaration order as positional arguments, and +with named arguments of the form `id: id` for each of its named parameters. If `C(parms) ...` or `factory C(parms) ...` is a constructor declared by `C` -then the corresponding forwarding declaration is -`C call(parms1) => C(args);`, here `args` are derived from `parms1` -in the same way as with the static method above, and `parms1` is derived -from `parms` by erasing `this.` and `super.` and inserting the inferred -types and default values that are implied by `this.` and `super.` in the -cases where those declared types and default values have been omitted. - -If `C.name(parms) ...` or `factory C.name(parms) ...` is a constructor -declared by `C` then the corresponding forwarding declaration is -`C name(parms1) => C.name(args);`, here `args` are derived from -`parms1` in the same way as with the static method above, and `parms1` is -derived from `parms` in the same way as in the previous case. +then _M_ declares an instance method named `call` with return type +`C` and a formal parameter list with the same shape and +names *(note that the name of `this.p` and `super.p` is `p`)*, and type +annotations denoting the same types as the corresponding type annotation of +the underlying constructor named `C` *(these type annotation in `C` may be +inferred based on the rules about initializing formals and +superparameters)*. When invoked, the `call` method returns the result of +invoking the constructor named `C` with actual type arguments `X1 .. Xk` +and values arguments corresponding to the parameter declarations. + +Similarly, if `C.name(parms) ...` or `factory C.name(parms) ...` is a +constructor declared by `C` then _M_ declares an instance method named +`name` with return type `C` and a formal parameter list with the +same shape and names, and type annotations denoting the same types as the +corresponding type annotation of the underlying constructor named `C`. +When invoked, the `name` method returns the result of invoking the +constructor named `C` with actual type arguments `X1 .. Xk` and value +arguments corresponding to the parameter declarations. ### Dynamic Semantics -It is allowed, but not required, for every metaobject to be constant if -possible. *Hence, programs should not depend on the identity of -metaobjects.* +It is allowed, but not required, for every metaobject to be constant, if +possible. With respect to canonicalization of metaobjects, the same rules +apply as the ones that specify canonicalization of reified type objects in +current Dart. + -When invoked on an object whose run-time type is `C`, in the case -where `C` has a `static implements` or `static extends` clause, the -implementation of the getter `runtimeType` in `Object` returns an instance -of the metaobject class for `C` with the same type arguments `T1 .. Tk`. +Assume that `o` is an object whose run-time type is `C`. Assume +that `C` has static superinterfaces. In this case, the implementation of +the getter `runtimeType` in `Object` with the receiver `o` returns an +instance of the metaobject class for `C` with the same type arguments +`T1 .. Tk`. *For example, `C().runtimeType` returns `MetaC()` -if `C` has the clause `static implements SomeInterface` and `MetaC` is the +(or a canonicalized object obtained from such an instance creation) if `C` +has the clause `static implements SomeInterface` and `MetaC` is the implicitly induced metaobject class for `C`.* In the case where a type `T` is introduced by a declaration that has a -`static implements` or a `static extends` clause, the step whereby this -type is evaluated as an expression yields the corresponding metaobject, -that is, an instance of the metaobject class for `T`, passing the same -actual type arguments as the ones which are part of `T`. +static superinterface, the step whereby this type is evaluated as an +expression yields the corresponding metaobject, that is, an instance of the +metaobject class for `T`, passing the same actual type arguments as the +ones which are part of `T`. *For example, if we are evaluating a type parameter `X` as an expression, and the value of `X` is a type `C` that has a metaobject class From 310aaa5239ca06ed953b9136aff6340fe64ef802 Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Fri, 28 Mar 2025 16:39:05 +0100 Subject: [PATCH 08/17] WIP --- working/4200-metaobjects/feature-specification.md | 1 - 1 file changed, 1 deletion(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index a1480622f..f5359cc8b 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -637,7 +637,6 @@ possible. With respect to canonicalization of metaobjects, the same rules apply as the ones that specify canonicalization of reified type objects in current Dart. - Assume that `o` is an object whose run-time type is `C`. Assume that `C` has static superinterfaces. In this case, the implementation of the getter `runtimeType` in `Object` with the receiver `o` returns an From 013a91692b380f53ec9ee388c5bfa3dcde76ca0f Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Fri, 28 Mar 2025 17:16:20 +0100 Subject: [PATCH 09/17] WIP --- working/4200-metaobjects/feature-specification.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index f5359cc8b..bc6f91b83 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -277,11 +277,10 @@ metaobject class. In general, `static extends` offers developers greater expressive power than `static implements` because it is possible for the metaobject to -inherit code that developers have written to do whatever they want. -Methods in a metaobject class which was induced by a `static implements` -clause, on the other hand, will only have methods whose implementation is a -forwarding invocation of a static member or constructor of the underlying -type. +inherit code that developers have written to do whatever they want. A +metaobject class which was induced by a `static implements` clause, on the +other hand, will only have methods whose implementation is a forwarding +invocation of a static member or constructor of the underlying type. Here is an example where the metaobject is used to provide access to the actual type arguments of a given object or type: From fc31a9351c1625830fa2c33cba57a7233882bf6b Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Fri, 28 Mar 2025 17:44:07 +0100 Subject: [PATCH 10/17] Typos --- .../4200-metaobjects/feature-specification.md | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index bc6f91b83..3cbb6c5e9 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -286,16 +286,16 @@ Here is an example where the metaobject is used to provide access to the actual type arguments of a given object or type: ```dart -abstract class CallWithTypeArguments { - const CallWithTypeArguments(); - int get numberOfTypeArguments; - R callWithTypeArgument(int number, R callback()); +abstract class CallWithTypeParameters { + const CallWithTypeParameters(); + int get numberOfTypeParameters; + R callWithTypeParameter(int number, R callback()); } -class _EStaticHelper implements CallWithTypeArguments { +class _EStaticHelper implements CallWithTypeParameters { const _EStaticHelper(); - int get numberOfTypeArguments => 2; - R callWithTypeArgument(int number, R callback()) { + int get numberOfTypeParameters => 2; + R callWithTypeParameter(int number, R callback()) { return switch (number) { 1 => callback(), 2 => callback(), @@ -313,10 +313,10 @@ void main() { final E e = E(); // When we don't know the precise type arguments we can't call - // `e.foo` safely. But `CallWithTypeArguments` can help! - final eType = e.runtimeType; - eType.callWithTypeArgument(1, () { - eType.callWithTypeArgument(2, () { + // `e.foo` safely. But `CallWithTypeParameters` can help! + final Object? eType = e.runtimeType; + eType.callWithTypeParameter(1, () { + eType.callWithTypeParameter(2, () { var potentialArgument1 = 'Hello'; var potentialArgument2 = 42; if (potentialArgument1 is X && potentialArgument2 is Y) { @@ -336,9 +336,9 @@ void main() { List> createSets() { final result = >[]; result.add({}); - final metaX = X; // Enable promotions. - if (metaX is CallWithTypeArguments) { - final maxNumber = metaX.numberOfTypeArguments; + final Object metaX = X; // Enable promotions. + if (metaX is CallWithTypeParameters) { + final maxNumber = metaX.numberOfTypeParameters; for (int number = 1; number <= maxNumber; ++number) { metaX.callWithTypeParameter(number, () { result.addAll(createSets()); From 54434eedeaf996520f5ed20a57b292e3c8886f0d Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Fri, 28 Mar 2025 17:55:26 +0100 Subject: [PATCH 11/17] WIP --- working/4200-metaobjects/feature-specification.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index 3cbb6c5e9..648ac2078 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -347,7 +347,15 @@ void main() { } } - createSets, Symbol>, double>>(); + // Returns a list containing the following sets: + // , Symbol>, double>>{} + // , Symbol>>{} + // {} + // >{} + // {} + // {} + // {} + final sets = createSets, Symbol>, double>>(); } ``` @@ -360,8 +368,8 @@ given arguments have the required types. In the first part of `main` this is used to get access to the actual type arguments of an existing object. In the last part, it is used to get access to the type arguments of a given _type_. The first part can be expressed -today by adding an instance member to the class, but the last part is not -expressible in current Dart. +today if we can add an instance member to the class, but the last part is +not expressible in current Dart. Here is the corresponding implicitly induced metaobject class: From 0c5f37000b13a14f0c7b9135311ae7932f1f119f Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Mon, 31 Mar 2025 15:33:48 +0200 Subject: [PATCH 12/17] WIP --- .../4200-metaobjects/feature-specification.md | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index 648ac2078..41f4d1dd2 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -59,7 +59,7 @@ class A static implements PrettyPrintable { class B static implements PrettyPrintable { final int size; B(this.size); - static String foo(B b) => "B of size ${b.size}"; + static String prettyPrint(B b) => "B of size ${b.size}"; } // Does not depend on `A` or `B`, but is still type safe. @@ -315,15 +315,17 @@ void main() { // When we don't know the precise type arguments we can't call // `e.foo` safely. But `CallWithTypeParameters` can help! final Object? eType = e.runtimeType; - eType.callWithTypeParameter(1, () { - eType.callWithTypeParameter(2, () { - var potentialArgument1 = 'Hello'; - var potentialArgument2 = 42; - if (potentialArgument1 is X && potentialArgument2 is Y) { - a.foo(potentialArgument1, potentialArgument2); // Safe! - } + if (eType is CallWithTypeParameters) { + eType.callWithTypeParameter(1, () { + eType.callWithTypeParameter(2, () { + var potentialArgument1 = 'Hello'; + var potentialArgument2 = 42; + if (potentialArgument1 is X && potentialArgument2 is Y) { + a.foo(potentialArgument1, potentialArgument2); // Safe! + } + }); }); - }); + } // If we didn't have this feature we could only do this: try { From e6c13c962ba54f376cada258eb3da1b6a6f6627d Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Mon, 31 Mar 2025 15:38:27 +0200 Subject: [PATCH 13/17] WIP --- working/4200-metaobjects/feature-specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index 41f4d1dd2..067c1a56b 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -314,7 +314,7 @@ void main() { // When we don't know the precise type arguments we can't call // `e.foo` safely. But `CallWithTypeParameters` can help! - final Object? eType = e.runtimeType; + final Object eType = e.runtimeType; // `Object`: Enable promotion. if (eType is CallWithTypeParameters) { eType.callWithTypeParameter(1, () { eType.callWithTypeParameter(2, () { From b7bddaab46722a8628af0983ece2fe3bed84816b Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Tue, 1 Apr 2025 13:39:40 +0200 Subject: [PATCH 14/17] WIP --- working/4200-metaobjects/feature-specification.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index 067c1a56b..aa2fa75de 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -489,6 +489,17 @@ the same member access where the receiver is parenthesized (`(X).foo()`). *This implies that the member access is treated as a member access on the result of evaluating the type literal as an expression.* +A term of the form `X.new` where `X` is a type variable whose static type +includes a member signature which is a method with the name `call` is +treated as `X.call`. + +*This implies that tear-offs and invocations of forwarders to constructors +can be expressed using the same syntax as statically resolved tear-offs and +invocations, e.g., `X.new` and `X.new(42)`. Note that these forms may also +invoke or tear off a method named `call` in situations where this method +does not forward the tear-off or invocation to a constructor. For example, +it could forward to a static method named `call` for some values of `X`.* + Consider a member access whose receiver is a possibly qualified identifier that denotes a class, mixin, mixin class, or enum declaration _D_ (e.g., `C.foo()` or `prefix.C.foo()`). If the accessed member is not declared as a From 3d63ebe07489af854cedf56bd0e5525e8b3f9635 Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Tue, 1 Apr 2025 13:44:44 +0200 Subject: [PATCH 15/17] WIP --- working/4200-metaobjects/feature-specification.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index aa2fa75de..fcc47770c 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -490,15 +490,15 @@ the same member access where the receiver is parenthesized (`(X).foo()`). result of evaluating the type literal as an expression.* A term of the form `X.new` where `X` is a type variable whose static type -includes a member signature which is a method with the name `call` is -treated as `X.call`. +includes a member signature which is a method with the name `call` with +return type `X` is treated as `X.call`. *This implies that tear-offs and invocations of forwarders to constructors can be expressed using the same syntax as statically resolved tear-offs and invocations, e.g., `X.new` and `X.new(42)`. Note that these forms may also invoke or tear off a method named `call` in situations where this method -does not forward the tear-off or invocation to a constructor. For example, -it could forward to a static method named `call` for some values of `X`.* +does not forward the invocation to a constructor. For example, it could +forward to a static method named `call` for some values of `X`.* Consider a member access whose receiver is a possibly qualified identifier that denotes a class, mixin, mixin class, or enum declaration _D_ (e.g., From 5d4ead687b1b2b39f1e26c586ba55c13ba3bc032 Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Tue, 1 Apr 2025 13:52:21 +0200 Subject: [PATCH 16/17] WIP --- working/4200-metaobjects/feature-specification.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index fcc47770c..94f49cd39 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -497,8 +497,8 @@ return type `X` is treated as `X.call`. can be expressed using the same syntax as statically resolved tear-offs and invocations, e.g., `X.new` and `X.new(42)`. Note that these forms may also invoke or tear off a method named `call` in situations where this method -does not forward the invocation to a constructor. For example, it could -forward to a static method named `call` for some values of `X`.* +does not forward the invocation to a constructor. For example, it could be +a method named `call` inherited due to a `static extends` clause.* Consider a member access whose receiver is a possibly qualified identifier that denotes a class, mixin, mixin class, or enum declaration _D_ (e.g., From 3db7641d84c0484a8b73b0acf7d485c18d6c3e4e Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Tue, 1 Apr 2025 13:53:19 +0200 Subject: [PATCH 17/17] WIP --- working/4200-metaobjects/feature-specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/working/4200-metaobjects/feature-specification.md b/working/4200-metaobjects/feature-specification.md index 94f49cd39..3be57a534 100644 --- a/working/4200-metaobjects/feature-specification.md +++ b/working/4200-metaobjects/feature-specification.md @@ -491,7 +491,7 @@ result of evaluating the type literal as an expression.* A term of the form `X.new` where `X` is a type variable whose static type includes a member signature which is a method with the name `call` with -return type `X` is treated as `X.call`. +return type `X` or a subtype thereof is treated as `X.call`. *This implies that tear-offs and invocations of forwarders to constructors can be expressed using the same syntax as statically resolved tear-offs and