tc39/proposal-decorators

Decorator points of execution

Closed this issue · 5 comments

Decorators allow us to execute code at different times and places in the class. As it is little confusing, we have prepared a simple guide to understand where we can act from a decorator.

A.- Class definition

  • A1.- execute the decorator for each member
  • A2.- execute the decorator for the class
  • A3.- execute the decorator for each static member
  • A4.- initialize the value for each static fields
  • A5.- initializer of each static decorator called with @init:
  • A6.- initializer of each class decorator called with @init:

B.- Create an instance object

  • (the original class constructor)
  • B1.- initialize the value for fields
  • B2.- initializer of decorators called with @init:
  • B3.- constructor of the class replaced by the decorator

C.- method or accessor invocation

  • C1.- replaced function by the decorator

You can see an example in: https://link.javascriptdecorators.org/UCogKm

Questions:

Is it complete list, or exist other points of execution?

Is it correct to run the original class decorator, the initializer for values, and the replaced class constructor? We are not sure that this is the proper behavior or the replaced class constructor must be called before the initializers.

I think you should include the decorator evaluation steps in there too, particularly, for example, when the decorator is called as @init:log(). I'm not sure transpiler (0.6.0) is handling this correctly. From the README:

The three steps of decorator evaluation:

  • Decorator expressions (the thing after the @) are evaluated interspersed with computed property names.
  • Decorators are called (as functions) during class definition, after the methods have been evaluated but before the constructor and prototype have been put together.
  • Decorators are applied (mutating the constructor and prototype) all at once, after all of them have been called.

Also:

Decorators are evaluated as expressions, being ordered along with computed property names. This goes left to right, top to bottom. The result of decorators is stored in the equivalent of local variables to be later called after the class definition initially finishes executing.

Turning log into:

function log() {
  console.log('- Eval')
  return function log(value, context) {
    // ...

to be used as @init:log(), it gives the output:

"--- start class definition ---"
"- Eval"
"A1.- decorator for member field with name "p""
"- Eval"
"A1.- decorator for member auto-accessor with name "j""
"- Eval"
"A1.- decorator for member method with name "m""
"- Eval"
"A2.- decorator for class with name "C""
"- Eval"
"A3.- decorator for static field with name "P""

When I'd expect (Edit: ... or maybe not, see comment below):

"--- start class definition ---"
"- Eval"
"- Eval"
"- Eval"
"- Eval"
"- Eval"
"A1.- decorator for member field with name "p""
"A1.- decorator for member auto-accessor with name "j""
"A1.- decorator for member method with name "m""
"A2.- decorator for class with name "C""
"A3.- decorator for static field with name "P""

since all the decorator expressions would have been evaluated in the first of the three steps ("-Eval") followed by being executed in the second ("A*").

Please @pzuraq, I need your support for this question because I not sure about the right behavior.

Looking at the spec text I'm not sure my interpretation of the readme is accurate. Decorators for each class member seem to get evaluated and called in the same ClassElementEvaluation step. So as you go through each member, decorators are both evaluated and called for that member before they are evaluated (and called) for any additional members. This would make the original output (from above) correct.

This being the case, I think the readme description could maybe use some tweaking?

...

One thing worth noting is that class name binding for the class scope isn't initialized until after all of this. So during the evaluations of the decorators, the class would not be accessible by name, even for static members. This is in line with how computed property names are handled today. Note that it is initialized in time for when initializers are run which happens after decorators are called.

class A {
    [A] = 'instance field'; // Error - A not inited
    static [A] = 'static field'; // Error - A not inited
}

so too should

class A {
    @decorator(A) // Error - A not inited
    a = 'instance field' + A; // OK
    @decorator(A) // Error - A not inited
    static a = 'static field' + A; // OK
}

This is what I'm seeing as far as it goes with respect to the current spec text (subject to change):
(Steps in parens (()) are not specific to decorators)

Class definition

  1. Evaluate Class decorators
  2. For each class member:
    1. (Evaluate computed member name)
    2. Evaluate member decorators
    3. Call member decorators
  3. Call class decorators
  4. (Initialization for class name binding in class scope)
  5. Call initializers for static members
    1. If static member is field:
      1. (Evaluate static field initializer)
      2. Call returned static field decorator initializers
    2. Call added static member decorator initializers
  6. Call added class decorator initializers
  7. (Initialization for class name binding in declaration scope)

Class instance definition

  1. Call constructor (may or may not have derived from a decorator call)
    1. (super())
    2. Call initializers for members
      1. If member is field:
        1. (Evaluate field initializer)
        2. Call returned field decorator initializers
      2. Call added member decorator initializers

Class instance method invocation

  1. Call method (may or may not have derived from a decorator call)

With respect to decorator order for any single decorated value, both evaluation and call of multiple decorators happen in the same order: bottom up (right to left) but all evaluations happen before any of the calls. For example, given (field):

@A()
@B()
value = C()

This would:

  1. Evaluate B()
  2. Evaluate A()
  3. Call B decorator
  4. Call A decorator
  5. ...
  6. Evaluate C()
  7. Call B (return) initializer
  8. Call A (return) initializer
  9. Call B (addInitializer) initializer
  10. Call A (addInitializer) initializer

Edit: included separation of returned field initializers and addInitializer initializers

Per #429 I put the class name binding after the class decorators running (steps 3 and 4) though the current spec text has it otherwise. I'll try to update it again when the spec gets updated again.