Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

clarify some edge cases around constructors #3737

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 16 additions & 9 deletions working/augmentation-libraries/feature-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,14 +697,21 @@ It is a compile-time error if:

### Augmenting constructors

Constructors are (as always) more complex. A constructor marked `augment`
replaces the body of the existing constructor with its body. If the augmenting
constructor has any initializers, they are appended to the original
constructor's initializers, but before any original super initializer or
original redirecting initializer if there is one.

In the augmenting constructor's body, an `augmented()` call invokes the
original constructor's body.
Constructors are (as always) more complex. A constructor marked `augment` may:
jakemac53 marked this conversation as resolved.
Show resolved Hide resolved

- Add or replace the body of the existing constructor with a new body.
- If the augmenting constructor has no explicit body (terminates with a `;`),
then the original is left in place.
jakemac53 marked this conversation as resolved.
Show resolved Hide resolved
jakemac53 marked this conversation as resolved.
Show resolved Hide resolved
- In the augmenting constructor's body, an `augmented()` call invokes the
original constructor's body. Unlike regular function augmentations, you do
not pass parameters explicitly. All of the same parameters are in scope in
the augmented body as the augmenting body.
- If a parameter variable is overwritten prior to calling `augmented()`, the
augmented body will see the updated value. All references to the parameter
in the augmented and augmenting bodies refer to the exact same variable.
Copy link
Member

Choose a reason for hiding this comment

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

I think I understand what's going on here but I could probably benefit from more elaboration and motivation. Maybe something like:


Generative constructors are unlike other functions because they:

  1. Allocate a fresh instance of the class before any other work happens.

  2. If there is a superclass, execute the superclass constructor's initializer list (recursively).

  3. Execute this constructor's initializer list.

  4. Execute this constructor's body.

  5. If there is a superclass, execute the superclass constructor's body (recursively).

Note that initializer lists are run "down" the inheritance chain from superclasses to subclass while constructor bodies are run "up" the chain from subclass to superclasses.

This order is fixed by the language and, importantly, we require all initializer lists to be executed before any constructor body is run so that all final and non-nullable fields are definitely initialized before the new instance can be seen.

The parameters to a constructor are visible both in the initializer list and in the constructor body. The initializer list runs before the body which means the parameters are already potentially seen and used before the augmented constructor body begins.

That's why when calling augmented(), the augmenting constructor doesn't pass any parameters to the original body. Those parameters are already available before the augmenting constructor began and thus the augmenting constructor can't interfere with them.

Copy link
Member

@lrhn lrhn Apr 30, 2024

Choose a reason for hiding this comment

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

It's the other way around, actually. Superclass bodies run before subclass bodies. (The order of pre-body initialization doesn't really matter, but it's defined in case there are visible side effects.)

An object creation expression creates an object, then it invokes the constructors to initialize that object.
Invoking a non-redirecting generative constructor does the following:

  • Evaluate initializers of instance variables, to initialize those.
  • Evaluate the parameter list (which can include initializing formals to initialize the corresponding variables).
  • Evaluate the initializer list, which initializes the remaining variables that need it.
  • Recursively invoke the super-constructor (which is at the end of the initializer list, explicitly or implicitly), unless this is the Object class.
  • Execute the body, if any, with this bound to the new object.

That ensures that all fields are initialized before the first body executes, and then it ensures that all super-class bodies have been executed before the subclass body runs, so the object is at least "fully initialized as the superclass" before the subclass sees it.

But 'nuff nitpicking.


Does that matter wrt. whether we can pass arguments to augmented?

When augmenting function bodies, we can invoke the parent body using augmented(args) because nothing happens after (or during) binding actuals to formals and before executing the body. Calling "as a function" is safe because it does nothing except set up an environment and then execute the code we wanted in that environment.

Why can we not do that for constructors. Because we can't invoke the constructor without side effects.
But if we only want to invoke the body, then we probably could.
Take:

class C {
   final int x = 1;
   final int y;
   final int z;
   C(int w, {this.z = -1}) : y = w + 1{
     print("Banana: $x: $y: $z: $w");
   }
   augment C(int y, {this.z = -1}) {
     augmented(y + 1, z: z + 1);
   }
}

This augment would invoke the body as if it was a function with a parameter list derived from the parameter list of the constructor (same types and names, but never any initializing or super parameters).

An equivalent desugaring (not how it's specified!!) could be:

class C {
   final int x = 1;
   final int y;
   final int z;
   _$augmented$C(int w, {int z = -1}) {
     print("Banana: $x: $y: $z: $w");
   }
   C(int y, {this.z = -1}) : this.y = y { // Assuming you can rename positional parameters.
     _$augmented$C(y + 1, z: z + 1);
   }
}

That would allow invoking the augmented constructor body with new arguments, like what we do for normal functions.
We can say that we don't want to allow that, and that the body must use the same argument list as the augmented construtor. (I'm fine with that. But then we should consider why we don't say that for functions too.)
And as written, the augmented body is evaluated with the same variables as the augmented body, which is scary. That means a function call-looking expression like augmented() can change local variables. (Also, "the same variables" probably does not extend to local variables, so at most the same parameter scope.)

Generally, we can't just assume that the augmented body has its arguments already passed, because we are not chaining through "parent constructors" in the augmentation chain the same way we are between classes.
(If we were, new initializing formals would be prepended, not appended.)

Which means that we never invoke the augmented constructor. We probably don't even evaluate its parameter list, because we use the one from the last augmentation instead, which should replace the original (or match it, in which case we don't know which one we evaluate). That mean no binding actuals to formals for the augmented constructors formals. (Which is also a problem, because we do need to do that to set up the correct names for the initializer list. And if we do that, we can probably do the same for the body.)

All in all, this is a mess. I'll have to think 🤔.

Copy link
Member

Choose a reason for hiding this comment

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

I have thought a little about it.
Here is one possible approach: https://gist.github.com/lrhn/47d4161c4743a09659732952b21591f7

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't allow altering parameters via augmentation (even names of positional parameters), which simplifies a lot of this I think?

I did change this up a bit, to specify that local variables are not in scope, it is only the parameter scope which is shared. It does mean that a call to augmented() can change a non-final parameter variable in the augmenting scope. That is weird, to be sure, but I think it is probably acceptable.

Copy link
Member

Choose a reason for hiding this comment

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

I've thought some more (far too much, likely): https://gist.github.com/lrhn/47db7dd136bb5ed388b0cd1c8260001d

And couldn't find any reasonable way to execute the augmented body of the augmented declaration in any other scope than the same parameter scope that the current body is executed in. That was the scope that the parameter list introduced, and which the initializer list and super constructor invocation has already been run in, which means those variables might be captured in closures already stored inside the object. Introducing a new scope just to execute the augmented body in, would not be consistent with the augmented constructor's initializer list and body sharing a scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think what I have specified here is a subset of that? Basically it doesn't allow bashing completely over anything pre-existing, or changing something to/from redirecting or not, if it was clearly one or the other.

- Add initializers to the initializer list.
- These are appended to the original constructor's initializers, but before
any super initializer or redirecting initializer (if present).
Copy link
Member

Choose a reason for hiding this comment

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

Can an augmenting constructor add a super initializer or redirecting initializer?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe!
Porbably not a redirecting initializer, unless the constructor already was redirecting.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed the part about redirecting initializers here.

I am allowing a constructor with no body and no initializers to be augmented with a redirecting initializer. This means it can be ambiguous if a constructor is redirecting or not until after augmentation.

We could remove that, but I think code generators will sometimes want to implement a constructor as redirecting.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a section clarifying that you can add a super initializer.

jakemac53 marked this conversation as resolved.
Show resolved Hide resolved

It is a compile-time error if:

Expand All @@ -725,7 +732,7 @@ It is a compile-time error if:
or vice versa.

* The original constructor is a factory constructor and the augmenting
constructor has an initializer list.
constructor is not or vice versa.
Copy link
Member

Choose a reason for hiding this comment

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

Should be safe!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As in, you think we should allow converting constructors between generative and factories?

Or, you think it is good that this is specified as an error?


* The original constructor has a super initializer or redirecting initializer
and the augmenting constructor does too.
Expand Down