Skip to content

Augmenting primary constructors and instance variable initializer scopes. #4727

Description

@lrhn

Primary constructors ships with the behavior that:

class C({final int _x = 0}) {
  this: assert(_x >= 0);
  final int y = _x + 1;
}

declares a constructor with a signature C Function({int x}) and _x + 1 is evaluated in the initializer list scope of that constructor which has a binding for _x.

For augmentations this raises the complexity of constructor overrides.

The currently proposed behavior is that:

class C({int x});
augment class C({final int _x = 0});

is valid since the first declaration's constructor is incomplete, and the two constructor declarations have compatible signatures. The _x is an "implementation detail".
However, that detail leaks.

You can write:

class C({int x}) {
  final int y = x;
}
augment class C({int x = 0}) {
  this: _x = x;
  final int _x;
}

and it will work, but, the second primary constructor declaration is complete (it has an initializer list).

However if you try to write:

class C({int x}) {
  final int y = x;
}
augment class C({final int _x = 0});

it won't work, because x in final int y = x; is not in the initializer list scope for the primary constructor, that contains _x.

The augmentation, written later and using an idiomatic instance variable declaring parameter (and valid by itself) introduces an error in the earlier class declaration.

Introducing errors in other fragments isn't special, that can happen with augmentations when everybody have to agree. What's annoying here is that they're not really disagreeing on what happens, only on how to refer to the constructor parameter, and only because one of them uses the supposedly helpful private-named-parameters feature.

Possible ways to handle this

  1. Don't use the initializer list scope for instance variable initialziers. Instead use a scope derived from the parameter list of the primary constructor on the same fragment that contains the instance variable initializer. So the class declaration with C({int x}) would see x in that scope, the class with C({final int _x = 0}) would see _x.

    • It introduces a new scope. Per class declaration.
    • You can't (as easily) move field initializers between the field and an initializer list if they have different scopes. (But they don't if they're inside the same class declaration.)
    • It's an error to refer to the public name of a parameter in an initializer of a class fragment that doesn't have a primary constructor declaring that parameter with that name. (There is only one declaration which can possibly have a private-named parameter, since that's necessarily a complete declaration, and inside that class declaration field initializers can then refer to that private name, and not the public one.)
  2. Say that C({int x}) not only introduces a signature, but also a name into the initializer list scope. It's then an error of another constructor introduces a different name into the initializer list scope, which effectively prevents any use of privately named parameters for that parameter. That's just the cost of doing augmentations.

    • We then allow incomplete declarations to use private names, so one could write a primary constructor C({int _x}) in the first fagment and refer to it in a field initializer as final int y = _x;. Then you'll have to provide a complete declaration with a parameter that is either this._x or final int this._x, that's the only way to have a complete implementation of that parameter.
  3. Just allow every field initializer to refer to either the public or private name of the parameter, if there is any private name for it anywhere.

    • This is breaking for the current non-augmenting primary constructor behavior, where
      const x = 42;
      class C({final int _x = 0}) {
         final y = x + _x;
      }
      is valid and alllows x to refer to the top-level constant.

@dart-lang/language-team
I think 2 is the most direct approach, and I'll work on specifying that.

I think 1 is possible, more complex, but possibly more understandable to users
(You refer to a primary constructor variable by the name it has in the primary constructor
of the current fragment. For all but one fragment, that's the public name. You only refer to
it by the private name inside the one fragment which actually has a parameter with that
private name.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    augmentationsIssues related to the augmentations proposal.bugThere is a mistake in the language specification or in an active documentprimary-constructorsFeature for less verbose constructors, otherwise known as declaring constructors.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions