Skip to content

Commit 02fc3ae

Browse files
authored
Create proposal-simple-lrhn.md
1 parent ef213c6 commit 02fc3ae

File tree

1 file changed

+188
-0
lines changed

1 file changed

+188
-0
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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

Comments
 (0)