From 024cb93f45bd2ab93e6e129cb20dc8163a01aa32 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 17 Sep 2015 01:04:51 -0700 Subject: [PATCH] Update the spec based on user feedback --- README.md | 35 +- .../initializer.md | 3 + interop/private-state.md | 157 +++ interop/reusability.md | 1049 +++++++++++++++++ 4 files changed, 1233 insertions(+), 11 deletions(-) rename INITIALIZER_INTEROP.md => interop/initializer.md (96%) create mode 100644 interop/private-state.md create mode 100644 interop/reusability.md diff --git a/README.md b/README.md index 781fd04..788db13 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +> This file is under active development. Refer to `interop/reusability.md` for the +> most up to date description. + # Summary Decorators make it possible to annotate and modify classes and properties at @@ -13,8 +16,8 @@ A decorator is: * an expression * that evaluates to a function -* that takes the target, name, and property descriptor as arguments -* and optionally returns a property descriptor to install on the target object +* that takes the target, name, and decorator descriptor as arguments +* and optionally returns a decorator descriptor to install on the target object Consider a simple class definition: @@ -49,22 +52,25 @@ Now, before installing the descriptor onto `Person.prototype`, the engine first invokes the decorator: ```js -let descriptor = { - value: specifiedFunction, +let description = { + type: 'method', + initializer: () => specifiedFunction, enumerable: false, configurable: true, writable: true }; -descriptor = readonly(Person.prototype, 'name', descriptor) || descriptor; -Object.defineProperty(Person.prototype, 'name', descriptor); +description = readonly(Person.prototype, 'name', description) || description; +defineDecoratedProperty(Person.prototype, 'name', description); + +function defineDecoratedProperty(target, { initializer, enumerable, configurable, writable }) { + Object.defineProperty(target, { value: initializer(), enumerable, configurable, writable }); +} ``` -The decorator has the same signature as `Object.defineProperty`, and has an -opportunity to intercede before the relevant `defineProperty` actually occurs. +The has an opportunity to intercede before the relevant `defineProperty` actually occurs. -A decorator that precedes syntactic getters and/or setters operates on the -accessor descriptor: +A decorator that precedes syntactic getters and/or setters operates on an accessor description: ```js class Person { @@ -72,7 +78,14 @@ class Person { get kidCount() { return this.children.length; } } -function nonenumerable(target, name, descriptor) { +let description = { + type: 'accessor', + get: specifiedGetter, + enumerable: true, + configurable: true +} + +function nonenumerable(target, name, description) { descriptor.enumerable = false; return descriptor; } diff --git a/INITIALIZER_INTEROP.md b/interop/initializer.md similarity index 96% rename from INITIALIZER_INTEROP.md rename to interop/initializer.md index 9b56ac8..5532491 100644 --- a/INITIALIZER_INTEROP.md +++ b/interop/initializer.md @@ -1,3 +1,6 @@ +> This file is under active development. Refer to `interop/reusability.md` for the +> most up to date description. + This document is a high-level explainer of how property decorators interoperate with the [class properties][class-properties] proposal. diff --git a/interop/private-state.md b/interop/private-state.md new file mode 100644 index 0000000..844291e --- /dev/null +++ b/interop/private-state.md @@ -0,0 +1,157 @@ +## Interoperability With Private Slots + +In principle, this system could be retrofitted to work nicely with true +privates with one additional abstraction. + +For this exploration, let's assume that the syntax for private +declarations is `#name [= initializer]` and the syntax for private usage +is `#name` or `receiver.#name`. + +```js +class Person { + @reader #first, #last; + + constructor(first, last) { + #first = first; + #last = last; + this._first = first; + this._last = last; + } + + @reader get #fullName() { + return `${#first} ${#last}`; + } +} +``` + +#### Usage + +```js +let person = new Person("Dmitry", "Lomov"); +person.first // "Dmitry" +person.last // "Lomov" +person.fullName // "Dmitry Lomov" +``` + +#### Desugaring + +```js +const PERSON_SLOTS = new WeakMap(); + +class Person { + constructor(first, last) { + let slots = { first: undefined, last: undefined }; + slots.first = first; + slots.last = last; + PERSON_SLOTS.set(this, slots); + } +} + +let target = Person.prototype; + +decorate('slot', target, [reader], Slot(PERSON_SLOTS, 'first', null)); +decoratePrivate(target, 'last', [reader], Slot(PERSON_SLOTS, 'last', null)); +decoratePrivate(target, 'fullName', [reader], Slot(PERSON_SLOTS, { get: function() { /* ... */ })); +``` + +#### Decorator Descriptor Extensions + +The decorator descriptors for fields, methods, and accessors are extended with an +additional `slot` boolean (true for slots, false otherwise). + +```diff +interface FieldDecoratorDescriptor extends DecoratorDescriptor { + type: string, // 'field' + enumerable: boolean, // default: true + configurable: boolean, // default: true + writable: boolean, // default: true + property: DataProperty, + ++ // true for private fields (#name = expr), false for public fields (name = expr) ++ slot: boolean +} + +interface MethodDecoratorDescriptor extends DecoratorDescriptor { + type: string, // 'method' + enumerable: boolean, // default: false + configurable: boolean, // default: true + writable: boolean, // default: true + property: MethodProperty, + ++ // true for private methods (#name() {}), false for public methods (name() {}) ++ slot: boolean +} + +interface AccessorDecoratorDescriptor { + type: string, // 'accessor' + hint: string, // 'getter', 'setter', 'both' + enumerable: boolean, // default: true + wrtiable: boolean, // default: true + property: AccessorProperty, + ++ // true for private accessors (get #name() {}), false for public accessors (get name() {}) ++ slot: boolean +} +``` + +#### Updated Decorator Definition + +With the addition of true private fields, we can update our decorator +definition to make it support true private fields as well as +"underscored" fake privates. + +```js +function reader(target, descriptor) { + let property = descriptor.property; + let { slot, name } = property; + + let publicName = slot ? name : descriptor.name.slice(1); + + Object.defineProperty(target, publicName, { + enumerable: descriptor.enumerable, + configurable: descriptor.configurable, + get: function() { return property.get(this); } + }); + + // special meaning for object literal: { @reader _first } + if (descriptor.shorthand) delete descriptor.initializer; +} +``` + +#### Private State Utilities + +```js +function decoratePrivate(target, name, decorators, descriptor) { + decorators.reduce((desc, decorator) => decorate(decorator, target, name, desc), descriptor) +} + +function PrivateFieldDecoratorDescriptor(slots, name, initializer) { + return { + type: 'field', + private: PrivateLookup(slots, name) + initializer: initializer + } +} + +function PrivateMethodDecoratorDescriptor(slots, name, initializer) { + return { + type: 'method', + private: PrivateLookup(slots, name), + initializer: initializer + } +} + +function PrivateAccessorDecoratorDescriptor(slots, name, { get, set }) { + let type; + + if (get && set) type = 'accessor'; + else if (get) type = 'getter'; + else type = 'setter'; + + return { type, private: PrivateLookup(slots, name), get, set }; +} + +function PrivateLookup(slots, name) { + return { for: (self) => slots.get(self)[name] }; +} +``` \ No newline at end of file diff --git a/interop/reusability.md b/interop/reusability.md new file mode 100644 index 0000000..028e66c --- /dev/null +++ b/interop/reusability.md @@ -0,0 +1,1049 @@ +This document illustrates two major goals for the property decorator +proposal: + +1. It should be possible to write general-purpose decorators that work + across all of the different syntactic forms for creating declarative + properties in JavaScript (both classes and object literals). +2. Decorators implement syntactic abstractions, and therefore receive + information about the syntactic form they are decorating. + +## Motivating Example + +To illustrate, I'll write a decorator called `reader` that decorates a +property beginning with an `_` and creates a public getter for that +property without an `_`. + +Here's the decorator definition: + +```js +function reader(target, descriptor) { + let { enumerable, configurable, property: { name, get }, hint } = descriptor; + + // extractPublicName('_first') === 'first' + let publicName = extractPublicName(name() /* extract computed property */); + + // define a public accessor: get first() { return this._first; } + Object.defineProperty(target, publicName, { + // give the public reader the same enumerability and configurability + // as the property it's decorating + enumerable, configurable, get: function() { return get(this, name); } + }); +} + +function extractPublicName(name) { + // _first -> first + return name.slice(1); +} +``` + +> For those of you thinking this is weak tea without true private state, +> I have a treat for you in this repository: how this decorator could be +> extended to work with hypothetical true private state. In the +> meantime, bear with me. + +Next, we'll explore how this definition works on each kind of +declarative property creation in JavaScript. + +## Basic Rules of Decorators + +Decorators always operate on a particular syntactic element, +providing a hook into the runtime semantics for that syntax. + +If the runtime semantics for the syntax include "let x be the +result of evaluating *SomeExpression*`, that expression is passed +into the decorator as a function that, when called, evaluates the +expression (a "thunk"). + +Decorators are not macros: they cannot introduce new bindings into +the scope and cannot see any downstream syntax. They are restricted +to operating on a local declaration using reflection tools. + +## The Programming Model of Decorators + +Decorators allow library and framework authors to provide abstractions +that extend the JavaScript syntax, but only in controlled ways, and +only by using a distinguished syntax. + +Property decorators (including method decorators) can change the property +being installed or install other properties alongside it, always operating +on inert values. + +Decorators that simply attach metadata to a property without altering it +or defining any additional properties on the target can be documented +simply. + +Other decorators will be documented in terms of their effects on the +target, which might include completely replacing the property they +are decorating, wrapping it, or installing other properties. + +With great power comes great responsibility. + +## Usage With Property Declarations in JavaScript + +The examples below use a few library functions that are defined in an +appendix, which will be explained the first time they are encountered. + +## Initialized Property Declarations + +```js +class Person { + @reader _first = "Andreas"; + @reader _last = "Rossberg"; +} +``` + +#### Usage + +```js +let andreas = new Person(); +andreas.first // "Andreas" +andreas.last // "Rossberg" +``` + +#### Resulting Class + +```js +class Person { + _first = "Andreas"; + _last = "Rossberg"; + + get first() { return this._first; } + get last() { return this._last; } +} +``` + +#### Desugaring + +```js +class Person {} + +let target = Person.prototype; + +decorate('field', target, [reader], Property('_first', () => "Andreas")); +decorate('field', target, [reader], Property('_last', () => "Rossberg")); +``` + +#### The `decorate` and `Property` Utilities + +The `decorate` function constructs a decorator descriptor for the appropriate type, +and calls the array of decorators in reverse order, passing the decorator descriptor +returned in the previous step into the next step. + +The final descriptor is used to construct the arguments for an invocation of +`Object.defineProperty` or `Reflect.defineField`. + +You can find full details in the appendix. + +```ts +// type is "field" | "property" | "accessor" | "method" +// different for each combination of defaults passed to defineProperty or defineField +function decorate(type: string, target: any, decorators: Decorator[], property: Property, hint=null: string); + +// A Decorator is a function that takes a target and a decorator descriptor and return +// an optional decorator descriptor. +type Decorator = (target: Object, descriptor: DecoratorDescriptor): DecoratorDescriptor; + +// The full list of `DecoratorDescriptor`s are listed in the appendix +``` + +## Initialized Computed Property Declarations + +```js +const first = Symbol('first'); +const last = Symbol('last'); + +class Person { + @reader [first] = "Andreas"; + @reader [last] = "Rossberg"; +} +``` + +#### Usage + +```js +let andreas = new Person(); +andreas.first // "Andreas" +andreas.last // "Rossberg" +``` + +#### Resulting Class + +```js +const first = Symbol('first'); +const last = Symbol('last'); + +class Person { + [first] = "Andreas"; + [last] = "Rossberg"; + + get first() { return this[first]; } + get last() { return this[last]; } +} +``` + +#### Desugaring + +```js +const first = Symbol('first'); +const last = Symbol('last'); + +class Person {} + +let target = Person.prototype; + +decorate('field', target, [reader], Property(first, () => "Andreas")); +decorate('field', target, [reader], Property(last, () => "Rossberg")); +``` + +#### Updated Decorator + +Let's update the decorator to handle symbols used for harder-to-access +"private" fields. + +```diff +function reader(target, descriptor) { + let { enumerable, configurable, property: { name, get }, hint } = descriptor; + + // extractPublicName('_first') === 'first' + let publicName = extractPublicName(name() /* extract computed property */); + + // define a public accessor: get first() { return this._first; } + Object.defineProperty(target, publicName, { + // give the public reader the same enumerability and configurability + // as the property it's decorating + enumerable, configurable, get: function() { return get(this, name); } + }); +} + +function extractPublicName(name) { ++ // Symbol(first) -> first ++ if (typeof name === 'symbol') return String(name).slice(7, -1); ++ + // _first -> first + return name.slice(1); +} +``` + +## Uninitialized Field Declarations + +```js +class Person { + @reader _first, _last; + + constructor(first="Waldemar", last="Horwat") { + this._first = first; + this._last = last; + } +} +``` + +#### Usage + +```js +let waldemar = new Person(); +waldemar.first // "Waldemar" +waldemar.last // "Horwat" + +let jeff = new Person("Jeff", "Morrison"); +jeff.first // "Jeff" +jeff.last // "Morrison" +``` + +#### Resulting Class + +```js +class Person { + _first, _last; + + constructor(first="Waldemar", last="Horwat") { + this._first = first; + this._last = last; + } + + get first() { return this._first; } + get last() { return this._last; } +} +``` + +#### Desugaring + +```js +class Person {} + +let prototype = Person.prototype; + +decorate(target, 'field', [reader], Property('_first')); +decorate(target, 'field', [reader], Property('_last')); +``` + +## Initialized Static Properties + +```js +class Person { + @reader static _first = "Brendan"; + @reader static _last = "Eich"; +} +``` + +#### Usage + +```js +let brendan = Person; +brendan.first // "Brendan" +brendan.last // "Eich" +``` + +#### Resulting Class + +```js +class Person { + static _first = "Brendan"; + static _last = "Eich"; + + static get first() { return this._first; } + static get last() { return this._last; } +} +``` + +#### Desugaring: + +```js +class Person {} + +let target = Person; + +decorate(target, 'property', [reader], Property('_first', () => "Brendan"), 'static'); +decorate(target, 'property', [reader], Property('_last', () => "Eich"), 'static'); +``` + +## Uninitialized Static Properties + +```js +class Person { + static @reader _first, _last; +} +``` + +#### Usage + +```js +let jonathan = Person; + +jonathan.first // undefined + +Object.assign(jonathan, { _first: "Jonathan", _last: "Turner" }); + +jonathan.first // "Jonathan" +Jonathan.last // "Turner" +``` + +#### Resulting Class + +```js +class Person { + static _first, _last; + + static get first() { return this._first; } + static get last() { return this._last; } +} +``` + +#### Desugaring + +```js +class Person {} + +let target = PersonF; + +decorate(target, 'property', [reader], Property('_first'), 'static'); +decorate(target, 'property', [reader], Property('_last'), 'static'); +``` + +## Initialized Properties in Object Literals + +```js +let person = { + @reader _first: "Mark", + @reader _last: "Miller" +} +``` + +#### Usage + +```js +person.first // "Mark" +person.last // "Miller" +``` + +#### Resulting Object + +```js +let person = { + _first: "Mark", + _last: "Miller", + + get first() { return this._first; }, + get last() { return this._last; } +} +``` + +#### Desugaring + +```js +let person = {}; + +let target = person; + +decorate(target, 'property', [reader], Property('_first', () => "Mark"), 'explicit')); +decorate(target, 'property', [reader], Property('_last', () => "Miller"), 'explicit')); +``` + +## "Uninitialized" Properties in Object Literals + +```js +let person = { + @reader _first, + @reader _last +} +``` + +#### Usage + +```js +person.first // "undefined" + +Object.assign(person, { _first: "Brian", _last: "Terlson" }); + +person.first // "Brian" +person.last // "Terlson" +``` + +#### Resulting Object + +``` +let person = { + _first: undefined, + _last: undefined, + + get first() { return this._first; }, + get last() { return this._last; } +} +``` + +#### Desugaring + +```js +let person = {}; + +let target = person; + +decorate(target, 'property', [reader], Property('_first', () => _first), 'shorthand'); +decorate(target, 'property', [reader], Property('_last', () => _last, 'shorthand'); +``` + +#### Updated Decorator + +Let's update the decorator to handle shorthand properties interpreted by +the decorator as uninitialized fields. + +```diff +function reader(target, descriptor) { + let { enumerable, configurable, property: { name, get }, hint } = descriptor; + + // extractPublicName('_first') === 'first' + let publicName = extractPublicName(name() /* extract computed property */); + + // define a public accessor: get first() { return this._first; } + Object.defineProperty(target, publicName, { + // give the public reader the same enumerability and configurability + // as the property it's decorating + enumerable, configurable, get: function() { return get(this, name); } + }); + ++ // if we're looking at { @reader _first }, interpret it as { @reader _first: undefined } ++ if (hint === 'shorthand') descriptor.initializer = null; +} + +function extractPublicName(name) { + // Symbol(first) -> first + if (typeof name === 'symbol') return String(name).slice(7, -1); + + // _first -> first + return name.slice(1); +} +``` + +#### Note: Static Properties, Object Literal Shorthand, and Hints + +The decorator descriptors for static properties and object literal +properties are the same, since both describe an immediate installation +of a property onto a target. + +As we've seen, to help a decorator distinguish between the contexts for +high-fidelity syntactic abstractions, the data decorator descriptor +contains an additional `hint` field, which is one of: + +* `static` for static class properties +* `shorthand` for object literal properties defined via shorthand (`{ + @reader _first }`) +* `explicit` for object literal properties defined with an explicit + initializer (`{ @reader _first: "Yehuda" }`) + +## Methods + +The same decorator would work on methods. + +```js +class Person { + @reader _first, _last; + + constructor(first, last) { + this._first = first; + this._last = last; + } + + @reader _update(first, last) { + this._first = first; + this._last = last; + } +} +``` + +#### Usage + +```js +let alex = new Person("Alex", "Russell"); +alex.first // "Alex" +alex.update("Alexander", "Russell"); +alex.first // "Alexander" +``` + +#### Resulting Class + +```js +class Person { + _first, _last; + + constructor(first, last) { + this._first = first; + this._last = last; + } + + _update({ first, last }) { + this._first = first; + this._last = last; + } + + get first() { + return this._first; + } + + get last() { + return this._last; + } + + get update() { + return this._update; + } +} +``` + +#### Desugaring + +```js +class Person { + constructor(first, last) { + this._first = first; + this._last = last; + } +} + +let target = Person.prototype; + +decorate(target, 'field', [reader], Property('_first')); +decorate(target, 'field', [reader], Property('_last')); +decorate(target, 'method', [reader], Property('_update', () => function _update() { /* ... */ })); +``` + +## Symbol Methods + +## Methods + +The same decorator would work on methods. + +```js +const first = Symbol("first"), last = Symbol("last"), update = Symbol("update"); + +class Person { + @reader [first], [last]; + + constructor(first, last) { + this[first] = first; + this[last] = last; + } + + @reader [update](first, last) { + this[first] = first; + this[last] = last; + } +} +``` + +#### Usage + +```js +let alex = new Person("Alex", "Russell"); +alex.first // "Alex" +alex.update("Alexander", "Russell"); +alex.first // "Alexander" +``` + +#### Resulting Class + +```js +const first = Symbol("first"), last = Symbol("last"), update = Symbol("update"); + +class Person { + [first], [last]; + + constructor(first, last) { + this[first] = first; + this[last] = last; + } + + [update](first, last) { + this[first] = first; + this[last] = last; + } + + get first() { + return this[first]; + } + + get last() { + return this[last]; + } + + get update() { + return this[update]; + } +} +``` + +#### Desugaring + +```js +const first = Symbol("first"), last = Symbol("last"), update = Symbol("update"); + +class Person { + constructor(first, last) { + this[first] = first; + this[last] = last; + } +} + +let target = Person.prototype; + +decorate(target, 'field', [reader], Property('_first')); +decorate(target, 'field', [reader], Property('_last')); +decorate(target, 'method', [reader], Property('_update', () => function _update() { /* ... */ })); +``` + +## Getters + +It would also work just fine with getters: + +```js +class Person { + @reader _first, last; + + constructor(first, last) { + this._first = first; + this._last = last; + } + + @reader get _fullName() { + return `${this._first} ${this._last}`; + } +} +``` + +#### Usage + +```js +let jason = new Person("Jason", "Orendorff"); + +jason.first // "Jason" +jason.last // "Orendorff" +jason.fullName // "Jason Orendorff" + +jason.update("JSON", "Orendorff") +jason.first // "JSON" +jason.fullName // "JSON Orendorff" +``` + +#### Resulting Class + +```js +class Person { + constructor(first, last) { + this._first = first; + this._last = last; + } + + get _fullName() { + return `${this._first} ${this._last}`; + } + + get first() { + return this._first; + } + + get last() { + return this._last; + } + + get fullName() { + return this._fullName; + } +} +``` + +#### Desugaring + +```js +class Person { + constructor(first, last) { + this._first = first; + this._last = last; + } +} + +let target = Person.prototype; + +decorate(target, 'field', [reader], Property('_first')); +decorate(target, 'field', [reader], Property('_last')); +decorate(target, 'accessor', [reader], Property({ get() { /* ... */ } }), 'getter'); +``` + +## Appendix: Making `PropertyDefinitionEvaluation` Decoratable + +As an illustration, we'll make `PropertyDefinitionEvaluation` decoratable. The basic +strategy is to run the decorators before the first expression evaluation, passing the +unevaluated expression into the decorator as a function that, when called, evaluates +the expression. + +> This has the rough intuition of "wrap any expressions in the decorated declaration +> in an arrow automatically". + +--- + +First, the existing definition of `PropertyDefinition : PropertyName : AssignmentExpression` + +1. Let propKey be the result of evaluating PropertyName. +2. ReturnIfAbrupt(propKey). +3. Let exprValueRef be the result of evaluating AssignmentExpression. +4. Let propValue be GetValue(exprValueRef). +5. ReturnIfAbrupt(propValue). +6. If IsAnonymousFunctionDefinition(AssignmentExpression) is true, then + a. Let hasNameProperty be HasOwnProperty(propValue, "name"). + b. ReturnIfAbrupt(hasNameProperty). + c. If hasNameProperty is false, perform SetFunctionName(propValue, propKey). +7. Assert: enumerable is true. +8. Return CreateDataPropertyOrThrow(object, propKey, propValue). + +--- + +### DecoratedPropertyDefinition + +What we're going to do is create a new `DecoratedPropertyDefinition`, which looks +like this: + +```js +DecoratedPropertyDefinition: DecoratorExpression+ PropertyDefinition; +``` + +In this case, we we see that Step 1 of the original algorithm evaluates an expression, +so the decorator must intercede at the very beginning of the process. + +The two top-level expressions in the decorated `PropertyDefinition` are `PropertyName` +(which can be a computed property) and `AssignmentExpression`. + +For `DecoratedPropertyDefinition`, the first step is to reify each of the two expressions +into a function that, when called, evaluates the expression (a "thunk"). + +> The following algorithm uses the suffix `?` as a shorthand for `ReturnIfAbrupt`. + +1. Let *propertyNameThunk* = `Thunk(PropertyName)` +2. Let *assignmentExpressionThunk* = `Thunk(AssignmentExpression)` +3. Let *decoratorDescriptor* = + +```js +{ + type: 'property-definition', + name: propertyNameThunk, + initializer: assignmentExpressionThunk, + enumerable: true, + configurable: true, + writable: true +} +``` + +4. For each `DecoratorExpression`, in reverse order: + 1. Let *decorator* = `GetValue(Evaluate(DecoratorExpression))?` + 2. Let *possibleDescriptor* = `decorator(object, decoratorDescriptor)?` + 3. If *possibleDescriptor* is not `undefined`, *decoratorDescriptor* = *possibleDescriptor*. +5. Let *propNameThunk* = `GetValue(Get(decoratorDescriptor, 'name'))?` +6. Let *propName* = `Call(propNameThunk)?` +7. Let *enumerable* = `GetValue(Get(decoratorDescriptor, 'enumerable'))?` +8. Let *configurable* = `GetValue(Get(decoratorDescriptor, 'configurable'))?` +9. Let *writable* = `GetValue(Get(decoratorDescriptor, 'writable'))?` +10. If `'initializer'` in *decoratorDescriptor*: + 1. assert `'get'` not in *decoratorDescriptor*, TypeError + 2. assert `'set'` not in *decoratorDescriptor*, TypeError + 2. Let *propValueThunk* = `GetValue(Get(decoratorDescriptor, 'initializer'))?`. + 3. Let *value* = `Call(propValueThunk)?`. + 4. return `object.[[DefineOwnProperty]](propName, { value, enumerable, configurable, writable })?` +11. Otherwise, if `'get'` in *decoratorDescriptor* or `'set'` in *decoratorDescriptor*: + 1. Let *get* = `GetValue(Get(decoratorDescriptor, 'get'))?` + 2. Let *set* = `GetValue(Get(decoratorDescriptor, 'set'))?` + 3. return `object.[[DefineOwnProperty]](propName, { get, set, enumerable, configurable, writable })?` +12. Otherwise: + 1. Throw a TypeError + +--- + +#### Algorithm: Thunk(Expression) + +1. Let *thunk* = new ThunkedExpression (exotic object) +2. *thunk.[[Expression]]* = Expression +3. Return *thunk* + +--- + +#### The ThunkedExpression Exotic Object + +Internal Slots: + +| Internal Slot | Type | Description | +| ---------------- | ---------- | ----------- | +| `[[Expression]]` | Expression | An unevaluated JavaScript expression | +| `[[Value]]` | any | The value of the JavaScript expression, once evaluated | + +##### [[Call]] + +1. If `this.[[Value]]` is populated, return `this.[[Value]]` +2. Let *value* = `Evaluate(this.[[Expression]])` +3. `this.[[Value]]` = *value* +4. Return *value* + +--- + +### Pseudo-Code + +Described as pseudo-TypeScript, reifying unevaluated expressions into JavaScript values. + +```ts +function PropertyDefinitionEvaluation(object: Object, decorators: Decorator[], definition: PropertyDefinition) { + let propertyNameThunk = new Thunk(definition.PropertyName); + let expressionThunk = new Thunk(definition.AssignmentExpression); + + let initialDescriptor = { + type: 'property-definition', + name: propertyNameThunk, + initializer: assignmentExpressionThunk, + enumerable: true, + configurable: true, + writable: true + }; + + let descriptor = decorators.reverse().reduce((descriptor, decorator) => { + let possibleDescriptor = Evaluate(decorator)(object, descriptor); + return possibleDescriptor === undefined ? descriptor : possibleDescriptor; + }, initialDescriptor); + + let { enumerable, configurable, writable, initializer, get, set } = descriptor; + let name = Call(descriptor.name); + + if ('initializer' in descriptor) { + assert(!('get' in descriptor) && !('set' in descriptor), TypeError); + let value = initializer(); + Object.defineProperty(obj, name, { value, enumerable, configurable, writable }); + } else if ('get' in descriptor || 'set' in descriptor) { + Object.defineProperty(obj, name, { get, set, enumerable, configurable, writable }); + return; + } else { + throw new TypeError(); + } +} + +const EMPTY_SENTINEL = function() {}; + +function Thunk { + let value = EMPTY_SENTINEL; + return function() { + if (value === EMPTY_SENTINEL) value = Evaluate(expression); + return value; + } +} +``` + +## Appendix: Decorator Descriptor List + +##### Decorator Descriptor Interfaces + +The shared interface for all decorator descriptors: + +```ts +interface DecoratorDescriptor { + type: string, // 'property' | 'field' | 'method' | 'accessor' + configurable: boolean, // default: true + enumerable: boolean, // default: true except for methods + property: Property +} +``` + +--- + +```ts +interface PropertyDecoratorDescriptor extends DecoratorDescriptor { + type: string, // 'property' + hint: string, // 'shorthand' or 'explicit' or 'static' + enumerable: boolean, // default: true + configurable: boolean, // default: true + writable: boolean, // default: true + property: DataProperty +} + +interface FieldDecoratorDescriptor extends DecoratorDescriptor { + type: string, // 'field' + enumerable: boolean, // default: true + configurable: boolean, // default: true + writable: boolean, // default: true + property: DataProperty +} + +interface MethodDecoratorDescriptor extends DecoratorDescriptor { + type: string, // 'method' + enumerable: boolean, // default: false + configurable: boolean, // default: true + writable: boolean, // default: true + property: MethodProperty +} + +interface AccessorDecoratorDescriptor { + type: string, // 'accessor' + hint: string, // 'getter', 'setter', 'both' + enumerable: boolean, // default: true + wrtiable: boolean, // default: true + property: AccessorProperty +} +``` + +##### Property Interfaces + +The shared interface for the `property` member of all decorator descriptors. + +```ts +interface Property { + name: () => string; // the name of the property, as a thunk (computed properties) + initializer: any; // the initializer for the property, as a thunk for expressions + get(obj: Class): any; // a function that gets the property for an object (future-proof for slots) + set(obj: Class, value: any); // a function that sets the property for an object (future-proof for slots) +} +``` + +--- + +``` +interface DataProperty extends Property { + // for a property, field or static field, the initializer is a thunk of the expression + initializer?: () => any; +} + +interface MethodProperty extends Property { + // for a method, the initializer is the function + initializer: function, +} + +interface AccessorProperty extends Property { + // for an accessor, the intializer is an object containing the `get` and `set` functions + initializer: { get: () => any, set: (any) => void }, +} +``` + +## Appendix: Assumed Library APIs + +Since this proposal is on a parallel track with declarative fields, it +assumes the following APIs: + +* `Reflect.defineField(constructor, name, desc: FieldDescriptor)` +* `Reflect.getFieldDescriptor(constructor, name): FieldDescriptor)` +* `Reflect.getOwnFieldDescriptor(constructor, name): FieldDescriptor)` + +```ts +interface FieldDescriptor { + initializer: Initializer, // nullable + enumerable: boolean, + configurable: boolean, + writable: boolean +} + +interface Initializer { + (): any // thunk +} +``` + +## Appendix: General Purpose Utilities + +The defininition of the utilities used throughout the rest of this document. + +```js +const DESCRIPTOR_DEFAULTS = { + method: { enumerable: false, configurable: true, writable: true }, + field: { enumerable: true, configurable: true, writable: true }, + property: { enumerable: true, configurable: true, writable: true }, + accessor: { enumerable: true, configurable: true } +} + +export function decorate(type, target, decorators, property, hint) { + let desc = Object.assign({ type, hint, property }, DESCRIPTOR_DEFAULTS[type]); + + descriptor = decorators.reverse() + .reduce((desc, decorator) => applyDecorator(decorator, target, desc), desc) + + let { enumerable, configurable, writable, initializer, get, set, property: { name } } = descriptor; + name = name(); // computed properties + + if ('initializer' in descriptor) { + assert(!('get' in descriptor) && !('set' in descriptor), TypeError); + let value = initializer(); + define(type, target, name, { value, enumerable, configurable, writable }); + } else if ('get' in descriptor || 'set' in descriptor) { + Object.defineProperty(target, name, { get, set, enumerable, configurable, writable }); + return; + } else { + throw new TypeError("Your decorator must return a descriptor with an initializer or an accessor"); + } +} + +function define(type, target, name, descriptor) { + if (type === 'field') Reflect.defineField(target, name, descriptor); + else Object.defineProperty(target, name, descriptor); +} + +function applyDecorator(decorator, target, _descriptor) { + descriptor = decorator(target, _descriptor); + if (descriptor === undefined) return _descriptor; + return descriptor; +} + +export function Property(name, initializer=null) { + return { + name: typeof name === 'function' ? name : () => name, + initializer, + get(obj) { return obj[name]; }, // future-proof for slots + set(obj, value) { obj[name] = value; } // future-proof for slots + } +} +```