1 of 55

Decorators, Stage 2 update:�Statically Analyzable

Daniel Ehrenberg

Igalia, in partnership with Bloomberg

March 2019 TC39

2 of 55

Popularity

  • The JS ecosystem makes heavy use of
    • TypeScript "experimental" decorators
    • Babel "legacy" decorators

i.e. roughly, the proposal as of 2014

  • 2015: Proposal switched to descriptors�� many developers didn't

3 of 55

Excitement

  • JS developers are really excited about decorators!

  • Features people make use of or ask for:
    • Initially proposed features
      • ad-hoc implementation limitations
    • Features of the Stage 2 proposal
      • Interaction with private and fields
      • Scheduling callbacks on construction
    • Features not yet proposed (mixins, functions, let)

4 of 55

Some goals

  • Decorators should be fast in implementations:
    • Transpilers
    • JS engines

  • Decorators should be easy to use:
    • Using someone else's decorators
    • Writing your own

5 of 55

History of decorator proposals

6 of 55

In 2014/2015: TypeScript "experimental"/Babel "legacy"

7 of 55

How decorators originally worked

8 of 55

2016: Descriptor-based decorators

interface MemberDesciptor {

kind: "property"

key: string,

isStatic: boolean,

descriptor: PropertyDescriptor,

extras?: MemberDescriptor[]

finisher?: (klass): void;

}

interface ClassDesciptor {

kind: "class",

constructor: Function,

heritage: Object | null,

elements: MemberDescriptor[]

}

9 of 55

2017-2019: Descriptor-based proposal grows and evolves

10 of 55

Language-level issues with decorators

11 of 55

Complex to write decorators

  • Need to understand Object.defineProperty deeply
    • Even with original decorators
    • Ecosystem abstraction layers

  • With Stage 2 proposal, expanded descriptor language

12 of 55

Difficult to extend over time

  • Past discussion about "elements"

  • Passing new fields back from decorators

  • Mixin feature request

13 of 55

Ember's experience and issues with the original decorators proposal

14 of 55

Why are descriptor-based decorators slow?

15 of 55

Transpiler implementations

  • Lots of code generated
  • Elements array shows up big
  • Traverse many dynamic data structures
    • Including in the constructor
    • Even without class elements
  • It takes time to allocate class declaration
    • Lots of descriptors to create and parse
  • Unclear how to generate good code

16 of 55

Native implementations

  • Fundamentally, similar overhead!
  • Several things observable which weren't before:
    • E.g., Initializer thunks
  • Class declaration may run multiple times and have different shpaes different times
  • Fancy optimizations harder to do in interpreter (but the JIT may do them)
  • Best case: Class structure available when generating bytecode
  • Thanks for the detailed analysis, Sathya Gunasekaran!

17 of 55

It's nice if it's easier to see what's going on

18 of 55

Decorators tend to have a fixed shape

19 of 55

Built-in decorators and composition

20 of 55

The idea

  • Basic building blocks: Built-in decorators

  • Compose to make JavaScript-defined decorators

  • @decorators are separate, static, lexically scoped

21 of 55

Built-in decorators

  • @wrap
  • @register
  • @expose
  • @initialize

  • More in follow-on proposals

22 of 55

Decorators defined in JavaScript

  • Compose built-in decorators
  • Pass computed arguments to them

decorator @foo { @bar @baz @bing }

23 of 55

@register

24 of 55

@register

class C {

@register(f) method() { }

}

class C {

method() { }

}

f(C.prototype, "method");

25 of 55

@register

@register(f)

class C { }

class C {

method() { }

}

f(C);

26 of 55

@defineElement

import { @defineElement } from"./defineElement.mjs";

@defineElement('my-class')

class MyClass extends HTMLElement {

/* ... */�}

// defineElement.mjs

export decorator @defineElement(� name, options) {

@register(klass =>� customElements.define(� name, klass, options))

}

27 of 55

@wrap

28 of 55

@wrap

class C {

@wrap(f) method() { }

}

class C {

method() { }

}

C.prototype.method =f(C.prototype.method);

29 of 55

@wrap

@wrap(f)

class C { }

class C { }

C = f(C);

30 of 55

@logged

import { @logged } from "./logged.mjs";

class C {

@logged

method(arg) {

this.#x = arg;

}

@logged

set #x(value) { }

}

new C().method(1);

// starting method with arguments 1

// starting set #x with arguments 1

// ending set #x// ending method

// logged.mjs

export decorator @logged {

@wrap(f => {

const name = f.name;

function wrapped(...args) {

console.log(`starting ${name} with arguments ${args.join(", ")}`);

f.call(this, ...args);

console.log(`ending ${name}`);

}

wrapped.name = name;

return wrapped;

})}

31 of 55

@initialize

32 of 55

@initialize

class C {

@initialize(f) a = b;

}

class C { }

C = f(C);

class C {

constructor() {

f(this, "a", b);

}

}

33 of 55

@initialize

@initialize(f)

class C { }

class C {

constructor() {

f(this);

}

}

34 of 55

@bound

import { @bound } from "./bound.mjs";

class Foo {

x = 1;

@bound method() {� console.log(this.x);� }

queueMethod() {� setTimeout(this.method, 1000);� }

}

new Foo().queueMethod();�// logs 1, not undefined

// bound.mjs

export decorator @bound {

@initialize((instance, name) =>� instance[name] =� instance[name].bind(instance))

}

35 of 55

@expose

36 of 55

@expose

class C {

@expose(f) #x;

}

class C {

@register(proto =>f(proto,

"#x",

instance => instance.#x,

(instance, value) =>� instance.#x = value ))

#x;

}

37 of 55

@show

import { FriendKey, @show } from"./friend.mjs";�

let key = new FriendKey;

export class Box {

@show(key) #contents;

}

export function setBox(box, contents) {

return key.set(box, "#x", contents);

}

export function getBox(box) {

return key.get(box, "#x");

}

export class FriendKey {

#map = new Map();

expose(name, get, set) {

this.#map.set(name, { get, set });

}

get(obj, name) {

return this.#map.get(name).get(obj);

}

set(obj, name, value) {

return this.#map.get(name)� .set(obj, value);

}

}

export decorator @show(key) {

@expose((target, name, get, set) =>� key.expose(name, get, set))

}

38 of 55

Common features of decorators

39 of 55

Syntax

  • Uses DecoratorList syntax before an existing construct
  • Doesn't change the syntax of what's decorated
  • No early errors, but rather runtime errors

40 of 55

Semantics/phasing

  • Decorator arguments are evaluated at runtime
    • In classes: Interspersed with computed property names

  • In spec-land, they run at runtime
    • But it's always apparent which built-in decorators are used�
  • When code is executing multiple times, different arguments, but the same built-in decorators

41 of 55

Potential future built-in decorators

42 of 55

Decorators for other syntactic forms

  • Functions
  • Parameters
  • Variable declarations
  • Blocks
  • Labels
  • Numeric literals
  • Object literals
  • Object properties
  • (not expressions)

43 of 55

Built-in support for common scenarios

  • @bound
  • @tracked
  • @reader
  • @set

44 of 55

Error checking

  • @assertClass
  • @assertMethod
  • @assertField
  • @assertPrivate
  • etc

45 of 55

Property descriptor/placement change

  • @own
  • @prototype
  • @static
  • @enumerable
  • @nonenumerable
  • @writable
  • @nonwritable
  • @configurable
  • @nonconfigurable

46 of 55

Statically change the structure of the class

  • Adding a private field
  • Converting a field to an accessor
  • Adding a mixin (even to a base class)
  • etc.

  • May use some kind of descriptor (as input, not output)
  • Or, may use some yet-to-be-designed mini-language
    • Could be a separate declaration form

47 of 55

Implementation notes:�Some complexity, but more optimizable

48 of 55

Could implement this directly

  • Can be executed dynamically
    • Similar to the previous proposal
    • This is how the spec will be written

  • But, to take advantage of the guarantees and optimize...

49 of 55

JavaScript compilation becomes non-local

50 of 55

Transpilers: .decorators.json

  • Describes which built-in decorators a composed decorator breaks down into
  • Referenced when compiling a file which uses the decorators
  • Separate runtime representation used for arguments

  • Different from how tooling works now, but seems doable

51 of 55

Custom decorators in tooling

  • Decorators could be a good basis for more general macros
  • To start: Follow lead of babel-plugin-macros and let tools define decorators which are tree transforms
    • Then, they can be composed!
    • So, prototype proposed built-in decorators

52 of 55

Native JS implementations:�Bytecode based on dependencies

  • Classes with decorators are lazy-parsed if their decorator has not yet been parsed
  • Cached bytecode is invalidated when imported decorators change

  • Generally positive feedback from browsers
  • Different from how engines work now, but a much better starting point for optimization than the last proposal

53 of 55

Next steps

54 of 55

Recommendations for authors of decorators today

  • The original decorators proposal is broadly supported
  • The newer January 2019 "Stage 2" proposal is not
  • Just keep using "legacy"/"experimental" decorators
  • Tools may need to maintain support for set-based fields until it's possible to transition to Stage 3 decorators

  • Goal of this proposal: For users (not authors) of decorators, upgrading to standard should be a codemod

55 of 55

Prototyping this proposal

  • Write specification
  • Implement these new decorators in Babel
    • Including some "post-MVP" decorators
  • Try out the new decorators
  • Collect feedback

  • Propose for Stage 3 after some months of stability+use