dart-lang / language

Design of the Dart language
Other
2.65k stars 202 forks source link

Make `augmented` an error in the body of a non-redirecting generative constructor? #4026

Open eernstg opened 1 month ago

eernstg commented 1 month ago

The semantics of augmented() in the body of an augmenting declaration of a non-redirecting generative constructor is complex, and it treats parameters in a way which is different from the treatment of parameters of other functions.

class A {
  A(int i) {
    print(i);
  }
  augment A(i) {
    i = i + 1;
    augmented(); // NB: just `augmented()`, not `augmented(i)`.
  }
}

void main() {
  A(0); // Prints '1'.
}

I'd recommend that we postpone the discussions about how to handle constructor body augmentations in this case, and simply specify that it is an error to specify a body in an augmenting declaration of this kind of constructor: The body can be provided by at most one declaration (introductory or augmenting) in the stack, and it is an error to provide more than one.

These tricky details probably arise because a non-redirecting generative constructor can have initializing formal parameters, and we can't allow the invocation of augmented() to repeat the initialization of certain final instance variables, nor can we allow the omission of an invocation of augmented() to allow those instance variables to remain uninitialized. (A super-parameter may seem similar to an initializing formal, but they are probably easier to handle.)

In the future, we can choose to support case where the constructor body is augmented in any way we want because it will be a non-breaking change.

@dart-lang/language-team, WDYT?

lrhn commented 1 month ago

Another reason fo r the complicated behavior is that unlike functions, a non-redirecting generative constructor's body is not the only scope where parameters are available. If an initializing formal closes over a constructor parameter, then, the body of that constructor must agree on the identity of the parameter, so it's not possible to pass different arguments, or even have different variables, when invoking the augmented body.

jakemac53 commented 1 month ago

We do already have users who want to be able to augment constructor bodies, so I don't think we should punt on this.

Do we see an issue with the current specification? It is indeed very specialized, but we did put a fair bit of effort into coming up with something sensible.

eernstg commented 3 weeks ago

A new proposal came up during the language team meeting today, and it was received very well:

According to this proposal, augmented() (and similar forms like augmented or augmented(with, some, arguments)) is an error when it occurs in the body of a generative, non-redirecting constructor.

However, it is still allowed for zero or more elements in an augmentation stack to specify a body for this constructor, and they will all be executed. The rule that governs the execution is that each body is executed (you can't skip any, you can't run it several times). They all receive the exact same actual argument list (an assignment to a formal parameter in one constructor body does not affect the value of that formal parameter for the next constructor body). Finally, the constructors are executed in augmentation order: The body of the introductory declaration (if any) is executed to completion, which is followed by execution of the body of the first augmentation (if any), etc., until the body of the last augmentation completes.

For example:

class A {
  A(int i) {
    i = 0;
    print('Introductory declaration: $i');
    i = 1;
  }

  augment A(int i) {
    print('First augmentation: $i');
  }

  @metadata
  augment A(int i); // It is possible to not contribute a body.

  augment A(int i) {
    print('Second augmentation: $i');
  }
}

void main() {
  A(24);
}

The invocation of the constructor in main yields the following output:

Introductory declaration: 0
First augmentation: 24
Second augmentation: 24

The arguments in favor of this model is that it avoids the very special rules that we currently have for the treatment of parameters, it avoids having a special form augmented() whose meaning is something that isn't quite expressible in Dart otherwise, and it enforces execution of all augmentations of the constructor (a bit like execution of constructors in a superclass chain: you can't skip the constructor body for a class in the superclass chain, and you can't execute it twice, and that's usually a very meaningful restriction).

Also, this model will execute the bodies of the augmentation stack from the introductory and down through all the augmenting declarations, which is again similar to the order of execution of constructor bodies in a superclass chain (the first constructor body which is executed is Object, and so on). A model that uses augmented() will invoke the augmentation stack in the opposite order.

The most important argument against this model is that it is less flexible than a model that is based on some kind of explicit invocation (basically, something using augmented in some form).

@jakemac53, @munificent will get in touch with you and give more details.

jakemac53 commented 2 weeks ago

My main concern with this is that due to the lack of expressive power, an augmentation cannot perform initialization that is relied upon in a hand written constructor (or if we changed the ordering, the opposite would be true).

It also means you could not do anything that "wraps" the original constructor (running before and after it), which is generally expected to be common use case for normal functions - although it might not be as common for constructors. But, it seems highly likely that somebody will run into this restriction. Although I do expect that the behavior would be fine for most macros.

I have two lesser concerns: