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 1 commit into
base: main
Choose a base branch
from

Conversation

jakemac53
Copy link
Contributor

Closes #3555

Specifies a bit more precisely how augmenting constructors are handled:

  • specifies what it means if there is no explicit body
  • specifies that all the same parameter variables are in scope in the augmented body
  • specifies clearly that no arguments are passed to the augmented body

cc @polina-c


- 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.
Copy link
Member

Choose a reason for hiding this comment

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

Took me a few tries to parse this. How about:

"then the original constructor body is left in place".

in the augmented and augmenting bodies refer to the exact same variable.
- 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.

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
Member

@lrhn lrhn left a comment

Choose a reason for hiding this comment

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

I think there are many more edges 😉


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:
Copy link
Member

Choose a reason for hiding this comment

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

Is this about all constructors, only generative constructors, or only non-redirecting generative constructors?
(We have seven constructor kinds: generative or not, redirecting or not, const or not, except for non-redirecting const factory. The section should be explicit about which constructors it applies to. I'm guessing it's mainly non-redirecting generative constructors, factory constructors are treated more like normal functions, and redirecting constructors have no body anyway.)


- 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.
Copy link
Member

Choose a reason for hiding this comment

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

"the original" is just plain ambiguous. I kept reading it as the body of the augmenting constructor (until I realized that it was the augmenting constructor that had no body, not the augemented constructor).

General rule: Say what is, not what isn't. This item is stating that nothing happens.
So let's flip it and say:

  • If the augmenting constructor has a body (a block or => body, not just a single ;),
    then that body replaces the existing constructor body.
  • In the augmenting constructor's body, and augmented() call executes the original
    constructor's body in the same scope that the augmenting body is executing in.

(This requires an augmenting declaration to not change names of positional parameters.
Is that a requirement?
Would it be better to pass arguments explicitly, and then bind them to variables corresponding to the names of the augmented constructor's parameter list, but not
actually process the real parameter list or initializer list. So that:

class C {
  final int x;
  final int y;
  C(int x, {this.y = 42}) : this.x = x { print("$x, $y"); }
  augmented C(int first, {int y = 42}) { augmented(first + 1, y); }
}

would work?
If not, why not? (Which might be @munificent's "could probably benefit from more elaboration and motivation" again. Why are we doing what we are doing here, why is it different from normal function.)

in the augmented and augmenting bodies refer to the exact same variable.
- 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.

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

in the augmented and augmenting bodies refer to the exact same variable.
- 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.

As written, it says you can add initializer list entries before a redirecting initializer. That's not valid. A redirecting (generative) constructor cannot have initializer list entries at all. (It can't even have asserts according to spec, but it seems implementations allow that, which is kind of nice.)

I'll assume (or propose if not true) that an augmenting constructor must have the same kind (out of the seven possible) as the constructor it augments. (Unless it can add const, which may be possible in some cases, but definitely not all.)

  • Augmenting constructor must be const if and only if augmented constructor is const.
  • Augmetning constructor must be factory if and only if augmented constructor is factory.
  • Augmenting constructor must be redirecting if and only if augmented constructor is redirecting.

Then:

  • A non-redirecting factory constructor can augment the body of the augmented constructor. This works just like a function body augementation, and you can invoke the augmented body as augmented(arguments).
  • A redirecting factory constructor can augment the redirection target constructor of the augmented constructor. This replaces the target with the augmenting constructor's target. (That target must redirect be a constructor that creates a subtype of the target of the augmented constructor?)
  • A redirecting generative constructor can augment the target constructor invocation of the augmented constructor. This replaces the target with the target constructor invocation of the augmented constructor. It cannot use augmented.
    • Maybe it can also add asserts to the initializer list. These are executed after any asserts
      of the augmented constructor.
  • A non-redirecting generative constuctor can augment the parameter list, initializer list,
    super constructor invocation and body of the augmented constructor.
    • Changing a parameter to be an initializing formal removes existing initializer list entries
      for that variable.
    • Changing a parameter to not be an initializing formal then allows (possibly requires) it
      to be initialized in the initializer list.
    • Changing a parameter to be a super. parameter replaces a corresponding argument
      from the existing super-constructor invocation, if there is one.
    • Changing a parameter away from being super. makes the super-constructor invocation
      not have a parameter at that position. This likely makes the invocation invalid, and
      having to be replaced.
    • Changing the name of a positional parameter is allowed.
      Whenever an expression or body of the augmented constructor is evaluated, it happens
      in a corresponding scope, which is a scope which binds the same variables, but with
      the names that they would have had in the augmented constructor.
    • If the augmenting constructor has an initializer list entry for the instance variable n, then:
      • If the augmented constructor (the result of all prior augmentations) has no initializer
        for n, then the initializer is appended (evaluated after all existing initializer lists).
      • If the augmented constructor already has an initializer for n,
        then the augmenting initializer replaces that initializer's initializer expression,
        and augmented evaluates the replaced initializer expression in the corresponding scope.
    • If the augmenting constructor has a body, it replaces the body of the augmented
      constructor.
      It may use augmented as an expression with static type void, which executes
      the replaced body in its corresponding scope.

In all of these, if the constructors are const, any inserted expressions must be valid for that. (Which just means that augmenting an initializer list entry can safely refer to its augmented, because that's known to be potentially constant.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Specify the behavior for an augmented constructor with no body
3 participants