oxc-parser 0.90.0 → 0.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,402 @@
1
+ // Functions to compile 1 or more visitor objects into a single compiled visitor.
2
+ //
3
+ // # Visitor objects
4
+ //
5
+ // Visitor objects which are generated by rules' `create` functions have keys being either:
6
+ // * Name of an AST type. or
7
+ // * Name of an AST type postfixed with `:exit`.
8
+ //
9
+ // Each property value must be a function that handles that AST node.
10
+ //
11
+ // e.g.:
12
+ //
13
+ // ```
14
+ // {
15
+ // BinaryExpression(node) {
16
+ // // Do stuff on enter
17
+ // },
18
+ // 'BinaryExpression:exit'(node) {
19
+ // // Do stuff on exit
20
+ // },
21
+ // }
22
+ // ```
23
+ //
24
+ // # Compiled visitor
25
+ //
26
+ // Compiled visitor is an array with `NODE_TYPES_COUNT` length, keyed by the ID of the node type.
27
+ // `NODE_TYPE_IDS_MAP` maps from type name to ID.
28
+ //
29
+ // Each element of compiled array is one of:
30
+ // * No visitor for this type = `null`.
31
+ // * Visitor for leaf node = visit function.
32
+ // * Visitor for non-leaf node = object of form `{ enter, exit }`,
33
+ // where each property is either a visitor function or `null`.
34
+ //
35
+ // e.g.:
36
+ //
37
+ // ```
38
+ // [
39
+ // // Leaf nodes
40
+ // function(node) { /* do stuff */ },
41
+ // // ...
42
+ //
43
+ // // Non-leaf nodes
44
+ // {
45
+ // enter: function(node) { /* do stuff */ },
46
+ // exit: null,
47
+ // },
48
+ // // ...
49
+ // ]
50
+ // ```
51
+ //
52
+ // # Object reuse
53
+ //
54
+ // No more than 1 compiled visitor exists at any time, so we reuse a single array `compiledVisitor`,
55
+ // rather than creating a new array for each file being linted.
56
+ //
57
+ // To compile visitors, call:
58
+ // * `initCompiledVisitor` once.
59
+ // * `addVisitorToCompiled` with each visitor object.
60
+ // * `finalizeCompiledVisitor` once.
61
+ //
62
+ // After this sequence of calls, `compiledVisitor` is ready to be used to walk the AST.
63
+ //
64
+ // We also recycle:
65
+ //
66
+ // * `{ enter, exit }` objects which are stored in compiled visitor.
67
+ // * Temporary arrays used to store multiple visit functions, which are merged into a single function
68
+ // in `finalizeCompiledVisitor`.
69
+ //
70
+ // The aim is to reduce pressure on the garbage collector. All these recycled objects are long-lived
71
+ // and will graduate to "old space", which leaves as much capacity as possible in "new space"
72
+ // for objects created by user code in visitors. If ephemeral user-created objects all fit in new space,
73
+ // it will avoid full GC runs, which should greatly improve performance.
74
+
75
+ // TODO(camc314): we need to generate `.d.ts` file for this module.
76
+ // @ts-expect-error
77
+ import { LEAF_NODE_TYPES_COUNT, NODE_TYPE_IDS_MAP, NODE_TYPES_COUNT } from '../../generated/visit/types.mjs';
78
+
79
+ const { isArray } = Array;
80
+
81
+ // Compiled visitor used for visiting each file.
82
+ // Same array is reused for each file.
83
+ //
84
+ // Initialized with `.push()` to ensure V8 treats the array as "packed" (linear array),
85
+ // not "holey" (hash map). This is critical, as looking up elements in this array is a very hot path
86
+ // during AST visitation, and holey arrays are much slower.
87
+ // https://v8.dev/blog/elements-kinds
88
+ let compiledVisitor;
89
+
90
+ export function createCompiledVisitor() {
91
+ // Create a new compiled visitor array
92
+ compiledVisitor = [];
93
+ for (let i = NODE_TYPES_COUNT; i !== 0; i--) {
94
+ compiledVisitor.push(null);
95
+ }
96
+ return compiledVisitor;
97
+ }
98
+
99
+ // Arrays containing type IDs of types which have multiple visit functions defined for them.
100
+ //
101
+ // Filled with `0` initially up to maximum size they could ever need to be so:
102
+ // 1. These arrays never need to grow.
103
+ // 2. V8 treats these arrays as "PACKED_SMI_ELEMENTS".
104
+ const mergedLeafVisitorTypeIds = [],
105
+ mergedEnterVisitorTypeIds = [],
106
+ mergedExitVisitorTypeIds = [];
107
+
108
+ for (let i = LEAF_NODE_TYPES_COUNT; i !== 0; i--) {
109
+ mergedLeafVisitorTypeIds.push(0);
110
+ }
111
+
112
+ for (let i = NODE_TYPES_COUNT - LEAF_NODE_TYPES_COUNT; i !== 0; i--) {
113
+ mergedEnterVisitorTypeIds.push(0);
114
+ mergedExitVisitorTypeIds.push(0);
115
+ }
116
+
117
+ mergedLeafVisitorTypeIds.length = 0;
118
+ mergedEnterVisitorTypeIds.length = 0;
119
+ mergedExitVisitorTypeIds.length = 0;
120
+
121
+ // `true` if `addVisitor` has been called with a visitor which visits at least one AST type
122
+ let hasActiveVisitors = false;
123
+
124
+ // Enter+exit object cache.
125
+ //
126
+ // `compiledVisitor` may contain many `{ enter, exit }` objects.
127
+ // Use this cache to reuse those objects across all visitor compilations.
128
+ //
129
+ // `enterExitObjectCacheNextIndex` is the index of first object in cache which is currently unused.
130
+ // It may point to the end of the cache array.
131
+ const enterExitObjectCache = [];
132
+ let enterExitObjectCacheNextIndex = 0;
133
+
134
+ function getEnterExitObject() {
135
+ if (enterExitObjectCacheNextIndex < enterExitObjectCache.length) {
136
+ return enterExitObjectCache[enterExitObjectCacheNextIndex++];
137
+ }
138
+
139
+ const enterExit = { enter: null, exit: null };
140
+ enterExitObjectCache.push(enterExit);
141
+ enterExitObjectCacheNextIndex++;
142
+ return enterExit;
143
+ }
144
+
145
+ // Visit function arrays cache.
146
+ //
147
+ // During compilation, many arrays may be used temporarily to store multiple visit functions for same AST type.
148
+ // The functions in each array are merged into a single function in `finalizeCompiledVisitor`,
149
+ // after which these arrays aren't used again.
150
+ //
151
+ // Use this cache to reuse these arrays across each visitor compilation.
152
+ //
153
+ // `visitFnArrayCacheNextIndex` is the index of first array in cache which is currently unused.
154
+ // It may point to the end of the cache array.
155
+ const visitFnArrayCache = [];
156
+ let visitFnArrayCacheNextIndex = 0;
157
+
158
+ function createVisitFnArray(visit1, visit2) {
159
+ if (visitFnArrayCacheNextIndex < visitFnArrayCache.length) {
160
+ const arr = visitFnArrayCache[visitFnArrayCacheNextIndex++];
161
+ arr.push(visit1, visit2);
162
+ return arr;
163
+ }
164
+
165
+ const arr = [visit1, visit2];
166
+ visitFnArrayCache.push(arr);
167
+ visitFnArrayCacheNextIndex++;
168
+ return arr;
169
+ }
170
+
171
+ /**
172
+ * Initialize compiled visitor, ready for calls to `addVisitor`.
173
+ */
174
+ export function initCompiledVisitor() {
175
+ // Reset `compiledVisitor` array after previous compilation
176
+ for (let i = 0; i < NODE_TYPES_COUNT; i++) {
177
+ compiledVisitor[i] = null;
178
+ }
179
+
180
+ // Reset enter+exit objects which were used in previous compilation
181
+ for (let i = 0; i < enterExitObjectCacheNextIndex; i++) {
182
+ const enterExit = enterExitObjectCache[i];
183
+ enterExit.enter = null;
184
+ enterExit.exit = null;
185
+ }
186
+ enterExitObjectCacheNextIndex = 0;
187
+ }
188
+
189
+ /**
190
+ * Add a visitor to compiled visitor.
191
+ *
192
+ * @param visitor - Visitor object
193
+ */
194
+ export function addVisitorToCompiled(visitor) {
195
+ if (visitor === null || typeof visitor !== 'object') throw new TypeError('Visitor must be an object');
196
+
197
+ // Exit if is empty visitor
198
+ const keys = Object.keys(visitor),
199
+ keysLen = keys.length;
200
+ if (keysLen === 0) return;
201
+
202
+ hasActiveVisitors = true;
203
+
204
+ // Populate visitors array from provided object
205
+ for (let i = 0; i < keysLen; i++) {
206
+ let name = keys[i];
207
+
208
+ const visitFn = visitor[name];
209
+ if (typeof visitFn !== 'function') {
210
+ throw new TypeError(`'${name}' property of visitor object is not a function`);
211
+ }
212
+
213
+ const isExit = name.endsWith(':exit');
214
+ if (isExit) name = name.slice(0, -5);
215
+
216
+ const typeId = NODE_TYPE_IDS_MAP.get(name);
217
+ if (typeId === void 0) throw new Error(`Unknown node type '${name}' in visitor object`);
218
+
219
+ const existing = compiledVisitor[typeId];
220
+ if (typeId < LEAF_NODE_TYPES_COUNT) {
221
+ // Leaf node - store just 1 function, not enter+exit pair
222
+ if (existing === null) {
223
+ compiledVisitor[typeId] = visitFn;
224
+ } else if (isArray(existing)) {
225
+ if (isExit) {
226
+ existing.push(visitFn);
227
+ } else {
228
+ // Insert before last in array in case last was enter visit function from the current rule,
229
+ // to ensure enter is called before exit.
230
+ // It could also be either an enter or exit visitor function for another rule, but the order
231
+ // rules are called in doesn't matter. We only need to make sure that a rule's exit visitor
232
+ // isn't called before enter visitor *for that same rule*.
233
+ existing.splice(existing.length - 1, 0, visitFn);
234
+ }
235
+ } else {
236
+ // Same as above, enter visitor is put to front of list to make sure enter is called before exit
237
+ compiledVisitor[typeId] = isExit
238
+ ? createVisitFnArray(existing, visitFn)
239
+ : createVisitFnArray(visitFn, existing);
240
+ mergedLeafVisitorTypeIds.push(typeId);
241
+ }
242
+ } else {
243
+ // Not leaf node - store enter+exit pair
244
+ if (existing === null) {
245
+ const enterExit = compiledVisitor[typeId] = getEnterExitObject();
246
+ if (isExit) {
247
+ enterExit.exit = visitFn;
248
+ } else {
249
+ enterExit.enter = visitFn;
250
+ }
251
+ } else if (isExit) {
252
+ const { exit } = existing;
253
+ if (exit === null) {
254
+ existing.exit = visitFn;
255
+ } else if (isArray(exit)) {
256
+ exit.push(visitFn);
257
+ } else {
258
+ existing.exit = createVisitFnArray(exit, visitFn);
259
+ mergedExitVisitorTypeIds.push(typeId);
260
+ }
261
+ } else {
262
+ const { enter } = existing;
263
+ if (enter === null) {
264
+ existing.enter = visitFn;
265
+ } else if (isArray(enter)) {
266
+ enter.push(visitFn);
267
+ } else {
268
+ existing.enter = createVisitFnArray(enter, visitFn);
269
+ mergedEnterVisitorTypeIds.push(typeId);
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Finalize compiled visitor.
278
+ *
279
+ * After calling this function, `compiledVisitor` is ready to be used to walk the AST.
280
+ *
281
+ * @returns {boolean} - `true` if compiled visitor visits at least 1 AST type
282
+ */
283
+ export function finalizeCompiledVisitor() {
284
+ if (hasActiveVisitors === false) return false;
285
+
286
+ // Merge visit functions for node types which have multiple visitors from different rules,
287
+ // or enter+exit functions for leaf nodes
288
+ for (let i = mergedLeafVisitorTypeIds.length - 1; i >= 0; i--) {
289
+ const typeId = mergedLeafVisitorTypeIds[i];
290
+ compiledVisitor[typeId] = mergeVisitFns(compiledVisitor[typeId]);
291
+ }
292
+
293
+ for (let i = mergedEnterVisitorTypeIds.length - 1; i >= 0; i--) {
294
+ const typeId = mergedEnterVisitorTypeIds[i];
295
+ const enterExit = compiledVisitor[typeId];
296
+ enterExit.enter = mergeVisitFns(enterExit.enter);
297
+ }
298
+
299
+ for (let i = mergedExitVisitorTypeIds.length - 1; i >= 0; i--) {
300
+ const typeId = mergedExitVisitorTypeIds[i];
301
+ const enterExit = compiledVisitor[typeId];
302
+ enterExit.exit = mergeVisitFns(enterExit.exit);
303
+ }
304
+
305
+ // Reset state, ready for next time
306
+ mergedLeafVisitorTypeIds.length = 0;
307
+ mergedEnterVisitorTypeIds.length = 0;
308
+ mergedExitVisitorTypeIds.length = 0;
309
+
310
+ // Note: Visit function arrays have been emptied in `mergeVisitFns`, so all arrays in `visitFnArrayCache`
311
+ // are now empty and ready for reuse. We just need to reset the index.
312
+ visitFnArrayCacheNextIndex = 0;
313
+
314
+ hasActiveVisitors = false;
315
+
316
+ return true;
317
+ }
318
+
319
+ /**
320
+ * Merge array of visit functions into a single function, which calls each of input functions in turn.
321
+ *
322
+ * The array passed is cleared (length set to 0), so the array can be reused.
323
+ *
324
+ * The merged function is statically defined and does not contain a loop, to hopefully allow
325
+ * JS engine to heavily optimize it.
326
+ *
327
+ * `mergers` contains pre-defined functions to merge up to 5 visit functions.
328
+ * Merger functions for merging more than 5 visit functions are created dynamically on demand.
329
+ *
330
+ * @param visitFns - Array of visit functions
331
+ * @returns Function which calls all of `visitFns` in turn.
332
+ */
333
+ function mergeVisitFns(visitFns) {
334
+ const numVisitFns = visitFns.length;
335
+
336
+ // Get or create merger for merging `numVisitFns` functions
337
+ let merger;
338
+ if (mergers.length <= numVisitFns) {
339
+ while (mergers.length < numVisitFns) {
340
+ mergers.push(null);
341
+ }
342
+ merger = createMerger(numVisitFns);
343
+ mergers.push(merger);
344
+ } else {
345
+ merger = mergers[numVisitFns];
346
+ if (merger === null) merger = mergers[numVisitFns] = createMerger(numVisitFns);
347
+ }
348
+
349
+ // Merge functions
350
+ const mergedFn = merger(...visitFns);
351
+
352
+ // Empty `visitFns` array, so it can be reused
353
+ visitFns.length = 0;
354
+
355
+ return mergedFn;
356
+ }
357
+
358
+ /**
359
+ * Create a merger function that merges `fnCount` functions.
360
+ *
361
+ * @param fnCount - Number of functions to be merged
362
+ * @returns Function to merge `fnCount` functions
363
+ */
364
+ function createMerger(fnCount) {
365
+ const args = [];
366
+ let body = 'return node=>{';
367
+ for (let i = 1; i <= fnCount; i++) {
368
+ args.push(`visit${i}`);
369
+ body += `visit${i}(node);`;
370
+ }
371
+ body += '}';
372
+ args.push(body);
373
+ return new Function(...args);
374
+ }
375
+
376
+ // Pre-defined mergers for merging up to 5 functions
377
+ const mergers = [
378
+ null, // No merger for 0 functions
379
+ null, // No merger for 1 function
380
+ (visit1, visit2) => (node) => {
381
+ visit1(node);
382
+ visit2(node);
383
+ },
384
+ (visit1, visit2, visit3) => (node) => {
385
+ visit1(node);
386
+ visit2(node);
387
+ visit3(node);
388
+ },
389
+ (visit1, visit2, visit3, visit4) => (node) => {
390
+ visit1(node);
391
+ visit2(node);
392
+ visit3(node);
393
+ visit4(node);
394
+ },
395
+ (visit1, visit2, visit3, visit4, visit5) => (node) => {
396
+ visit1(node);
397
+ visit2(node);
398
+ visit3(node);
399
+ visit4(node);
400
+ visit5(node);
401
+ },
402
+ ];
@@ -1,6 +1,3 @@
1
- // Note: This code is repeated in `wrap.cjs`.
2
- // Any changes should be applied in that file too.
3
-
4
1
  export function wrap(result) {
5
2
  let program, module, comments, errors;
6
3
  return {
File without changes