Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 273 additions & 0 deletions working/3102 - implied-name/feature-specification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
# Dart Implied Parameter/Record Field Names

Author: [email protected]<br>
Version: 1.0

## Pitch
Writing the same name twice is annoying. That happens often when forwarding
named arguments:
```dart
var subscription = stream.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
```

To avoid redundant repetition, Dart will allow you to omit the
argument name if it's the same name as the value.

```dart
var subscription = stream.listen(
onData,
:onError,
:onDone,
:cancelOnError,
);
```

Same applies to record literal fields, where we have nice syntax
for destructuring, but not for re-creating:
```dart
typedef Color = ({int red, int green, int blue, int alpha});
extension on Color {
Color withAlpha(int alpha) {
var (:red, :green, :blue, alpha: _) = this;
return (red: red, green: green, blue: blue, alpha: alpha);
}
}
```
will become
```dart
extension on Color {
Color withAlpha(int alpha) {
var (:red, :green, :blue, alpha: _) = this;
return (:red, :green, :blue, :alpha);
}
}
```

## Specification

The current grammar for a named argument is:
```ebnf
<namedArgument> ::= <label> <expression>
```
The [current grammar](../../accepted/3.0/records/feature-specification.md#record-expressions) for a record literal field:
```ebnf
recordField ::= (identifier ':' )? expression
```
So basically the same.

The new grammar is:
```ebnf
<namedArgument> ::= <namedExpression>

<recordField> ::= <namedExpression> | <expression>

<namedExpression> ::= <identifier>? `:' <expression>
```

This grammar change does not introduce any new ambiguities.
A named record field and a named argument can *only* occur
immediately after a `(` or a `,`, as the entire content until
a following `,` or `)`.
Starting with a `:` at that point is not currently possible.
(Even if we allow metadata annotations inside argument lists or record literals,
it's unambiguous whether those annotations includes a following identifier or
not.)

As a non-grammatical restriction, it's a **compile-time error**
if a `<namedExpression>` omits the leading`<identifier>`, and the following
expressions is not a _single identifier expression_, as defined below.

In short, an expression is a _single identifier expression_
which has a specific single _identifier_ if and only if
it is one of the following:
* An identifier, with that identifier.
* `s!`, `s as T`, `(s)`, `s..cascadeSection`/`s?..cascadeSection`
where `s` is a single identifier expression,
and then it has the same identifier as `s`.

More formally, An expression `e` is a single identifier expression with a certain identifier if and only if it is defined as such by the following:

* If `e` is a `<primary>` expression which is an `<identifier>`,
it is a single identifier expression with that `<identifier>` as identifier.
* `s!`: If `e` is a `<primary> <selector>*` production where `<selector>*`
is the single `<selector>` `` `!' ``, and `<primary>` is
a single identifier expression, then `e` is a single identifier expression
with the same identifier as the `<primary>`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the interest of completeness, I'm compelled to ask about (:foo++) and (:foo--) as well.

(I could go either way with them. But I'd lean towards "no".)

Copy link
Member Author

@lrhn lrhn Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably ... they work and stay within the bounds set out.

The value of foo++ was the value of foo (which has since been incremented).
That means that the meaning of foo-the-parameter is likely the same as foo-the-variable, since the same value is used for both. That's my mental check for whether this makes sense: We write only one name to cover two things, so the name should mean "the same thing" in both uses.

The argument against :foo++ is that we won't allow :++foo, and that asymmetry can be grating.

Or we could allow :++foo too. I'd be perfectly happy with both - they are "one identifier expressions" and the value of the expression is the value of the variable - either what it was before increment, or what it is now, after increment. As long as people don't do patological things with setters. The name is the next identifier in the expression, and it'll be prefixed at most by (s and then at most one ++ or --, the name is easy to find.

And also that if :x++ is allowed, but :x+=1 is not, even if it's definitionally the same, it's a little special-casey. You can use it if you need to add 1, but not if you need to add 2?

But it's a single identifier with the same name and its value is used as the value, so it's not completely out of bounds.

(My biggest argument against cascades is that they contain more identifiers. This one doesn't.
Even if the first one is easy to find, it is still messy. (And more if we'd want :a.b.) Increment expressions don't suffer from that.)

Copy link
Member

@munificent munificent Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we could allow :++foo too.

If we did that, then we'd also probably want :-foo, :!foo, and :~foo.

My inclination would to not do any of those. I'd even be OK with not allowing foo!, but I think that one's worth doing because (a) patterns allow it and (b) it makes nullable types easier to work with, which is always good.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue against -foo, !foo and ~foo in that they don't mean the same thing as the name. They likely mean the exact opposite.

Doing foo(isOpen: !isOpen) or foo(count: -count) would look wrong, and allowing foo(:!isOpen) and foo(:-count) would hide that.

The only operators allowed are ones that do not change the value of the identifier expression, they just might throw before evaluating to that value (! or as Type).

For id++ and ++id, the value of the expression is the value of the variable, either before or after the variable is incremented, that's why they could be argued to fit. But as I said, a mutable variable is mutable state, being the state can mean something else than just its value. This feature is mainly intended for
forwarding values from one place to another with the same name.

* `s as T`: If `e` is a `<relationalExpression>` of the form
`<bitwiseOrExpression> <typeCast>` and the `<bitwiseOrExpression>`
is a single identifier expression,
then `e` is a single identifier expression with the same identifier as
the `<bitwiseOrExpression>`.
* `(s)`: If `e` is a `<primary>` production of the form
`` `(' <expression> `)' `` and the `<expression>`
is a single-identifier expression,
then `e` is a single identifier expression with the same identifier as
the `<expression>`.
* `s..cascade`:
* If `e` a `<cascade>` of the form ``<cascade> `..' <cascadeSection>``
and the `<cascade>` is a single identifier expression,
then `e` is a single identifier expression with the same identifier as
the `<cascade>`.
* If `e` a `<cascade>` of the form
``<conditionalExpression> (`?..' | `..') <cascadeSection>``
and the `<conditionalExpression>` is a single identifier expression,
then `e` is a single identifier expression with the same identifier as
the `<conditionalExpression>`.

The _name of a named expression_ is then:
* If the named expression has a leading identifier, then that identifier.
* Otherwise the following expression must be a single identifier expression
with an identifier *I*, and then the name of the named expression is *I*.

The name of a `<namedArgument>` or a named `<recordField>` is the name of
its `<namedExpression>`.


Where the language specification refers to a named argument's name,
it now uses this definition of the name of a `<namedArgument>`.

Where the Record specification for a record literal refers to a field name,
it now uses to this definition.

## Semantics

There are no changes to static or runtime semantics, other than extending the
notion of "name of a named argument" and "name of a field" to the name of the
single identifier expression following an unnamed `:`.

After that, there is no distinction between `(name: name,)` and `(: name,)`,
both syntaxes introduce a named argument or record field with name `name`
and expression `name`, and the semantics is only defined in terms of those
properties.


## Examples

From `sdk/lib/_http/http_impl.dart` (among many others):
```dart
return _incoming.listen(
onData,
:onError,
:onDone,
:cancelOnError,
);
```

From `pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart`:
```dart
return new SwitchStatementTypeAnalysisResult(
:hasDefault,
:isExhaustive,
:lastCaseTerminates,
:requiresExhaustivenessValidation,
:scrutineeType,
:switchCaseCompletesNormallyErrors,
:nonBooleanGuardErrors,
:guardTypes,
);
```

From `pkg/dds/lib/src/dap/isolate_manager.dart`:
```dart
final uniqueBreakpointId = (:isolateId, :breakpointId);
```

## Migration

None.

## Implementation

Experiment flag name could be `implicit-names`.

## Tooling

Analysis server can suggest removing a redundant name before a `:`.

There should *probably* be a lint to report when an argument name is redundant.
Because if there isn't, someone will almost immediately ask for one.
Whether one likes omitting the name or not is a matter of taste, so it should
be an optional lint, not just a warning. It should be easy to have a fix.

A _rename operation_ may need to recognize if it changes a parameter
name or an identifier used as argument, and insert an argument name
if now necessary. That is:
* If a name changes, then if any occurrences of that name is in
single identifier position of an implicitly named argument,
the old name must be inserted as explicit argument name.
* If a named parameter name changes, then any invocation of the function
which uses an implicitly named argument must have the new name
inserted as explicit argument name.

A rename can also introduce new possibilities for removing argument names.
(It may be better to not do that automatically, and rely on the user fixing
the positions afterwards. Or only do it automatically if the lint is enabled.)

## Discussion

The definition of "single identifier expression" is the place this feature
can be tweaked.

It's currently restricted to expressions where the value of the expression
is always the value of evaluating a single identifier.
That identifier is always the *next* identifier, and the next non-`(` token.

The identifier expression can be wrapped in casts `!` or `as T`, in parentheses,
or can be pre-used/modified using cascade invocations, but none of those
operations change the value from evaluating the identifier, only, potentially,
whether it evaluates to a value at all.

### Cascades

The cascade sections are the most syntactically intrusive. They can contain
any other expression, including other identifiers.
However, the places where cascades are used are often in argument position.

Examples of uses that could be made shorter (from `package:csslib`):
```dart
stylesheet = parseCss(input, errors: errors..clear());
```
which would become:
```dart
stylesheet = parseCss(input, :errors..clear());
```

An example from Flutter:
```dart
TextSpan(text: offScreenText, recognizer: recognizer..onTap = () {})
```
which would become:
```dart
TextSpan(text: offScreenText, :recognizer..onTap = () {})
```

It is the use that has the biggest risk of being confusing to read,
but you can always write the identifier if you prefer it.

### Assignments

An expression of the form `id1 = id2` is an expression which has the same
value as a single identifier, `id2`.
(We don't know what assigning to `id1` means, or if it can even be read,
but the value of the expression is the value of evaluating `id2`.)

It's not included because that identifier is not the next identifier
of the expression, and because it can easily be confusing which identifier
defines the name. (And more so for more steps, like `:foo = bar = baz`.)


### Future additions.

It's possible to extend the "single identifier expression" definition
to more expressions in the future without breaking any code existing
at that point.

## Revision history

* 1.0: Initial version