Skip to content

Commit 3fda6f7

Browse files
committed
Address a bunch of records issues.
- Support constant records. Fix #2337. - Support empty and one-positional-field records. Fix #2386. - Re-add support for positional field getters Fix #2388. - Specify the behavior of `toString()`. Fix #2389. - Disambiguate record types in `on` clauses. Fix #2406.
1 parent 4c4c0ba commit 3fda6f7

File tree

1 file changed

+114
-25
lines changed

1 file changed

+114
-25
lines changed

working/0546-patterns/records-feature-specification.md

Lines changed: 114 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Author: Bob Nystrom
44

55
Status: In progress
66

7-
Version 1.5 (see [CHANGELOG](#CHANGELOG) at end)
7+
Version 1.6 (see [CHANGELOG](#CHANGELOG) at end)
88

99
## Motivation
1010

@@ -71,11 +71,8 @@ modified, but may contain references to mutable objects. It implements
7171
`hashCode` and `==` structurally based on its fields to provide value-type
7272
semantics.
7373

74-
A record may have only positional fields or only named fields, but cannot be
75-
totally empty. *There is no "unit type".* A record with no named fields must
76-
have at least two positional fields. *This prevents confusion around whether a
77-
single positional element record is equivalent to its underlying value, and
78-
avoids a syntactic ambiguity with parenthesized expressions.*
74+
A record may have only positional fields, only named fields, both, or none at
75+
all.
7976

8077
## Core library
8178

@@ -98,18 +95,17 @@ grammar is:
9895
```
9996
literal ::= record
10097
| // Existing literal productions...
101-
record ::= '(' recordField ( ',' recordField )* ','? ')'
98+
record ::= 'const'? '(' recordField ( ',' recordField )* ','? ')'
10299
recordField ::= (identifier ':' )? expression
103100
```
104101

105-
This is identical to the grammar for a function call argument list. There are a
106-
couple of syntactic restrictions not captured by the grammar. It is a
107-
compile-time error if a record has any of:
102+
This is identical to the grammar for a function call argument list (with an
103+
optional `const` at the beginning). There are a couple of syntactic restrictions
104+
not captured by the grammar. It is a compile-time error if a record has any of:
108105

109106
* The same field name more than once.
110107

111-
* No named fields and only one positional field. *This avoids ambiguity with
112-
parenthesized expressions.*
108+
* Only one positional field and no trailing comma.
113109

114110
* A field named `hashCode`, `runtimeType`, `noSuchMethod`, or `toString`.
115111

@@ -119,6 +115,21 @@ compile-time error if a record has any of:
119115
hidden state. Two such records might unexpectedly compare unequal even
120116
though all of the fields the user can see are equal.*
121117

118+
* A field name that collides with the synthesized getter name of a positional
119+
field. *For example: `('pos', $0: 'named')` since the named field '$0'
120+
collides with the getter for the first positional field.*
121+
122+
In order to avoid ambiguity with parenthesized expressions, a record with
123+
only a single positional field must have a trailing comma:
124+
125+
```dart
126+
var number = (1); // The number 1.
127+
var record = (1,); // A record containing the number 1.
128+
```
129+
130+
There is no syntax for a zero-field record expression. Instead, there is a
131+
static constant `empty` on `Record` that returns the empty record.
132+
122133
### Record type annotations
123134

124135
In the type system, each record has a corresponding record type. A record type
@@ -159,7 +170,7 @@ typeNotFunction ::= 'void' // Existing production.
159170
// New rules:
160171
recordType ::= '(' recordTypeFields ',' recordTypeNamedFields ')'
161172
| '(' recordTypeFields ','? ')'
162-
| '(' recordTypeNamedFields ')'
173+
| '(' recordTypeNamedFields? ')'
163174
164175
recordTypeFields ::= recordTypeField ( ',' recordTypeField )*
165176
recordTypeField ::= metadata type identifier?
@@ -171,24 +182,30 @@ recordTypeNamedField ::= metadata typedIdentifier
171182
```
172183

173184
*The grammar is exactly the same as `parameterTypeList` in function types but
174-
without `()`, `required`, and optional positional parameters since those don't
175-
apply to record types. A record type can't appear in an `extends`, `implements`,
185+
without `required`, and optional positional parameters since those don't apply
186+
to record types. A record type can't appear in an `extends`, `implements`,
176187
`with`, or mixin `on` clause, which is enforced by being a production in `type`
177188
and not `typeNotVoid`.*
178189

190+
The type `()` is the type of an empty record with no fields.
191+
179192
It is a compile-time error if a record type has any of:
180193

181194
* The same field name more than once.
182195

183-
* No named fields and only one positional field. *This isn't ambiguous, since
184-
there are no parenthesized type expressions in Dart. But there is no reason
185-
to allow single positional element record types when the corresponding
186-
record values are prohibited.*
196+
* Only one positional field and no trailing comma. *This isn't ambiguous,
197+
since there are no parenthesized type expressions in Dart. But prohibiting
198+
this is symmetric with record expressions and leaves the potential for
199+
later support for parentheses for grouping in type expressions.*
187200

188201
* A field named `hashCode`, `runtimeType`, `noSuchMethod`, or `toString`.
189202

190203
* A field name that starts with an underscore.
191204

205+
* A field name that collides with the synthesized getter name of a positional
206+
field. *For example: `(int, $0: int)` since the named field '$0' collides
207+
with the getter for the first positional field.*
208+
192209
### No record type literals
193210

194211
There is no record type literal syntax that can be used as an expression, since
@@ -201,6 +218,28 @@ var t = (int, String);
201218
This is a record expression containing two type literals, `int` and `String`,
202219
not a type literal for a record type.
203220

221+
### Ambiguity with `on` clauses
222+
223+
Consider:
224+
225+
```dart
226+
void foo() {
227+
try {
228+
;
229+
} on Bar {
230+
;
231+
}
232+
on(a, b) {;} // <--
233+
}
234+
```
235+
236+
Before, the marked line could only be declaring a local function named `on`.
237+
With record types, it could be a second `on` clause for the `try` statement
238+
whose matched type is the record type `(a, b)`. When presented with this
239+
ambiguity, we disambiguate by treating `on` as a clause for `try` and not a
240+
local function. This is technically a breaking change, but is unlikely to affect
241+
any code in the wild.
242+
204243
## Static semantics
205244

206245
We define **shape** to mean the number of positional fields (the record's
@@ -218,17 +257,18 @@ handle function typedefs.)
218257

219258
A record type declares all of the members defined on `Object`. It also exposes
220259
getters for each named field where the name of the getter is the field's name
221-
and the getter's type is the field's type.
222-
223-
Positional fields are not exposed as getters. *Record patterns in pattern
224-
matching can be used to access a record's positional fields.*
260+
and the getter's type is the field's type. For each positional field, it exposes
261+
a getter whose name is `$` followed by the number of preceding positional fields
262+
and whose type is the type of the field.
225263

226264
For example, the record expression `(1.2, name: 's', true, count: 3)` has a
227265
record type whose signature is like:
228266

229267
```dart
230268
class extends Record {
269+
double get $0;
231270
String get name;
271+
bool get $1;
232272
int get count;
233273
}
234274
```
@@ -283,17 +323,54 @@ fields are) and collection literals.
283323

284324
**TODO: Specify this more precisely.**
285325

326+
### Constants
327+
328+
A record expression in a constant context or beginning with `const` defines a
329+
constant record. A record expression starting with `const` establishes a const
330+
context for its fields. It is a compile-time error if a field of a constant
331+
record is not constant.
332+
333+
Since identity is definely loosely for records, an implementation is not
334+
required to canonicalize equivalent constant records.
335+
336+
```dart
337+
print(identical(const (1, 2), const (1, 2)));
338+
```
339+
340+
This may print `true` or `false`.
341+
286342
## Runtime semantics
287343

288344
### Records
289345

290-
#### Members
346+
#### Field getters
291347

292348
Each field in the record's shape exposes a corresponding getter. Invoking that
293349
getter returns the value provided for that field when the record was created.
294350
Record fields are immutable and do not have setters.
295351

296-
The `toString()` method's behavior is unspecified.
352+
#### `toString()`
353+
354+
In debug builds, the `toString()` method converts each field to a string by
355+
calling `toString()` on its value and prepending it with the field name followed
356+
by `: ` if the field is named. It concatenates these with `, ` as a separator
357+
and returns the resulted surrounded by parentheses. For example:
358+
359+
```dart
360+
print((1, 2, 3).toString()); // "(1, 2, 3)".
361+
print((a: 'str', 'ing').toString()); // "(a: str, int)".
362+
```
363+
364+
The order that named fields appear and how they are interleaved with positional
365+
fields is unspecified. Positional fields must appear in position order. *This
366+
gives implementations freedom to choose a canonical order for named fields
367+
independent of the order that the record was created with.*
368+
369+
In a release or optimized build, the behavior of `toString()` is unspecified.
370+
*This gives implementations freedom to discard the full names of named fields in
371+
order to reduce code size.* Users should only use `toString()` on records for
372+
debugging purposes. They are strongly discouraged from parsing the results of
373+
calling `toString()` or relying on it for end-user visible output.
297374

298375
#### Equality
299376

@@ -379,6 +456,18 @@ covariant in their field types.
379456

380457
## CHANGELOG
381458

459+
### 1.6
460+
461+
- Support constant records (#2337).
462+
463+
- Support empty and one-positional-field records (#2386).
464+
465+
- Re-add support for positional field getters (#2388).
466+
467+
- Specify the behavior of `toString()` (#2389).
468+
469+
- Disambiguate record types in `on` clauses (#2406).
470+
382471
### 1.5
383472

384473
- Make the grammar for record types closer to function type parameter lists.

0 commit comments

Comments
 (0)