Skip to content
Merged
Changes from all 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
137 changes: 80 additions & 57 deletions accepted/future-releases/primary-constructors/feature-specification.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Declaring and Primary Constructors
# Declaring Constructors

Author: Erik Ernst

Status: Accepted

Version: 1.9
Version: 1.10

Experiment flag: declaring-constructors

Expand Down Expand Up @@ -243,9 +243,9 @@ constructor parameter is `final`.

In the case where the declaration is an `extension type`, the modifier
`final` on the representation variable can be specified or omitted. Note
that an extension type declaration is specified to use a primary
constructor (in that case there is no other choice, it is in the grammar
rules):
that an extension type declaration is specified to use a declaring
constructor (it is not supported to declare the representation variable
using a normal instance variable declaration):

```dart
// Using a primary constructor.
Expand Down Expand Up @@ -619,6 +619,13 @@ constructors as well.
'const' <constructorName> <formalParameterList>
| <declaringConstantConstructorSignature>;

<constructorName> ::= // Modified rule.
<typeIdentifierOrNew> ('.' identifierOrNew)?;

<typeIdentifierOrNew> ::= // New rule.
<typeIdentifier>
| 'new';

<declaringConstantConstructorSignature> ::= // New rule.
'const' 'this' ('.' <identifierOrNew>)? <declaringParameterList>;

Expand Down Expand Up @@ -686,8 +693,8 @@ extension type declaration, together with a declaration in the body that
contains a `<declaringConstructorSignature>` *(which does not contain a
`<declaringParameterList>`, because that's an error)*.

A class declaration whose class body is `;` is treated as a class
declaration whose class body is `{}`.
A class or extension type declaration whose class body is `;` is treated as
a declaration whose body is `{}`.

Let _D_ be a class, extension type, or enum declaration.

Expand Down Expand Up @@ -732,8 +739,19 @@ superclass.*
A compile-time error occurs if a `<defaultDeclaringNamedParameter>` has the
modifier `required` as well as a default value.

*Note that the updated grammar rule for `<constructorName>` allows
non-declaring constructors to use `new` where the current rules require
the class name. This is not a necessary part of the declaring constructors
feature, but it contributes to the overall brevity of Dart programs.*


### Static processing

The ability to use `new` rather than the class name in declarations of
ordinary (non-declaring) constructors is purely syntactic. The static
analysis and meaning of such constructors is identical to the form that
uses the class name.

The name of a primary constructor of the form
`'const'? id1 <typeParameters>? <declaringParameterList>` is `id1` *(that
is, the same as the name of the class)*.
Expand Down Expand Up @@ -874,12 +892,13 @@ reason for this error is that the modifier `covariant` must be specified on
the declaration of `v` which is known to exist, not on the parameter.*

A compile-time error occurs if _p_ has both of the modifiers `covariant`
and `final`. *A final instance variable cannot be covariant, because being
covariant is a property of the setter.*
and `final`, also if the latter is implicitly induced *(which can occur in a
primary constructor of an extension type declaration)*. *A final instance
variable cannot be covariant, because being covariant is a property of the
setter.*

A compile-time error occurs if _p_ has the modifier `covariant`, but
neither `var` nor `final`. *This parameter does not induce an instance
variable, so there is no setter.*
not `var`. *This parameter does not induce a setter.*

Conversely, it is not an error for the modifier `covariant` to occur on a
declaring formal parameter _p_ of a declaring constructor. This extends the
Expand All @@ -890,33 +909,36 @@ constructors.

The semantics of the declaring constructor is found in the following steps,
where _D_ is the class, extension type, or enum declaration in the program
that includes a declaring constructor, and _D2_ is the result of the
that includes a declaring constructor _k_, and _D2_ is the result of the
derivation of the semantics of _D_. The derivation step will delete
elements that amount to the declaring constructor; it will add a new
constructor _k_; and it will add zero or more instance variable
elements that amount to the declaring constructor. Semantically, it will
add a new constructor _k2_, and it will add zero or more instance variable
declarations.

*Adding program elements 'semantically' implies that this is not a source
code transformation, it is a way to obtain semantic program elements that
differ from the ones that are obtained from pre-feature declarations, but
can be specified in terms of pre-feature declarations.*

Where no processing is mentioned below, _D2_ is identical to _D_. Changes
occur as follows:

Assume that `p` is an optional formal parameter in _D_ which has the
modifier `var` or the modifier `final` *(that is, `p` is a declaring
parameter)*.

Assume that the combined member signature for a getter with the same name
as `p` from the superinterfaces of _D_ exists, and has return type `T`. In
that case the parameter `p` has declared type `T` as well.

*In other words, an instance variable introduced by a declaring parameter
is subject to override inference, just like an explicitly declared instance
variable.*

Otherwise, assume that `p` does not have a declared type, but it does have
a default value whose static type in the empty context is a type (not a
type schema) `T` which is not `Null`. In that case `p` is considered to
have the declared type `T`. When `T` is `Null`, `p` is considered to have
the declared type `Object?`. If `p` does not have a declared type nor a
default value then `p` is considered to have the declared type `Object?`.
Let `p` be a formal parameter in _k_ which has the modifier `var` or the
modifier `final` *(that is, `p` is a declaring parameter)*.

Consider the situation where `p` has no type annotation:
- if combined member signature for a getter with the same name as `p` from
the superinterfaces of _D_ exists and has return type `T`, the parameter
`p` has declared type `T`. If no such getter exists, but a setter with
the same basename exists, with a formal parameter whose type is `T`, the
parameter `p` has declared type `T`. *In other words, an instance
variable introduced by a declaring parameter is subject to override
inference, just like an explicitly declared instance variable.*
- otherwise, if `p` is optional and has a default value whose static type
in the empty context is a type `T` which is not `Null` then `p` has
declared type `T`. When `T` is `Null`, `p` has declared type `Object?`.
- otherwise, if `p` does not have a default value then `p` has declared
type `Object?`.

*Dart has traditionally assumed the type `dynamic` in such situations. We
have chosen the more strictly checked type `Object?` instead, in order to
Expand All @@ -932,25 +954,25 @@ this by specifying the current scope explicitly as the body scope, in spite
of the fact that the declaring constructor is actually placed outside the
braces that delimit the class body.*

Next, _k_ has the modifier `const` iff the keyword `const` occurs just
Next, _k2_ has the modifier `const` iff the keyword `const` occurs just
before the name of _D_ or before `this`, or if _D_ is an `enum`
declaration.

Consider the case where _D_ is a declaring header constructor. If the name
Consider the case where _k_ is a declaring header constructor. If the name
`C` in _D_ and the type parameter list, if any, is followed by `.id` where
`id` is an identifier then _k_ has the name `C.id`. If it is followed by
`.new` then _k_ has the name `C`. If it is not followed by `.` then _k_
has the name `C`. If it exists, _D2_ omits the part derived from
`'.' <identifierOrNew>` that follows the name and type parameter list, if
any, in _D_. Moreover, _D2_ omits the formal parameter list _L_ that
follows the name, type parameter list, if any, and `.id`, if any.
`id` is an identifier then _k2_ has the name `C.id`. If it is followed by
`.new` then _k2_ has the name `C`. If it is not followed by `.` then _k2_
has the name `C`. _D2_ omits the part derived from `'.' <identifierOrNew>`
that follows the name and type parameter list in _D_, if said part exists.
Moreover, _D2_ omits the formal parameter list _L_ that follows the name,
type parameter list, if any, and `.id`, if any.

Otherwise, _D_ is a declaring body constructor. If the reserved word `this`
is followed by `.id` where `id` is an identifier then _k_ has the name
`C.id`. If it is followed by `.new` then _k_ has the name `C`. If it is not
followed by `.` then _k_ has the name `C`.
is followed by `.id` where `id` is an identifier then _k2_ has the name
`C.id`. If it is followed by `.new` then _k2_ has the name `C`. If it is not
followed by `.` then _k2_ has the name `C`.

The formal parameter list _L2_ of _k_ is identical to _L_, except that each
The formal parameter list _L2_ of _k2_ is identical to _L_, except that each
formal parameter is processed as follows.

The formal parameters in _L_ and _L2_ occur in the same order, and
Expand All @@ -968,22 +990,19 @@ positional or named parameter remains optional; if it has a default value
unchanged from _L_ to _L2_ *(this is a plain, non-declaring parameter)*.
- Otherwise, a formal parameter (named or positional) of the form `var T p`
or `final T p` where `T` is a type and `p` is an identifier is replaced
in _L2_ by `this.p`, along with its default value, if any. Next, an
instance variable declaration of the form `T p;` or `final T p;` is added
to _D2_. The instance variable has the modifier `final` if the parameter
in _L_ has the modifier `final`, or _D_ is an `extension type`
declaration, or _D_ is an `enum` declaration. In all cases, if `p` has
the modifier `covariant` then this modifier is removed from the parameter
in _L2_, and it is added to the instance variable declaration named `p`.

In every case, any DartDoc comments are copied along with the formal
parameter, and in the case where an instance variable is implicitly
induced, the DartDoc comment is also added to that instance variable.
in _L2_ by `this.p`, along with its default value, if any. Next, a
semantic instance variable declaration corresponding to the syntax `T p;`
or `final T p;` is added to _D2_. It includes the modifier `final` if the
parameter in _L_ has the modifier `final`, or _D_ is an `extension type`
declaration and _k_ is a declaring header constructor. In all cases, if
`p` has the modifier `covariant` then this modifier is removed from the
parameter in _L2_, and it is added to the instance variable declaration
named `p`.

If there is an initializer list following the formal parameter list _L_
then _k_ has an initializer list with the same elements in the same order.
then _k2_ has an initializer list with the same elements in the same order.

Finally, _k_ is added to _D2_, and _D_ is replaced by _D2_.
Finally, _k2_ is added to _D2_, and _D_ is replaced by _D2_.
Copy link
Member

Choose a reason for hiding this comment

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

Replaced in what? ("Everywhere", probably. Any and every reference to D is now a reference to D2 instead, there is no D.)

When does this replacement happen, precisely?

  • After parsing, for sure.

  • And after all transitivie file dependencies are parsed, probably.

  • After top-level name resolution and export scope resolution, probably.

  • After checking type hierarchy validity? (No cycles, no extending a non-class.) Probably. This change does not affect - type hierarchy relations, can be before or after.

  • After checking class interface/base/final modifier rules. Again, the rewrite doesn't change anything, so can be after.

  • After rewriting the superclass, if necessary? Probably, since it refers to superclass getters declarations, and those should not

  • Before checking if a mixin class has a non-trivial constructor. Since it doesn't specify when a primary constructor is trivial, it must be checked after desugaring.

  • Before identifier resolution in members. The name of the new instance variables need to be in scope for that.

  • Before checking if class members are valid overrides of superinterface members.

  • Before checking if classes have duplicate member names. The rewrite adds declartions which should not have name clashes.

  • Before type inference.

That means that some parts of the language semantics see the original syntax, and some parts of the langauge semantics see the rewritten syntax.
And every part of the semantics must know and understand which one it sees, and what that means. Are we sure that's done correctly everywhere?


### Discussion

Expand Down Expand Up @@ -1017,6 +1036,10 @@ of declaration, and the constructor might be non-const).

### Changelog

1.10 - October 3, 2025

* Rename the feature to 'declaring constructors'. Fix several small errors.

1.9 - August 8, 2025

* Change the scoping such that non-late initializing expressions have the
Expand Down