-
Notifications
You must be signed in to change notification settings - Fork 203
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
Augmentations, non-redirecting generative constructors and instance variable initializer expressions. #4063
Comments
@dart-lang/language-team As TL;DR says, I suggest doing instance variable initializer expression evaluation for a class before starting invocation of the (agumentation stack of) the non-redirecting generative constructor, |
The only real piece of pushback I have here is that this proposal removes the ability for an augmentation to add an explicit super constructor invocation. This was allowed in the previous proposal. It does complicate things, because now it isn't a strict pre-pend operation for initializers, but it seems like if augmentations can add an |
Interesting! I had not realized that we were allowing augmentations to add an The reason I ask is because in my mental model all classes (except So if we're allowing an Related question: if a class is declared with
|
I agree, all classes have an So in my head, a fragment isn't replacing an extends clause, it's contributing one to the merge operation. And the merge operation doesn't allow multiple extends clauses to be contributed (maybe unless they're identical? I haven't looked at the current proposal in a while). |
What @bwilkerson said basically - the implicit |
Adding a super constructor invocation in an augmentation makes sense. If we do that, then I think it should be an error to have super parameters in declarations prior to adding the super constructor invocation. Or rather, having a super parameter with no declared super constructor invocation implies one of the form Until the first super parameter or explicit super constructor invocation, the super invocation remains undetermined, and open for later augmentations to determine. And if they don't, it was The invocation chain will then carry with it the partially or fully filled out super constructor invocation (the argument list and the target constructor) until reaching the base declaration. That contains only values and a static reference, so it doesn't need access to the scope of the later augmentations when it's invoked. Can still work. |
Let me disagree then. Pedantically and semantically only, I agree with the conclusion. I'm just not-picking terminology. Every class has a superclass (except So I do agree that every class declaration introduces a class definition, and And if that still had no declared superclass, mixins or constructors, the actual class gets (I call these "definitions" because I want a name for the stack of declarations, and have a way to talk about its cumulative properties. We could achieve that entirely with helper functions, asking "what is the declared superclass of this stack of declarations", and just change the source to say "stack of declarations" where it currently says "declaration". There is a difference between The lesson to give users is that for augmentations, saying nothing actually means nothing. Defaults come after augmentations. |
I think this is probably a reasonable restriction. |
I think we could safely allow an augmenting constructor to have an initializer list that ends in It could even be useful, fx, to allow an augmenting constructor to wrap an argument: class ZonedTask {
final Zone zone;
final void Function() task;
ZonedTask(this.zone, this.task);
void run() { zone.run(task); }
}
augment class ZonedTask {
augment ZonedTask(Zone zone, void Function() task) : augmented(
zone.fork(zoneValues: {#_log: Logger.instance}),
() {
(Zone.current[#_log] as Logger).log("task");
task();
},
);
} (OK, not a very good example.) I think that should give all the benefits of being able to control the augment-super-invocation, without actually being able to run more or less than once. |
I would rather just not allow it. I am much more interested in allowing you to control the order in which the bodies run, than changing the arguments passed to them, which I think is an anti feature in this case. It is just too weird to see a field formal parameter, pass a value to it, and not have that be the value the field is initialized with. |
We can easily not allow an If we allow it, it would be consistent with how we use The end of the initializer list is the point where a constructor invokes another constructor. |
Also need to specify the other off instance variable initializer evaluation. It's a little convoluted because it mixes ordering (order of first declaration for each variable, then last-to-first order of the expressions (at least if they refer to |
TL;DR: All instance variables with initializer expressions of a class definition are initialized first thing, before invoking any non-redirecting generative constructor code. The the non-redirecting generative constructor code is executed in stack order, with the parameter list and initializer list of augmenting constructors executed before recursing on the augmenting definition, and the body executed after coming back. When reaching the base declaration it recurses on the superclass constructor as an outside call, which means it too initializes instance variables when firt reaching a non-redirecting generative constructor, but possibly after redirecting generative constructors. Super-parameters are accumulated along the way, put into pre-computed positions.
There is entirely too much backgound and details below, and it may be slightly more general than necessary, but I think it is consistent and usable.
This defines when initializer expressions for instance variable declarations are evaluated (first) and in which order (base declaration source order, with multi-file ordering being preorder depth-first on parts).
The biggest thing here is that an augmenting constructor (effectively) prepends its initializer list to the existing list.
This ensures that the scope used to evaluate the initializer list can be stack allocated.
We need to have recursed to the base definition at the end of the initialzier list, where we call the superclass constructor.
If we append new initializer lists, then we need the augmenting declaration's initializer list entries between the initialzier list and the super-constructor invocation of the base declaration, at which point we don't want to unwind the stack to the augmenting declaration's parameter scope. If we prepend, it just works.
Motivation
We have agreed that an augmenting non-redirecting generative constructor declaration with a body will execute its body after the execution of the body (or transitive bodies) of the constructor it is augmenting.
We also need to specify when instance variable initializer expressions are evaluated. Those are currently evaluated at the beginning of the invocation of a non-redirecting generative constructor. (Or just prior to the invocation, depending on perspective. Since every generative constructor performs the same initialization, it may be preferable to see it as existing outside of the constructors.)
With augmentations, we can now have augmenting constructors and augmenting variable initializer expressions. We need to place the variable initialization in the invocation hierarchy, preferably in a consistent and predictable place.
Terminology
A base (non-augmenting) declaration introduces a definition which mirrors the declaration, but at a slightly higher abstraction level. It’s possible for two differently written declarations to introduce indistinguishable definitions, if the difference is entirely syntactic. (For example
abstract final int x;
andint get x;
both introduce a single abstract getter definition namedx
with typeint
.)The kind of definition determines which properties it has.
An augmenting declaration is applied to an existing definition and introduces a new definition of the same kind. The new definition may refer to the prior definition, so that its implementation may refer to the augmented implementation.
Proposed augmented generative constructor behavior
When we invoke a generative constructor named g (for example
C.id
orC
) on an class definition C with type parametersX1
,..,Xn
, instantiated with type arguments T1,…,Tn, with an argument list L to initialize an object o (with a runtime type that is known to extend C and implement C<T1,..,Tn>):this.id
denotes a target ofC.id
in a class namedC
):late
:augmented
may be referenced, and doing so evalautes the initializer expression of the augmented definition. That is: Having an initializer expression is a property we can know directly for each variable definition, but how to evaluate it depends on the entire stack of definitions.We then define the invocation of a constructor definition as follows, allowing an augmenting constructor’s definition to delegate to the definition it augments.
Invocation of a non-redirecting generative constructor definition d on an instantiated class definition C<T1,…,Tn> with argument list L and super parameters S to initialize an object o proceeds as follows (at least for any class other than
Object
, which just completes immediately):null
value.false
, evaluate the second operand to a message value m, if there is a second operand, otherwise let m be thenull
value. Throw anAssertionError
with m as message..id
if referenced assuper.id
).The consequence of this definition is an execution order for initialization of an instance of a class of:
It ensures that we only recurse in one place, keeping a stack discipline. The parameter scope of the invocation can be stack-allocated, and be on top of the stack when the scope is used. (If we execute initializer list entries after the ones of an augmented definition, we fail to maintain that stack order.)
Details of the definitions
This focuses only on the properties of definitions that are relevant here.
A variable definition has (among others) the following properties:
augmented
reserved words if there is an augmentedDefinition that hasInitializerExpression.)A (non-abstract, non-external) variable declaration introduces a variable definition. This is the definition that implies a backing storage. A variable declaration also introduces a getter definition, and maybe a setter definition. Those are not relevant here.
These properties are updated when applying an augmenting variable declaration A to a variable definition d as follows:
The augmentedDefinition of the result is always d.
If A has an initializer expression, then that is the resulting definition’s initializerExpression and hasInitializerExpression is true.
Otherwise the resulting definition has no initializerExpression and its hasInitializerExpression has teh same value as the hasInitializerExpression of d.
(Any other change an augmenting variable declaration can do, which is just adding metadata. Name and types are inherited as-is.)
You evaluate the initializer expression of a variable definition d which hasInitialierExpression to a value v in a scope S as follows:
If d does not have an initializerExpression,
Otherwise let e be the initializerExpression of d:
If d does not have an augmentedDefinition (it’s the definition of a base declaration, so
augmented
is not reserved in e), then evaluate the expression e to a value v in scope S. Then evaluating the initializer expression of d evaluates to v too.Otherwise:
Let d’ be the augmentedDefinition of d.
Then
augmented
is a reserved word inside e. Ifaugmented
occurs inside e, then the hasInitializerExpression of d’ must be true.Evaluate the expression
e
in an “augmented-allowing” scope extending S to a value v, where evaluating the identifier expressionaugmented
performs the following operation:augmented
evalautes to v’.Then evaluating the initializer expression of d evaluates to v.
A non-redirecting generative constructor definition has (among others) the following properties:
augmentedDefintion: Optional, set if introduced by an augmentation application, the non-redirecting genreative constructor defintion that was augmented.
parameterList: Sequence of constructor parameter definitions. Each has a name and type, can be optional or required, positional or named, have a default value expression if optional, and be either an initializing formal, a super-parameter or a “normal” parameter.
The parameters must always match the types and names of parameterList of augmentedDefinition, if any. The “position” of a positional parameter in such a list is defined as one plus the count of positional parameters earlier in the sequence.
initializerList: Sequence of initializer entries, either variable initializer or assert.
InitializedVariables: Set of identifiers. The instance variables initialized by this definition stack.
superConstructorInvocation: Optional, only set on base declaration’s definition.
superParameters: integer count and set of identifier names, the super-parameter positions and names already filled by the initializer lists and super-constructor invocation of this definition, positional and named.
body: Optional code block.
The definition of a non-augmenting non-redirecting generative constructor declaration G has the following properties:
Applying an augmenting non-redirecting generative constructor declaration A to such a definition, d produces a result with the following properties:
Existing object creation and generative constructor behavior
For generative constructors, the existing pre-augmentation specified behavior is:
When you evaluate a constructor-based object creation expression (expression which invokes a constructor of a class, C, with or without an explicit
new
, as opposed to, for example, an object creation expression which is a list literal), the following happens (which ignores cases that would have been rejected as compile-time errors):Invoking a generative constructor G of a class C with an argument list L to initialize an object o (phew) is performed as follows:
If the constructor G is a redirecting generative constructor:
null
). The parent scope of the parameter scope is the member scope of the surrounding class.If G is a non-redirecting generative constructor:
For each instance variable declaration in the surrounding class declaration, in source order, if the instance variable declaration has an initializer expression
e
:e
in the body scope of the class C to a value v. As usual, if anything throws, then so does the constructor invocation to initialize.Let A be an uninitialized argument list corresponding to the super parameters of the constructor and the arguments of the super-constructor invocation, if any: One positional entry per positional super parameter or positional argument in the super-constructor invocation, one named entry per named super parameter or named argument in the super-constructor invocation, with no values set yet.)
Bind arguments to formals. This has more cases than normal function invocations due to initializing formals and super-parameters.
For each parameter of the constructor declaration, in source order:
If the argument list has a corresponding value, let v be that value.
Otherwise the parameter must be optional.
If the parameter has a default value expression, let v be the value of that expression.
Otherwise let v be the
null
value.If the parameter is an initializing formal for an instance variable:
If the parameter is an super parameter.
Otherwise the parameter is a normal paramter.
For each non-super-constructor entry in the initializer list of the constructor declaration:
e
:e
to a value v of typebool
in the initializer list scope.e
isfalse
:AssertionError
containing the value m as message.AssertionError
with no message.e
(id = e
orthis.id = e
).e
in the initializer list scope to a value v.For each instance variable of the class declaration, if the corresponding instance variable of o has not yet been assigned a value, initialize that instance variable to the
null
value.Let S be the superclass of the current class. (Not class declaration, but instantiated class, which can be an anonymous mixin application class with no explicit declaration.)
If the initializer list ends with a super-constructor invocation (
super(args)
orsuper.name(args)
).super
(unnamed) orsuper.name
(named)Otherwise, if there is no super-constructor invocation, let T be the unnamed constructor of class S.
Invoke the constructor T of class S with argument list A to initialize o.
When this completes, execute the body of C with
this
bound to o. The parent scope of the body block’s scope is the parameter scope of the invocation.The text was updated successfully, but these errors were encountered: