1 | These are notes about the implementation of the 2021-12 decorators transform.
|
2 | The implementation's goals are (in descending order):
|
3 |
|
4 | 1. Being accurate to the actual proposal (e.g. not defining additional
|
5 | properties unless required, matching semantics exactly, etc.). This includes
|
6 | being able to work properly with private fields and methods.
|
7 | 2. Transpiling to a very minimal and minifiable output. This transform will
|
8 | affect each and every decorated class, so ensuring that the output is not 10x
|
9 | the size of the original is important.
|
10 | 3. Having good runtime performance. Decoration output has the potential to
|
11 | drastically impact startup performance, since it runs whenever a decorated
|
12 | class is defined. In addition, every instance of a decorated class may be
|
13 | impacted for certain types of decorators.
|
14 |
|
15 | All of these goals come somewhat at the expense of readability and can make the
|
16 | implementation difficult to understand, so these notes are meant to document the
|
17 | motivations behind the design.
|
18 |
|
19 | ## Overview
|
20 |
|
21 | Given a simple decorated class like this one:
|
22 |
|
23 | ```js
|
24 | @dec
|
25 | class Class {
|
26 | @dec a = 123;
|
27 |
|
28 | @dec static #b() {
|
29 | console.log('foo');
|
30 | }
|
31 |
|
32 | [someVal]() {}
|
33 |
|
34 | @dec
|
35 | @dec2
|
36 | accessor #c = 456;
|
37 | }
|
38 | ```
|
39 |
|
40 | It's output would be something like the following:
|
41 |
|
42 | ```js
|
43 | import { applyDecs } from '@babel/helpers';
|
44 |
|
45 | let _initInstance, _initClass, _initStatic, _init_a, _call_b, _computedKey, _init_c, _get_c, _set_c;
|
46 |
|
47 | let _dec = dec,
|
48 | _dec2 = dec,
|
49 | _computedKey = someVal,
|
50 | _dec3 = dec,
|
51 | _dec4 = dec2;
|
52 |
|
53 | let _Class;
|
54 | class Class {
|
55 | static {
|
56 | [
|
57 | _init_a,
|
58 | _call_b,
|
59 | _init_c,
|
60 | _get_c,
|
61 | _set_c,
|
62 | _Class,
|
63 | _initClass,
|
64 | _initProto,
|
65 | _initStatic,
|
66 | ] = _applyDecs(Class,
|
67 | [
|
68 | [_dec, 0, "a"],
|
69 | [
|
70 | _dec2,
|
71 | 7,
|
72 | "b",
|
73 | function () {
|
74 | console.log('foo');
|
75 | }
|
76 | ],
|
77 | [
|
78 | [_dec4, _dec5],
|
79 | 1,
|
80 | "c",
|
81 | function () {
|
82 | return this.#a;
|
83 | },
|
84 | function (value) {
|
85 | this.#a = value;
|
86 | }
|
87 | ]
|
88 | ],
|
89 | [dec]
|
90 | );
|
91 |
|
92 | _initStatic(Class);
|
93 | }
|
94 |
|
95 | a = (initInstance(this), _init_a(this, 123));
|
96 |
|
97 | static #b(...args) {
|
98 | _call_b(this, args);
|
99 | }
|
100 |
|
101 | [_computedKey]() {}
|
102 |
|
103 | #a = _init_c(this, 123);
|
104 | get #c() {
|
105 | return _get_c(this);
|
106 | }
|
107 | set #c(v) {
|
108 | _set_c(this, v);
|
109 | }
|
110 |
|
111 | static {
|
112 | initClass(C);
|
113 | }
|
114 | }
|
115 | ```
|
116 |
|
117 | Let's break this output down a bit:
|
118 |
|
119 | ```js
|
120 | let initInstance, initClass, _init_a, _call_b, _init_c, _get_c, _set_c;
|
121 | ```
|
122 |
|
123 | First, we need to setup some local variables outside of the class. These are
|
124 | for:
|
125 |
|
126 | - Decorated class field/accessor initializers
|
127 | - Extra initializer functions added by `addInitializers`
|
128 | - Private class methods
|
129 |
|
130 | These are essentially all values that cannot be defined on the class itself via
|
131 | `Object.defineProperty`, so we have to insert them into the class manually,
|
132 | ahead of time and populate them when we run our decorators.
|
133 |
|
134 | ```js
|
135 | let _dec = dec,
|
136 | _dec2 = dec,
|
137 | _computedKey = someVal,
|
138 | _dec3 = dec,
|
139 | _dec4 = dec2;
|
140 | ```
|
141 |
|
142 | Next up, we define and evaluate the decorator expressions. The reason we
|
143 | do this _before_ defining the class is because we must interleave decorator
|
144 | expressions with computed property key expressions, since computed properties
|
145 | and decorators can run arbitrary code which can modify the runtime of subsequent
|
146 | decorators or computed property keys.
|
147 |
|
148 | ```js
|
149 | let _Class;
|
150 | class Class {
|
151 | ```
|
152 |
|
153 | This class is being decorated directly, which means that the decorator may
|
154 | replace the class itself. Class bindings are not mutable, so we need to create a
|
155 | new `let` variable for the decorated class.
|
156 |
|
157 |
|
158 | ```js
|
159 | static {
|
160 | [
|
161 | _init_a,
|
162 | _call_b,
|
163 | _init_c,
|
164 | _get_c,
|
165 | _set_c,
|
166 | _Class,
|
167 | _initClass,
|
168 | _initProto,
|
169 | _initStatic,
|
170 | ] = _applyDecs(Class,
|
171 | [
|
172 | [_dec, 0, "a"],
|
173 | [
|
174 | _dec2,
|
175 | 7,
|
176 | "b",
|
177 | function () {
|
178 | console.log('foo');
|
179 | }
|
180 | ],
|
181 | [
|
182 | [_dec4, _dec5],
|
183 | 1,
|
184 | "c",
|
185 | function () {
|
186 | return this.#a;
|
187 | },
|
188 | function (value) {
|
189 | this.#a = value;
|
190 | }
|
191 | ]
|
192 | ],
|
193 | [dec]
|
194 | );
|
195 |
|
196 | _initStatic(Class);
|
197 | }
|
198 | ```
|
199 |
|
200 | Next, we immediately define a `static` block which actually applies the
|
201 | decorators. This is important because we must apply the decorators _after_ the
|
202 | class prototype has been fully setup, but _before_ static fields are run, since
|
203 | static fields should only see the decorated version of the class.
|
204 |
|
205 | We apply the decorators to class elements and the class itself, and the
|
206 | application returns an array of values that are used to populate all of the
|
207 | local variables we defined earlier. The array's order is fully deterministic, so
|
208 | we can assign the values based on an index we can calculate ahead of time.
|
209 |
|
210 | Another important thing to note here is that we're passing some functions here.
|
211 | These are for private methods and accessors, which cannot be replaced directly
|
212 | so we have to extract their code so it can be decorated. Because we define these
|
213 | within the static block, they can access any private identifiers which were
|
214 | defined within the class, so it's not an issue that we're extracting the method
|
215 | logic here.
|
216 |
|
217 | We'll come back to `applyDecs` in a bit to dig into what its format is exactly,
|
218 | but now let's dig into the new definitions of our class elements.
|
219 |
|
220 | ```js
|
221 | a = (_initInstance(this), _init_a(this, 123));
|
222 | ```
|
223 |
|
224 | Alright, so previously this was a simple class field. Since it's the first field
|
225 | on the class, we've updated it to immediately call `initInstance` in its
|
226 | initializer. This calls any initializers added with `addInitializer` for all of
|
227 | the per-class values (methods and accessors), which should all be setup on the
|
228 | instance before class fields are assigned. Then, it calls `_init_a` to get the
|
229 | initial value of the field, which allows initializers returned from the
|
230 | decorator to intercept and decorate it. It's important that the initial value
|
231 | is used/defined _within_ the class body, because initializers can now refer to
|
232 | private class fields, e.g. `a = this.#b` is a valid field initializer and would
|
233 | become `a = _init_a(this, this.#b)`, which would also be valid. We cannot
|
234 | extract initializer code, or any other code, from the class body because of
|
235 | this.
|
236 |
|
237 | Overall, this decoration is pretty straightforward other than the fact that we
|
238 | have to reference `_init_a` externally.
|
239 |
|
240 | ```js
|
241 | static #b(...args) {
|
242 | _call_b(this, args);
|
243 | }
|
244 | ```
|
245 |
|
246 | Next up, we have a private static class method `#b`. This one is a bit more
|
247 | complex, as our definition has been broken out into 2 parts:
|
248 |
|
249 | 1. `static #b`: This is the method itself, which being a private method we
|
250 | cannot overwrite with `defineProperty`. We also can't convert it into a
|
251 | private field because that would change its semantics (would make it
|
252 | writable). So, we instead have it proxy to the locally scoped `_call_b`
|
253 | variable, which will be populated with the fully decorated method.
|
254 | 2. The definition of the method, kept in `_call_b`. As we mentioned above, the
|
255 | original method's code is moved during the decoration process, and the wrapped
|
256 | version is populated in `_call_b` and called whenever the private method is
|
257 | called.
|
258 |
|
259 | ```js
|
260 | [_computedKey]() {}
|
261 | ```
|
262 |
|
263 | Next is the undecorated method with a computed key. This uses the previously
|
264 | calculated and stored computed key.
|
265 |
|
266 | ```js
|
267 | #a = _init_c(this, 123);
|
268 | get #c() {
|
269 | return _get_c(this);
|
270 | }
|
271 | set #c(v) {
|
272 | _set_c(this, v);
|
273 | }
|
274 | ```
|
275 |
|
276 | Next up, we have the output for `accessor #c`. This is the most complicated
|
277 | case, since we have to transpile the decorators, the `accessor` keyword, and
|
278 | target a private field. Breaking it down piece by piece:
|
279 |
|
280 | ```js
|
281 | #a = _init_c(this, 123);
|
282 | ```
|
283 |
|
284 | `accessor #c` desugars to a getter and setter which are backed by a new private
|
285 | field, `#a`. Like before, the name of this field doesn't really matter, we'll
|
286 | just generate a short, unique name. We call the decorated initializer for `#c`
|
287 | and return that value to assign to the field.
|
288 |
|
289 | ```js
|
290 | get #c() {
|
291 | return _get_c(this);
|
292 | }
|
293 | set #c(v) {
|
294 | _set_c(this, v);
|
295 | }
|
296 | ```
|
297 |
|
298 | Next, we have the getter and setter for `#c` itself. These methods defer to
|
299 | the `_get_c` and `_set_c` local variables, which will be the decorated versions
|
300 | of the two getter functions that we passed for decoration in the static block
|
301 | above. Those two functions are essentially just accessors for the private `#a`
|
302 | field, but the decorator may add additional logic to them.
|
303 |
|
304 | ```js
|
305 | static {
|
306 | _initClass(_Class);
|
307 | }
|
308 | ```
|
309 |
|
310 | Finally, we call `_initClass` in another static block, running any class and
|
311 | static method initializers on the final class. This is done in a static block
|
312 | for convenience with class expressions, but it could run immediately after the
|
313 | class is defined.
|
314 |
|
315 | Ok, so now that we understand the general output, let's go back to `applyDecs`:
|
316 |
|
317 | ```js
|
318 | [
|
319 | _init_a,
|
320 | _call_b,
|
321 | _init_c,
|
322 | _get_c,
|
323 | _set_c,
|
324 | _Class,
|
325 | _initClass,
|
326 | _initProto,
|
327 | _initStatic,
|
328 | ] = _applyDecs(Class,
|
329 | [
|
330 | [_dec, 0, "a"],
|
331 | [
|
332 | _dec2,
|
333 | 7,
|
334 | "b",
|
335 | function () {
|
336 | console.log('foo');
|
337 | }
|
338 | ],
|
339 | [
|
340 | [_dec4, _dec5],
|
341 | 1,
|
342 | "c",
|
343 | function () {
|
344 | return this.#a;
|
345 | },
|
346 | function (value) {
|
347 | this.#a = value;
|
348 | }
|
349 | ]
|
350 | ],
|
351 | [dec]
|
352 | );
|
353 | ```
|
354 |
|
355 | `applyDecs` takes all of the decorators for the class and applies them. It
|
356 | receives the following arguments:
|
357 |
|
358 | 1. The class itself
|
359 | 2. Decorators to apply to class elements
|
360 | 3. Decorators to apply to the class itself
|
361 |
|
362 | The format of the data is designed to be as minimal as possible. Here's an
|
363 | annotated version of the member descriptors:
|
364 |
|
365 | ```js
|
366 | [
|
367 | // List of decorators to apply to the field. Array if multiple decorators,
|
368 | // otherwise just the single decorator itself.
|
369 | dec,
|
370 |
|
371 | // The type of the decorator, represented as an enum. Static-ness is also
|
372 | // encoded by adding 5 to the values
|
373 | // 0 === FIELD
|
374 | // 1 === ACCESSOR
|
375 | // 2 === METHOD
|
376 | // 3 === GETTER
|
377 | // 4 === SETTER
|
378 | // 5 === FIELD + STATIC
|
379 | // 6 === ACCESSOR + STATIC
|
380 | // 7 === METHOD + STATIC
|
381 | // 8 === GETTER + STATIC
|
382 | // 9 === SETTER + STATIC
|
383 | 1,
|
384 |
|
385 | // The name of the member
|
386 | 'y',
|
387 |
|
388 | // Optional fourth and fifth values, these are functions passed for private
|
389 | // decorators
|
390 | function() {}
|
391 | ],
|
392 | ```
|
393 |
|
394 | Static and prototype decorators are all described like this. For class
|
395 | decorators, it's just the list of decorators since no other context
|
396 | is necessary.
|