Skip to content

Commit dfdebc2

Browse files
committed
feat(napi/parser): lazy deserializer NodeArray slice method (oxc-project#11680)
Implement an optimized `slice` method for `NodeArray` in lazy-deserialized AST. It returns a another `NodeArray`, instead of a plain `Array`, and doesn't deserialize any of the contents.
1 parent 2749931 commit dfdebc2

File tree

2 files changed

+181
-8
lines changed

2 files changed

+181
-8
lines changed

napi/parser/raw-transfer/node-array.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ class NodeArray extends Array {
4646
}
4747

4848
// Allow `arr.filter`, `arr.map` etc.
49-
// TODO: Would be better for `slice` method to return a `NodeArray`.
5049
static [Symbol.species] = Array;
5150

5251
// Override `values` method with a more efficient one that avoids going via proxy for every iteration.
@@ -73,6 +72,46 @@ class NodeArray extends Array {
7372
return new NodeArrayEntriesIterator(arr.#internal, arr.length);
7473
}
7574

75+
// Override `slice` method to return a `NodeArray`.
76+
//
77+
// The new `NodeArray` references this `NodeArray` so element accesses on the slice will
78+
// read/write to this `NodeArray`.
79+
//
80+
// TODO: Is there any way to avoid the `WeakMap`? Seems necessary because `this` here is a proxy.
81+
slice(start, end) {
82+
// Get actual `NodeArray`. `this` is a proxy.
83+
const arr = nodeArrays.get(this);
84+
if (arr === void 0) throw new Error('`slice` called on a value which is not a `NodeArray`');
85+
86+
start = toInt(start);
87+
if (start < 0) {
88+
start = arr.length + start;
89+
if (start < 0) start = 0;
90+
}
91+
92+
if (end === void 0) {
93+
end = arr.length;
94+
} else {
95+
end = toInt(end);
96+
if (end < 0) {
97+
end = arr.length + end;
98+
if (end < 0) end = 0;
99+
} else if (end > arr.length) {
100+
end = arr.length;
101+
}
102+
}
103+
104+
let length = end - start;
105+
if (length <= 0 || start >= arr.length) {
106+
start = 0;
107+
length = 0;
108+
}
109+
110+
const internal = arr.#internal,
111+
{ stride } = internal;
112+
return new NodeArray(internal.pos + start * stride, length, stride, internal.construct, internal.ast);
113+
}
114+
76115
// Make `console.log` deserialize all elements.
77116
[Symbol.for('nodejs.util.inspect.custom')]() {
78117
const values = [...this.values()];
@@ -271,3 +310,17 @@ function isIndex(key) {
271310
}
272311

273312
const INDEX_REGEX = /^[1-9]\d*$/;
313+
314+
/**
315+
* Convert value to integer.
316+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#integer_conversion
317+
*
318+
* @param {*} value - Value to convert to integer.
319+
* @returns {number} - Integer
320+
*/
321+
function toInt(value) {
322+
value = Math.trunc(+value);
323+
// `value === 0` check is to convert -0 to 0
324+
if (value === 0 || Number.isNaN(value)) return 0;
325+
return value;
326+
}

napi/parser/test/lazy-deserialization.test.ts

Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ function parseSyncLazy(filename, code, options = null) {
1010
return parseSync(filename, code, { ...options, experimentalLazy: true });
1111
}
1212

13+
// Get `NodeArray` constructor
14+
const NodeArray = Object.getPrototypeOf(parseSyncLazy('test.js', '').program.body).constructor;
15+
expect(NodeArray).not.toBe(Array);
16+
expect(NodeArray.toString().startsWith('class NodeArray extends Array {')).toBe(true);
17+
1318
it('parses', () => {
1419
const { program } = parseSyncLazy('test.js', 'let x = y + z;');
1520
expect(program.type).toBe('Program');
@@ -252,13 +257,128 @@ describe('NodeArray', () => {
252257
expect(() => body.shift()).toThrow(new TypeError('Cannot redefine property: 0'));
253258
});
254259

255-
it('slice', () => {
256-
const { body } = parseSyncLazy('test.js', 'let x = 1; x = 2;').program;
257-
const slice = body.slice(1);
258-
expect(Array.isArray(slice)).toBe(true);
259-
expect(Object.getPrototypeOf(slice)).toBe(Array.prototype);
260-
expect(slice).toHaveLength(1);
261-
expect(slice[0]).toBe(body[1]);
260+
describe('slice', () => {
261+
class Case {
262+
constructor(args, expected) {
263+
this.args = args;
264+
this.expected = expected;
265+
}
266+
267+
toString() {
268+
return `(${this.args.map(arg => JSON.stringify(arg) || 'undefined').join(', ')})`;
269+
}
270+
}
271+
272+
it.each([
273+
// No args
274+
new Case([], { start: 0, length: 3 }),
275+
// 1 arg integer
276+
new Case([1], { start: 1, length: 2 }),
277+
new Case([2], { start: 2, length: 1 }),
278+
new Case([3], { start: 0, length: 0 }),
279+
new Case([4], { start: 0, length: 0 }),
280+
// 1 arg negative integer
281+
new Case([-0], { start: 0, length: 3 }),
282+
new Case([-1], { start: 2, length: 1 }),
283+
new Case([-2], { start: 1, length: 2 }),
284+
new Case([-3], { start: 0, length: 3 }),
285+
new Case([-4], { start: 0, length: 3 }),
286+
// 1 arg undefined/null
287+
new Case([undefined], { start: 0, length: 3 }),
288+
new Case([null], { start: 0, length: 3 }),
289+
// 1 arg non-integer
290+
new Case(['0'], { start: 0, length: 3 }),
291+
new Case(['2'], { start: 2, length: 1 }),
292+
new Case(['-2'], { start: 1, length: 2 }),
293+
new Case(['0x2'], { start: 2, length: 1 }),
294+
new Case(['oops'], { start: 0, length: 3 }),
295+
new Case([false], { start: 0, length: 3 }),
296+
new Case([true], { start: 1, length: 2 }),
297+
new Case([{ valueOf: () => 1 }], { start: 1, length: 2 }),
298+
// 2 args integers
299+
new Case([0, 0], { start: 0, length: 0 }),
300+
new Case([0, 1], { start: 0, length: 1 }),
301+
new Case([0, 2], { start: 0, length: 2 }),
302+
new Case([0, 3], { start: 0, length: 3 }),
303+
new Case([0, 4], { start: 0, length: 3 }),
304+
new Case([1, 2], { start: 1, length: 1 }),
305+
new Case([1, 3], { start: 1, length: 2 }),
306+
new Case([1, 4], { start: 1, length: 2 }),
307+
new Case([3, 3], { start: 0, length: 0 }),
308+
new Case([3, 5], { start: 0, length: 0 }),
309+
new Case([3, 0], { start: 0, length: 0 }),
310+
// 2 args negative integers
311+
new Case([-1, 3], { start: 2, length: 1 }),
312+
new Case([-2, 3], { start: 1, length: 2 }),
313+
new Case([-3, 3], { start: 0, length: 3 }),
314+
new Case([-4, 3], { start: 0, length: 3 }),
315+
new Case([-2, 5], { start: 1, length: 2 }),
316+
new Case([0, -1], { start: 0, length: 2 }),
317+
new Case([1, -1], { start: 1, length: 1 }),
318+
new Case([2, -1], { start: 0, length: 0 }),
319+
new Case([3, -1], { start: 0, length: 0 }),
320+
new Case([0, -0], { start: 0, length: 0 }),
321+
new Case([-2, -1], { start: 1, length: 1 }),
322+
new Case([-3, -1], { start: 0, length: 2 }),
323+
new Case([-3, -4], { start: 0, length: 0 }),
324+
// 2 args undefined/null
325+
new Case([undefined, undefined], { start: 0, length: 3 }),
326+
new Case([null, null], { start: 0, length: 0 }),
327+
new Case([null, undefined], { start: 0, length: 3 }),
328+
new Case([undefined, null], { start: 0, length: 0 }),
329+
new Case([undefined, 1], { start: 0, length: 1 }),
330+
new Case([undefined, 3], { start: 0, length: 3 }),
331+
new Case([undefined, 4], { start: 0, length: 3 }),
332+
new Case([undefined, -1], { start: 0, length: 2 }),
333+
new Case([undefined, 0], { start: 0, length: 0 }),
334+
new Case([null, 1], { start: 0, length: 1 }),
335+
new Case([null, 3], { start: 0, length: 3 }),
336+
new Case([null, 4], { start: 0, length: 3 }),
337+
new Case([null, -1], { start: 0, length: 2 }),
338+
new Case([null, 0], { start: 0, length: 0 }),
339+
new Case([0, undefined], { start: 0, length: 3 }),
340+
new Case([1, undefined], { start: 1, length: 2 }),
341+
new Case([2, undefined], { start: 2, length: 1 }),
342+
new Case([3, undefined], { start: 0, length: 0 }),
343+
new Case([4, undefined], { start: 0, length: 0 }),
344+
new Case([-2, undefined], { start: 1, length: 2 }),
345+
new Case([-3, undefined], { start: 0, length: 3 }),
346+
new Case([-5, undefined], { start: 0, length: 3 }),
347+
new Case([0, null], { start: 0, length: 0 }),
348+
new Case([1, null], { start: 0, length: 0 }),
349+
new Case([3, null], { start: 0, length: 0 }),
350+
new Case([-2, null], { start: 0, length: 0 }),
351+
new Case([-3, null], { start: 0, length: 0 }),
352+
new Case([-5, null], { start: 0, length: 0 }),
353+
// 2 args non-integers
354+
new Case(['0', '2'], { start: 0, length: 2 }),
355+
new Case(['0', '-2'], { start: 0, length: 1 }),
356+
new Case(['1', '-1'], { start: 1, length: 1 }),
357+
new Case(['0x1', '0x2'], { start: 1, length: 1 }),
358+
new Case([0, 'oops'], { start: 0, length: 0 }),
359+
new Case([false, true], { start: 0, length: 1 }),
360+
new Case([true, true], { start: 0, length: 0 }),
361+
new Case([1, { valueOf: () => 2 }], { start: 1, length: 1 }),
362+
])('%s', ({ args, expected }) => {
363+
const { body } = parseSyncLazy('test.js', 'let x = 1; x = 2; x = 3;').program;
364+
const slice = body.slice(...args);
365+
expect(Array.isArray(slice)).toBe(true);
366+
expect(Object.getPrototypeOf(slice)).toBe(NodeArray.prototype);
367+
expect(slice.length).toBe(expected.length);
368+
369+
for (let i = 0; i < expected.length; i++) {
370+
expect(slice[i]).toBe(body[i + expected.start]);
371+
}
372+
373+
// Check `Array.prototype.slice` behaves the same
374+
const arr = [11, 22, 33];
375+
const arrSlice = arr.slice(...args);
376+
expect(arrSlice.length).toBe(expected.length);
377+
378+
for (let i = 0; i < expected.length; i++) {
379+
expect(arrSlice[i]).toBe(arr[i + expected.start]);
380+
}
381+
});
262382
});
263383

264384
it('some', () => {

0 commit comments

Comments
 (0)