Feature: Access to Initializing Formals

This document is an informal specification of the support for access to initializing formal parameters which has been implemented in dart2js with option --initializing-formal-access, starting with commit 442fc5. In SDK 1.21 this feature is enabled by default (that is, also without the option) in all tools.

The motivation for having this feature is that it enables less verbose and less repetitive code, and it is a feature that users have frequently requested.

Recall that an initializing formal parameter is a parameter to a generative constructor whose declaration is on the form this.x for some identifier x (possibly with a type annotation as in int this.x or bool this.x(int y)). At call sites this kind of parameter is passed just like other parameters, but its name (here: x) must be the name of a field, and that field is implicitly initialized to the value which is passed.

Previously, initializing formal parameters were never in scope, but this feature makes them available for the initializer list of the enclosing constructor. As a result, it is possible to make certain constructs less verbose. For example:

The motivation for this document is that it should be helpful as an informal specification in a process where other tools add support for this feature.

Syntax

This feature does not require any changes to the grammar of the language.

Semantics and Scoping

For brevity, this.x is used below as a standard example of an initializing formal parameter. This feature requires the following two semantic changes:

The parameter this.x must be visible in the initializer list with the name x, as if there were a final parameter with that name.

A name clash in the parameter list including initializing formals is a compile-time error.

Note that initializing formal parameters must not be in scope in the body of the constructor, just as they are not in scope in the body without this feature.

There are two semantically equivalent approaches to specify this—one which uniformly employs scopes, and another one which may be a more direct match with current implementations:

Use two scopes that share the non-initializing parameters: For a generative constructor, a scope is created which contains all parameters including the initializing formal ones, and that scope is the current scope for the initializer list of the constructor. Another scope is created which contains all parameters except the initializing formals, and that scope is the current scope for the body of the constructor. The formal parameter scope of the constructor is thus split in two scopes, the formal parameter initializer scope and the formal parameter body scope. These scopes are siblings: for both of them the enclosing scope is the type parameter scope of the constructor. Nested scopes are treated as in the language without this feature, except that they will be nested in one of the new scopes. For instance, the body scope will be nested in the formal parameter body scope. Note that some parameters will thus be entered into two distinct scopes, but each parameter is still a single variable (e.g., it will be stored in a single storage location in the activation record).

Introduce a new scope for the initializer list:

Check that there are no name clashes in the parameter list, where this.x is included and considered to have the name x.

Introduce a new scope that is only visible to the initializer list and whose parent is the formal parameter scope. This scope has a final entry x for every initializing formal parameter this.x.

Independently of the view on scoping, each scope entry corresponding to an initializing formal parameter must specify that it is final. The motivation for this requirement is that it would be highly error prone if mutation of an initializing formal parameter were accepted: programmers would almost certainly expect the mutation to apply to the field, not the parameter. Here is an example showing this phenomenon:

The semantics of passing an actual argument to an initializing formal parameter is unchanged: The set of bindings for the formal parameters is determined in the same way as in the current language specification, and so is the side-effect whereby the field x is initialized to the value bound to this.x when the constructor starts executing. The only change is the added visibility.

When all parameters including initializing formal ones are entered into the formal parameter initializer scope (or name clashes are checked in the full parameter list), additional conflicts due to duplicate entries with the same name may arise. In that respect this feature is a breaking change.

Note, however, that the virtual machine rejects these programs, and that it probably did so for a long time. This means that it is unlikely that there will be any cases in existing code where this new compile-time error occurs.