|
| 1 | +# Dart static access shorthand |
| 2 | +Author: [email protected]< br>Version: 0.9 |
| 3 | + |
| 4 | + |
| 5 | +Pitch: You can write `.foo` instead of `ContextType.foo` when it makes sense. |
| 6 | + |
| 7 | +### Elevator pitch |
| 8 | + |
| 9 | +An expression starting with `.` is an implicit static namespaces/class access on the context type. |
| 10 | + |
| 11 | +The type that the context expects is known, and the expression avoids repeating the type, and gets a value from that type. |
| 12 | + |
| 13 | +This makes immediate sense for accessing enum and enum-like constants or invoking constructors, which will have the desired type. There is no requirement that the expression ends at that member access, it can be followed by non-assignment selectors. |
| 14 | + |
| 15 | +There must be a context type that allows static member access. |
| 16 | + |
| 17 | +We also special-case the `==` and `!=` operators, but nothing else. |
| 18 | + |
| 19 | +### Specification |
| 20 | + |
| 21 | +### Grammar |
| 22 | + |
| 23 | +We introduce grammar productions of the form: |
| 24 | + |
| 25 | +```ebnf |
| 26 | +<primary> ::= ... |
| 27 | + | <staticMemberShorthand> |
| 28 | + |
| 29 | +<staticMemberShorthand> ::= |
| 30 | + `const` '.' (<identifier> | 'new') <argumentPart> |
| 31 | + | '.' (<identifier> | 'new') |
| 32 | +
|
| 33 | +<constantPattern> ::= ... ;; all the current cases |
| 34 | + | <staticMemberShorthand> |
| 35 | +``` |
| 36 | + |
| 37 | +and we add `.` to the tokens that an expression statement cannot start with. _(Just to be safe. If we ever allow metadata on statements, we don’t want `@foo . bar - 4 ;` to be ambiguous. If we ever allow metadata on expressions, we have bigger issues.)_ |
| 38 | + |
| 39 | +This is a simple grammatical change. |
| 40 | + |
| 41 | +A primary expression cannot follow any other complete expression, something which would parse as an expression that `e.id` could be a member access on. We know that because a primary expression already contains the production `'(' <expression> ')'` which would cause an ambiguity for `e1(e2)` if `(e2)` could also be parsed as a `<primary>`. |
| 42 | + |
| 43 | +A primary expression *can* follow a `?` in a conditional expression: `{e1?.id:e2}`. This could be ambiguous, but we handle it at tokenization time by making `?.` a single token, so there is no ambiguity here, just potentially surprising parsing if you omit a space between `?` and `.id`. It’s consistent, and a solved problem. |
| 44 | + |
| 45 | +### Semantics |
| 46 | + |
| 47 | +Dart semantics, static and dynamic, does not follow the grammar precisely. For example, a static member invocation expression of the form `C.id<T1>(e2)` is treated as an atomic entity for type inference (and runtime semantics), it’s not a combination of doing a `C.id` tear-off, then a `<T1>` instantiation and then an `(e2)` invocation. The context type of that entire expression is used throughout the inference, where `(e1.id<T1>)(e2)` has `(e1.id<T1>)` in a position where it has *no* context type. _(For now, come selector based inference, it may have something, but a selector context is not a type context, and it won’t be the context type of the entire expression)._ |
| 48 | + |
| 49 | +Because of that, the specification of the static and runtime semantics of the new constructs need to address all the forms <Code>.*id*</code>, <code>.*id*\<*typeArgs*\></code>, <code>.*id*(*args*)</code>, <code>.*id*\<*typeArgs*\>(*args*)</code>, `.new` or <code>.new(*args*)</code>. |
| 50 | + |
| 51 | +_(It also addresses `.new<typeArgs>` and `.new<typeArgs>(args)`, but those will always be compile-time errors because `.new` denotes a non-generic function, if it denotes anything.)_ |
| 52 | + |
| 53 | +The *general rule* is that any of the expression forms above, starting with <code>.id</code>, are treated exactly *as if* they were prefixed by a fresh variable, <code>*X*</code> which denotes an accessible type alias for the greatest closure of the context type scheme of the expression. |
| 54 | + |
| 55 | + |
| 56 | + |
| 57 | +#### Type inference |
| 58 | + |
| 59 | +In every place where the current specification specifies type inference for one of the forms <Code>*T*.*id*</code>, <code>*T*.*id*\<*typeArgs*\></code>, <code>*T*.*id*(*args*)</code>, <code>*T*.*id*\<*typeArgs*\>(*args*)</code>, <code>*T*.new</code> or <code>*T*.new(*args*)</code>, where *T* is a type clause or and identifier denoting a type declaration or a type alias declaration, we introduce a parallel “or <code>.id…</code>” clause, and then continue either with the type denoted by *T* as normal, or, for the <code>.*id*</code> clause, with the greatest closure of the context type scheme, and the *`id`* is looked up in that just as one would in the type denoted by *`T`* for *`T.id`*. |
| 60 | + |
| 61 | +That makes it is a compile-time error if the greatest closure of the context type scheme is not a type with a static namespace, so not a type introduced by a `class`, `enum`, `mixin`, or `extension type` declaration. _(There is no way to refer to a static `extension` namespace this way, since it introduces no type.)_ The same is the case for an explicit static member access like `dynamic.id` or `X.id`. |
| 62 | + |
| 63 | +Whichever static member or constructor the *`.id`* denotes, it is remembered for the runtime semantics. |
| 64 | + |
| 65 | +#### Special case for `==` |
| 66 | + |
| 67 | +For `==`, we special case the context type that is applied to a `.id`. |
| 68 | + |
| 69 | +If an expression has the form `e1 == e2` or `e1 != e2` , then |
| 70 | + |
| 71 | +* If `e1` starts with an implicit static access then: |
| 72 | + * Let *S2* be the static type of `e2` with context type scheme `_`. |
| 73 | + * Let *S1* be the static type of `e1` with context type *S2*. |
| 74 | + * Let <code>*R* Function(*T*)</code> be the function signature of `operator==` of *S1*. |
| 75 | +* Otherwise: |
| 76 | + * Let *S1* be the static type of `e1` with context type scheme `_`. |
| 77 | + * Let <code>*R* Function(*T*)</code> be the function signature of `operator==` of *S1*. |
| 78 | + * If `e2` *starts with an implicit static access*, and *T* is a supertype of `Object`, then let *S2* be the static type of `e2` with context type *S1*. |
| 79 | + * Otherwise let *S2* be the static type of `e2` with context type *T*. |
| 80 | +* It’s a compile-time error if *S2* is not assignable to <code>*T*?</code>. |
| 81 | +* The static type of the expression is *R*. |
| 82 | + |
| 83 | +An expression *starts with an implicit static access* if and only if one of the following: |
| 84 | + |
| 85 | +* The expression is an `<implicitStaticAccess>.` |
| 86 | +* The expression is `(e)` and `e` starts with an implicit static access. |
| 87 | +* The expression is `e..<cascadeSelector>` or `e?..<cascadeSelector>` and `e` starts with an implicit static access. |
| 88 | +* The expression is `e1 ? e2 : e3` and at least one of `e2` or `e3` starts with an implicit static access. |
| 89 | +* The expression is `e <selector>*` and `e` starts with an implicit static access. |
| 90 | + |
| 91 | +#### Runtime semantics |
| 92 | + |
| 93 | +Similar to type inference, in every place where we specify an explicit static member access or invocation, we introduce a clause including the <Code>.*id*…</code> variant too, the “implicit static access”, and refer to type inference for “the declaration denoted by <code>*id*</code> as determined during type inference”, then invoke it the same way an explicit static access would. |
| 94 | + |
| 95 | +#### Patterns |
| 96 | + |
| 97 | +A constant pattern is treated the same as the expression, with the matched value type used as typing context, and then the expression must be a constant expression. Since a constant pattern cannot occur in a declaration pattern, there is no need to assign an initial type scheme to the pattern in the first phase of the three-step inference. _If there were, the type scheme would be `_`._ |
| 98 | + |
| 99 | +#### Constant expressions |
| 100 | + |
| 101 | +The form starting with `const` is inferred in the same way, and then the identifier must denote a constant constructor, and the expression is then a constant constructor invocation of that constructor. |
| 102 | + |
| 103 | +An expression without a leading `const` is a potential constant and constant expression if the corresponding explicit static access would be one. |
| 104 | + |
| 105 | +## New complications and concerns |
| 106 | + |
| 107 | +The `.id` access is a static member access which cannot be resolved before type inference. |
| 108 | + |
| 109 | +Prior to this feature, static member accesses could always be resolved using only the lexical scopes and declaration namespaces, which does not require type inference. |
| 110 | + |
| 111 | +Similarly, it’s not known whether `.id` is a valid potentially constant or constant expression until it’s resolved what it refers to. This may delay some errors until after type inference. |
| 112 | + |
| 113 | +It’s not clear that this causes any problems, but it may need implementations to adapt, if they assumed that all static member accesses could be known (and the rest tree-shaken) before type inference. |
| 114 | + |
| 115 | +## Possible variations |
| 116 | + |
| 117 | +### Grammar |
| 118 | + |
| 119 | +Instead of introducing a new primary, we can make it a `<postfixExpression>`: |
| 120 | + |
| 121 | +```ebnf |
| 122 | +<postfixExpression> ::= <assignableExpression> <postfixOperator> |
| 123 | + | <primary> <selector>* |
| 124 | + | <staticMemberShorthand <selector>* |
| 125 | + |
| 126 | +<staticMemberShorthand> ::= |
| 127 | + `const` '.' (<identifier> | 'new') <argumentPart> |
| 128 | + | '.' (<identifier> | 'new') |
| 129 | +
|
| 130 | +<constantPattern> ::= ... ;; all the current cases |
| 131 | + | <staticMemberShorthand> |
| 132 | +``` |
| 133 | + |
| 134 | +where we recognize a `<staticMemberShorthand> <selector*>` and use the context type of the entire selector chain as the lookup type for the `.id`, and it can then continue doing things after than, like: |
| 135 | + |
| 136 | +```dart |
| 137 | +Range range = .new(0, 100).translate(userDelta); |
| 138 | +``` |
| 139 | + |
| 140 | +Here the `.new(0, 100)` itself has no context type, but the entire selector chain does, and that is the type used for resolving `.new`. |
| 141 | + |
| 142 | +This should allow more expressions to use the context. It may make it easier to get a *wrong* result, but you can do that in the first step if `.foo()` returns something of a wrong type. |
| 143 | + |
| 144 | +An expression of the form `.foo.bar.baz` might not be considered an *assignable* expression, so you can’t do `SomeType v = .current = SomeType();`. An expression of the form `.something` should *produce* the value of the context type, that’s why it’s based on the context type, and an assignment produces the value of its right-hand side, not of the assignable expression. |
| 145 | + |
| 146 | +On the other hand, an assignable expression being assigned to gets no context type, so that would automatically not work, and we may not need to make an exception. |
| 147 | + |
| 148 | +And for `.current ??= []`, which *sometimes* does and sometimes doesn’t produce the value, it might be convenient. However, the context type of `.current` in `Something something = .current ??= Something(0);` will be `Something?`, a union type which does not denote a type declaration, so it wouldn’t work either. For `Something something = .current += 1;`, the context type of `.current` needs to be defined. It probably has none today because assignable expressions cannot use a context type for anything. |
| 149 | + |
| 150 | +All in all, it doesn’t seem like an implicit static member access can be assigned to and have a useful context type at the same time, so effectively they are not assignable. |
| 151 | + |
| 152 | +#### Nullable types |
| 153 | + |
| 154 | +Should a nullable context type, `Foo?` look for members in `Foo`. Or in `Foo` *and* `Null`. (Which will make more sense when we get static extensions.) |
| 155 | + |
| 156 | +It would allow `Foo x = .current ?? Foo(0);` to work, which it doesn’t today when the context type of `.current` is `Foo?`, and a union type doesn’t denote a static namespace. |
| 157 | + |
| 158 | +It’s an option we can add later if there is big demand. Which there might be, because otherwise it makes a difference whether you declare your method as: |
| 159 | + |
| 160 | +```dart |
| 161 | +void foo([Foo? foo]) { |
| 162 | + foo ??= const Foo(null); |
| 163 | + // ... |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +or |
| 168 | + |
| 169 | +```dart |
| 170 | +void foo([Foo foo = const Foo(null)]) { |
| 171 | + // ... |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +which are both completely valid ways to write the same function. It makes a difference because the latter can be called as `foo(.someFoo)` and the former cannot. I’m not sure we *want* to cause that kind of forced choices on API design. |
| 176 | + |
| 177 | +#### Asynchrony and other element types |
| 178 | + |
| 179 | +If we say that a type is the authority on creating instances of itself, it *might* also be an authority on creating those instances *asynchronously*. With a context type of `Future<Foo>`, should we check the `Foo` declaration for a `Future<Foo>`-returning function, or just the `Future` class? If do we check `Foo`, we should probably check be both. |
| 180 | + |
| 181 | +This gets even further away from being simple, and it special cases the `Future` type. |
| 182 | + |
| 183 | +While `Future` is special, it’s not *that* special, and we could equally well have a context type of `List<Foo>` and decider to ask `Foo` for such a list. For enums, that’s even useful: `var fooSet = EnumSet<Foo>(.values)`. |
| 184 | + |
| 185 | +So probably a “no” to this. |
| 186 | + |
| 187 | +## Versions |
| 188 | +0.9: First version, for initial comments. |
0 commit comments