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

Conversation

jakemac53
Copy link
Contributor

@jakemac53 jakemac53 commented Apr 29, 2024

Closes #3555 and #3554

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

working/augmentation-libraries/feature-specification.md Outdated Show resolved Hide resolved
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.

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.

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.

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 😉

working/augmentation-libraries/feature-specification.md Outdated Show resolved Hide resolved
working/augmentation-libraries/feature-specification.md Outdated Show resolved Hide resolved
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.

working/augmentation-libraries/feature-specification.md Outdated Show resolved Hide resolved
@jakemac53
Copy link
Contributor Author

@lrhn PTAL, overhauled this a fair bit


**Non-redirecting generative constructors**: These always produce a new
instance, and have an optional initializer list and body (technically, there
is an implicit empty body). Their body has no explicit return value.
Copy link
Member

Choose a reason for hiding this comment

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

I'd say that they have an optional body. Having no body differs from an empty body because constant non-redirecting generative constructors cannot have a body, not even an empty one.
(And saying that ; is an empty body and {} is not is going to be too confusing.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Simplified this to just say optional initializer list and optional body. I removed the statement about the return type as well.

working/augmentation-libraries/feature-specification.md Outdated Show resolved Hide resolved

**Non-redirecting generative constructors**: These always produce a new
instance, and have an optional initializer list and body (technically, there
is an implicit empty body). Their body has no explicit return value.
Copy link
Member

Choose a reason for hiding this comment

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

(Also the only kind of constructor which can have initializing formals, super-parameters and a super-constructor invocation.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I am trying to keep this concise (with more description later on).

working/augmentation-libraries/feature-specification.md Outdated Show resolved Hide resolved
working/augmentation-libraries/feature-specification.md Outdated Show resolved Hide resolved
a normal function augmentation.

If it has a body, it replaces the body of the augmented constructor, and it may
invoke the augmented body by calling `augmented(arguments)`.
Copy link
Member

Choose a reason for hiding this comment

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

If the augmented constructor is a redirecting factory constructor, then there is no body.

We can still allow calling it! (And we should!)
A redirecting factory constructor is also like a static function, it just has a slight indirection, but there is no problem with the augmenting constructor calling it and thereby invoking the designated constructor with the provided arguments. You still get a value back.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Augmenting a redirecting factory constructor with a non-redirecting factory constructor isn't allowed right now.

We could allow it I suppose, but I don't think there is much value in that.

working/augmentation-libraries/feature-specification.md Outdated Show resolved Hide resolved

* The original constructor has a super initializer or redirecting initializer
and the augmenting constructor does too.
* The augmented constructor has a body.
Copy link
Member

Choose a reason for hiding this comment

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

(Why? If we treat factory constructors like static methods, just with two ways to specify an implementation, there is no problem in replacing a body with a redirection.

If you can't agument with = Foo;, but can augment with { return Foo(args); }, it's not a distinction, just a distraction.

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 it is cleaner to consider redirecting or not uniformly across regular constructors and factory constructors.

Why do we even have redirecting factory constructors at all? Honestly I don't know, but I prefer to keep them separate. We don't have redirecting static methods... so it feels different for sure.

working/augmentation-libraries/feature-specification.md Outdated Show resolved Hide resolved
working/augmentation-libraries/feature-specification.md Outdated Show resolved Hide resolved
@lrhn
Copy link
Member

lrhn commented May 23, 2024

This is pretty good. I'm nit-picking phrasing and order, just because this has to serve as a specification, so any potential ambiguity should be rooted out, with extreme prejudice. It's generally clear what the intent is.

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