Skip to content

Commit

Permalink
Normative: Add initializer callback for side effects (#165)
Browse files Browse the repository at this point in the history
* Normative: Add `kind: "initializer"` for side effects

Many decorators will want to perform a side effect when instantiating
a class, for example:
- To select [[Set]] rather than [[DefineOwnProperty]] semantics for
  defining a field
- To store the field in an entirely different place, neither an ordinary
  property or private field (e.g., MobX stores some fields in a Map)
- To register the instance under construction in some way (e.g., from
  a class decorator). "Instance finishers" have been proposed for this
  purpose, but it's not clear when to run such a finisher. For many use
  cases (e.g., interacting with the DOM), an instance "starter" is just
  as good, due to run-to-completion semantics.

This patch permits decorators to create elements of the form
  { kind: "initializer", placement: "own", initializer: fn }
to be used purely for their side effect, with evaluation semantics
and ordering identical to fields, and interspersed with them in
evaluation order.

* Fix most of the errors from review

* Normative: Throw an exception on a missing initializer

* Normative: Permit prototype and static initializers

Also move error checks in ToElementDescriptor to happen ASAP

* Normative: Throw a TypeError if an initializer returns undefined

* Documentation for initializers

* Add Oxford commas
  • Loading branch information
littledan committed Nov 8, 2018
1 parent 6275ae6 commit cc2db18
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 17 deletions.
16 changes: 16 additions & 0 deletions METAPROGRAMMING.md
Expand Up @@ -138,6 +138,22 @@ A class descriptor with the following properties:

Note: finishers run once per class (at class creation time), not once per instance.

### Initializers

The `extras` array can also contain stand-alone initializers, of the following form:

```js
{
kind: "initializer"
placement: "static", "prototype" or "own",
initializer() { /* ... */ }
}
```

The initializers added here are just like field initializers, except that they don't result in defining a field.

These can be used, e.g., to make a decorator which defines fields through `[[Set]]` rather than `[[DefineOwnProperty]]`, or to store the field in some other place.

## Examples

The three decorators from [README.md](README.md) could be defined as follows:
Expand Down
110 changes: 93 additions & 17 deletions spec.html
Expand Up @@ -159,7 +159,7 @@ <h1>The ElementDescriptor Specification Type</h1>
<tr> <th>Field Name</th> <th>Value</th> </tr>
</thead>
<tbody>
<tr> <td>[[Kind]]</td> <td>One of `"method"` or `"field"`</td> </tr>
<tr> <td>[[Kind]]</td> <td>One of `"method"`, `"field"` or `"initializer"`</td> </tr>
<tr> <td>[[Key]]</td> <td>A Property Key or %PrivateName% object</td> </tr>
<tr> <td>[[Descriptor]]</td> <td>A Property Descriptor</td> </tr>
<tr> <td>[[Placement]]</td> <td>One of `"static"`, `"prototype"`, or `"own"`</td> </tr>
Expand Down Expand Up @@ -188,6 +188,15 @@ <h1>The ElementDescriptor Specification Type</h1>
<li>_element_.[[Descriptor]].[[Configurable]] is *false*.</li>
</ul>
</li>
<li>
If _element_.[[Kind]] is `"initializer", then
<ul>
<li>_element_.[[Key]] absent.</li>
<li>_element_.[[Descriptor]] is absent.</li>
<li>_element_.[[Initializer]] is present.</li>
<li>_element_.[[Decorators]] is absent.</li>
</ul>
</li>
</ul>
</p>
</emu-clause>
Expand Down Expand Up @@ -408,6 +417,54 @@ <h1>CoalesceClassElements ( _elements_ )</h1>
<emu-note>In the case of public class elements, coalescing corresponds in semantics to ValidateAndApplyPropertyDescriptor. Note that this algorithm only coalesces method and accessor declarations, and it leaves field declarations as is.</emu-note>
</emu-clause>

<emu-clause id="initialize-public-instance-elements" aoid="InitializeInstanceFields">
<h1>InitializeInstanceElements ( _O_, _constructor_ )</h1>

<emu-alg>
1. Assert: Type ( _O_ ) is Object.
1. Assert: Assert _constructor_ is an ECMAScript function object.
1. Let _elements_ be the value of _F_'s [[Elements]] internal slot.
1. For each item _element_ in order from _elements_,
1. If _element_.[[Placement]] is `"own"` and _element_.[[Kind]] is `"method"`,
1. Perform ? DefineClassElement(_O_, _element_).
1. For each item _element_ in order from _elements_,
1. If _element_.[[Placement]] is `"own"` and _element_.[[Kind]] is `"field"`,
1. Assert: _element_.[[Descriptor]] does not have a [[Value]], [[Get]] or [[Set]] slot.
1. Perform ? DefineClassElement(_O_, _element_).
1. <ins>If _element_.[[Kind]] is `"initializer"` and _element_.[[Placement]] is `"own"`,</ins>
1. <ins>Let _res_ be ? Call(_element_.[[Initializer]], _O_).</ins>
1. <ins>If _res_ is not *undefined*, throw a *TypeError* exception.</ins>
1. Return.
</emu-alg>
</emu-clause>

<emu-clause id="initialize-class-elements" aoid="InitializeClassElements">
<h1>InitializeClassElements(_F_, _proto_)</h1>

<emu-alg>
1. Assert: Type(_F_) is Object and Type(_proto_) is Object.
1. Assert: _F_ is an ECMAScript function object.
1. Assert: _proto_ is ! Get(_F_, `"prototype"`).
1. Let _elements_ be the value of _F_'s [[Elements]] internal slot.
1. For each item _element_ in order from _elements_,
1. Assert: If _element_.[[Placement]] is `"prototype"` <del>or `"static"`</del>, then _element_.[[Key]] is not a Private Name.
1. If _element_.[[Kind]] is `"method"`,
1. Let _receiver_ be _F_ if _element_.[[Placement]] is `"static"`, else let _receiver_ be _proto_.
1. Perform ? DefineClassElement(_receiver_, _element_).
1. For each item _element_ in order from _elements_,
1. If _element_.[[Kind]] is `"field"` and _element_.[[Placement]] is `"static"` or `"prototype"`,
1. Assert: _element_.[[Descriptor]] does not have a [[Value]], [[Get]] or [[Set]] slot.
1. Let _receiver_ be _F_ if _element_.[[Placement]] is `"static"`, else let _receiver_ be _proto_.
1. Perform ? DefineClassElement(_receiver_, _element_).
1. <ins>If _element_.[[Placement]] is `"prototype"` or `"static"` and _element_.[[Kind]] is `"initializer"`,</ins>
1. <ins>Let _receiver_ be _F_ if _element_.[[Placement]] is `"static"`, else let _receiver_ be _proto_.</ins>
1. <ins>Let _res_ be ? Call(_element_.[[Initializer]], _receiver_).</ins>
1. <ins>If _res_ is not *undefined*, throw a *TypeError* exception.</ins>
1. Return.
</emu-alg>
<emu-note type=editor>Value properties are added before initializers so that all methods are visible from all initializers</emu-note>
</emu-clause>

</emu-clause>


Expand Down Expand Up @@ -563,7 +620,7 @@ <h1>Element Descriptors</h1>
<p>An <dfn>element descriptor</dfn> describes an element of a class or object literal and has the following shape:</p>
<pre><code class=typescript>
interface ElementDescriptor {
kind: "method" or "field"
kind: "method", "initializer", or "field"
key: String, Symbol or Private Name,
placement: "static", "prototype", or "own"
descriptor: PropertyDescriptor,
Expand Down Expand Up @@ -689,7 +746,12 @@ <h1>DecorateConstructor ( _elements_, _decorators_ )</h1>
1. Append _elementsAndFinisher_.[[Finisher]] to _finishers_.
1. If _elementsAndFinisher_.[[Elements]] is not *undefined*,
1. Set _elements_ to the concatenation of _elementsAndFinisher_.[[Elements]] and _privateElements_.
1. If there are two class elements _a_ and _b_ in _elements_ such that _a_.[[Key]] is _b_.[[Key]] and _a_.[[Placement]] is _b_.[[Placement]], throw a *TypeError* exception.
1. If there are two class elements _a_ and _b_ in _elements_ such that all of the following are true:
1. _a_.[[Kind]] is not `"initializer"`
1. _b_.[[Kind]] is not `"initializer"`
1. _a_.[[Key]] is _b_.[[Key]]
1. _a_.[[Placement]] is _b_.[[Placement]]
1. Then, throw a *TypeError* exception.
1. Return the Record { [[Elements]]: _elements_, [[Finishers]]: _finishers_ }.
</emu-alg>
</emu-clause>
Expand Down Expand Up @@ -744,12 +806,14 @@ <h1>FromElementDescriptor ( _element_ )</h1>
1. Let _desc_ be PropertyDescriptor{ [[Value]]: `"Descriptor"`, [[Writable]]: *false*, [[Enumerable]]: *false*, [[Configurable]]: *true* }.
1. Perform ! DefinePropertyOrThrow(_obj_, @@toStringTag, _desc_).
1. Perform ! CreateDataPropertyOrThrow(_obj_, `"kind"`, _element_.[[Kind]]).
1. Let _key_ be _element_.[[Key]].
1. If _key_ is a Private Name, set _key_ to ? PrivateNameObject(_key_).
1. Perform ! CreateDataPropertyOrThrow(_obj_, `"key"`, _key_).
1. If _element_.[[Kind]] is `"method"` or `"field"`,
1. Let _key_ be _element_.[[Key]].
1. If _key_ is a Private Name, set _key_ to ? PrivateNameObject(_key_).
1. Perform ! CreateDataPropertyOrThrow(_obj_, `"key"`, _key_).
1. Perform ! CreateDataPropertyOrThrow(_obj_, `"placement"`, _element_.[[Placement]]).
1. Perform ! CreateDataPropertyOrThrow(_obj_, `"descriptor"`, ! FromPropertyDescriptor(_element_.[[Descriptor]])).
1. If _element_.[[Kind]] is `"field"`,
1. If _element_.[[Kind]] is `"method"` or `"field"`,
1. Perform ! CreateDataPropertyOrThrow(_obj_, `"descriptor"`, ! FromPropertyDescriptor(_element_.[[Descriptor]])).
1. If _element_.[[Kind]] is `"field"` or `"initializer"`,
1. Let _initializer_ be _element_.[[Initializer]].
1. If _initializer_ is ~empty~, set _initializer_ to *undefined*.
1. Perform ! CreateDataPropertyOrThrow(_obj_, `"initializer"`, _initializer_).
Expand Down Expand Up @@ -781,26 +845,38 @@ <h1>ToElementDescriptor ( _elementObject_ )</h1>
<emu-alg>
1. Assert: _elementObject_ is an ECMAScript language value.
1. Let _kind_ be ? ToString(? Get(_elementObject_, `"kind"`)).
1. If _kind_ is not one of `"method"` or `"field"`, throw a *TypeError* exception.
1. If _kind_ is not one of `"initializer"`, `"method"`, or `"field"`, throw a *TypeError* exception.
1. Let _key_ be ? Get(_elementObject_, `"key"`).
1. If _kind_ is `"initializer"`,
1. If _key_ is not *undefined*, throw a *TypeError* exception.
1. If _key_ has a [[PrivateName]] internal slot, set _key_ to _key_.[[PrivateName]].
1. Otherwise, set _key_ to ? ToPropertyKey(_key_).
1. Let _placement_ be ? ToString(? Get(_elementObject_, `"placement"`)).
1. If _placement_ is not one of `"static"`, `"prototype"`, or `"own"`, throw a *TypeError* exception.
1. Let _descriptor_ be ? ToPropertyDescriptor(? Get(_elementObject_, `"descriptor"`)).
1. Let _initializer_ be ? Get(_elementObject_, `"initializer"`).
1. Let _elements_ be ? Get(_elementObject_, `"elements"`).
1. If _elements_ is not *undefined*, throw a *TypeError* exception.
1. If _kind_ not `"field"`,
1. If _initializer_ is not *undefined*, throw a *TypeError* exception.
1. Let _descriptor_ be ? Get(_elementObject_, `"descriptor"`)
1. If _kind_ is `"initializer"`,
1. If _descriptor_ is not *undefined*, throw a *TypeError* exception.
1. Otherwise,
1. Set _descriptor_ to ? ToPropertyDescriptor(_descriptor_).
1. If _key_ is a Private Name,
1. If _descriptor_.[[Enumerable]] is *true*, throw a *TypeError* exception.
1. If _descriptor_.[[Configurable]] is *true*, throw a *TypeError* exception.
1. If _placement_ is `"prototype"`, throw a *TypeError* exception.
1. If _kind_ is `"field"`,
1. If _descriptor_ has a [[Get]], [[Set]] or [[Value]] internal slot, throw a *TypeError* exception.
1. Let _element_ be the ElementDescriptor { [[Kind]]: _kind_, [[Key]]: _key_, [[Placement]]: _placement_, [[Descriptor]]: _descriptor_ }.
1. If _kind_ is `"field"`, set _element_.[[Initializer]] to _initializer_.
1. Let _initializer_ be ? Get(_elementObject_, `"initializer"`).
1. If _kind_ is `"initializer"`,
1. If _initializer_ is *undefined*, throw a *TypeError* exception
1. If _kind_ is `"method"`,
1. If _initializer_ is not *undefined*, throw a *TypeError* exception.
1. Let _elements_ be ? Get(_elementObject_, `"elements"`).
1. If _elements_ is not *undefined*, throw a *TypeError* exception.
1. Let _element_ be the ElementDescriptor { [[Kind]]: _kind_, [[Placement]]: _placement_ }.
1. If _kind_ is `"method"` or `"field",
1. Set _element_.[[Key]] to _key_.
1. Set _element_.[[Descriptor]] to _descriptor_.
1. If _kind_ is `"field"` or `"initializer"`,
1. Set _element_.[[Initializer]] to _initializer_.
1. Return _element_.
</emu-alg>
</emu-clause>
Expand Down

0 comments on commit cc2db18

Please sign in to comment.