Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ The declared exceptions are the guarded `performance.now()` behind `durationMs`,
If the method takes a callback, invoke it through `context.call` so it runs under the budget.
Never hand a script closure to a host implementation.
3. If the method needs no callback and cannot observe prototypes, it may delegate to the host implementation on a Maple-owned value (for example `Array.prototype.with`).
Call it through the prototype, as `Array.prototype.with.call(a, …)`, never as `a.with(…)`: a script can give its array an own property of that name.
Wrap it in `guardHostErrors` when the spec requires a `RangeError`, `URIError` or `SyntaxError` for it, or when it can build a string or array longer than the host allows.
4. Return only sandbox values: primitives, Maple-created objects and arrays, or wrappers.
Never return a native `Date`, `RegExp`, `Map`, `Set` or function.
Expand Down
4 changes: 3 additions & 1 deletion packages/maple/src/builtins/array/array-prototype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ import { ARRAY_METHOD_WITH } from './methods/with.ts';

/**
* Callbacks always run through the interpreter, so they stay under the budget and the host never
* holds a script function. Methods that mutate refuse read-only input arrays.
* holds a script function. Methods that mutate refuse read-only input arrays. A method that runs
* the host's implementation calls it through `Array.prototype`, never as `a.slice()`, since a
* script can give its array an own property of that name.
*/
export const ARRAY_PROTOTYPE: Prototype<Array<unknown>> = new Prototype(
'Array',
Expand Down
57 changes: 57 additions & 0 deletions packages/maple/src/builtins/array/array.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,63 @@ describe('array methods', () => {
).toEqual(['1-2', '1,2', '1,2', '1,2']);
});

it('call the host implementation even where the array has an own property of that name', () => {
expect(run('const a = [1, 2, 3]; a.at = 1; return [].at.call(a, -1);')).toBe(3);
expect(run('const a = [1]; a.concat = 1; return [].concat.call(a, [2], 3);')).toEqual([
1, 2, 3
]);
expect(
run('const a = [1, 2, 3]; a.copyWithin = 1; [].copyWithin.call(a, 0, 1); return [...a];')
).toEqual([2, 3, 3]);
expect(run('const a = ["x"]; a.entries = 1; return [...[].entries.call(a)];')).toEqual([
[0, 'x']
]);
expect(run('const a = [1, 2]; a.fill = 1; [].fill.call(a, 0); return [...a];')).toEqual([
0, 0
]);
expect(run('const a = [1, 2]; a.includes = 1; return [].includes.call(a, 2);')).toBe(true);
expect(run('const a = [1, 2]; a.indexOf = 1; return [].indexOf.call(a, 2);')).toBe(1);
expect(run('const a = ["x", "y"]; a.keys = 1; return [...[].keys.call(a)];')).toEqual([0, 1]);
expect(
run(
'const a = [1, 2, 1]; a.lastIndexOf = 1; return [[].lastIndexOf.call(a, 1), [].lastIndexOf.call(a, 1, 1)];'
)
).toEqual([2, 0]);
expect(run('const a = [1, 2]; a.pop = 1; return [[].pop.call(a), [...a]];')).toEqual([
2,
[1]
]);
expect(run('const a = [1]; a.push = 1; [].push.call(a, 2); return [...a];')).toEqual([1, 2]);
expect(run('const a = [1, 2]; a.reverse = 1; [].reverse.call(a); return [...a];')).toEqual([
2, 1
]);
expect(run('const a = [1, 2]; a.shift = 1; return [[].shift.call(a), [...a]];')).toEqual([
1,
[2]
]);
expect(run('const a = [3, 1, 2]; a.slice = 1; return [].slice.call(a, 1);')).toEqual([1, 2]);
expect(
run(
'const a = [1, 2, 3]; a.splice = 1; return [[].splice.call(a, 1), [].splice.call(a, 0, 1, 9), [...a]];'
)
).toEqual([[2, 3], [1], [9]]);
expect(run('const a = [1, 2]; a.map = 1; return [].toLocaleString.call(a);')).toBe('1,2');
expect(run('const a = [1, 2]; a.toReversed = 1; return [].toReversed.call(a);')).toEqual([
2, 1
]);
expect(run('const a = [2, 1]; a.toSorted = 1; return [].toSorted.call(a);')).toEqual([1, 2]);
expect(
run(
'const a = [1, 2, 3]; a.toSpliced = 1; return [[].toSpliced.call(a, 1), [].toSpliced.call(a, 0, 1, 9)];'
)
).toEqual([[1], [9, 2, 3]]);
expect(run('const a = [1]; a.unshift = 1; [].unshift.call(a, 0); return [...a];')).toEqual([
0, 1
]);
expect(run('const a = ["x"]; a.values = 1; return [...[].values.call(a)];')).toEqual(['x']);
expect(run('const a = [1, 2]; a.with = 1; return [].with.call(a, 0, 9);')).toEqual([9, 2]);
});

it('mutate script arrays', () => {
expect(run('const l = [1]; const n = l.push(2, 3); return [n, l];')).toEqual([3, [1, 2, 3]]);
expect(run('const l = [1, 2]; return [l.pop(), l];')).toEqual([2, [1]]);
Expand Down
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/at.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts';

export const ARRAY_METHOD_AT: Method<Array<unknown>> = method(
Array.prototype.at.length,
(a, [index]) => a.at(toNumber(index))
(a, [index]) => Array.prototype.at.call(a, toNumber(index))
);
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/concat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { guardHostErrors, method, type Method } from '../../../values/native.ts'

export const ARRAY_METHOD_CONCAT: Method<Array<unknown>> = method(
Array.prototype.concat.length,
(a, items, { fail }) => guardHostErrors(fail, () => a.concat(...items))
(a, items, { fail }) => guardHostErrors(fail, () => Array.prototype.concat.call(a, ...items))
);
4 changes: 3 additions & 1 deletion packages/maple/src/builtins/array/methods/copy-within.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const ARRAY_METHOD_COPY_WITHIN: Method<Array<unknown>> = method(
const to = toNumber(target);
const from = toNumber(start);
const until = optionalNumberArgument(end);
return guardIntegrityErrors(context.fail, a, () => a.copyWithin(to, from, until));
return guardIntegrityErrors(context.fail, a, () =>
Array.prototype.copyWithin.call(a, to, from, until)
);
}
);
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { IteratorWrapper } from '../../../values/wrappers/iterator-wrapper.ts';

export const ARRAY_METHOD_ENTRIES: Method<Array<unknown>> = method(
Array.prototype.entries.length,
(a) => new IteratorWrapper(a.entries())
(a) => new IteratorWrapper(Array.prototype.entries.call(a))
);
4 changes: 3 additions & 1 deletion packages/maple/src/builtins/array/methods/fill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const ARRAY_METHOD_FILL: Method<Array<unknown>> = method(
(a, [value, start, end], context) => {
const from = optionalNumberArgument(start);
const to = optionalNumberArgument(end);
return guardIntegrityErrors(context.fail, a, () => a.fill(value, from, to));
return guardIntegrityErrors(context.fail, a, () =>
Array.prototype.fill.call(a, value, from, to)
);
}
);
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/includes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts';

export const ARRAY_METHOD_INCLUDES: Method<Array<unknown>> = method(
Array.prototype.includes.length,
(a, [value, from]) => a.includes(value, optionalNumberArgument(from))
(a, [value, from]) => Array.prototype.includes.call(a, value, optionalNumberArgument(from))
);
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/index-of.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts';

export const ARRAY_METHOD_INDEX_OF: Method<Array<unknown>> = method(
Array.prototype.indexOf.length,
(a, [value, from]) => a.indexOf(value, optionalNumberArgument(from))
(a, [value, from]) => Array.prototype.indexOf.call(a, value, optionalNumberArgument(from))
);
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { IteratorWrapper } from '../../../values/wrappers/iterator-wrapper.ts';

export const ARRAY_METHOD_KEYS: Method<Array<unknown>> = method(
Array.prototype.keys.length,
(a) => new IteratorWrapper(a.keys())
(a) => new IteratorWrapper(Array.prototype.keys.call(a))
);
4 changes: 3 additions & 1 deletion packages/maple/src/builtins/array/methods/last-index-of.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ import { method, type Method } from '../../../values/native.ts';
export const ARRAY_METHOD_LAST_INDEX_OF: Method<Array<unknown>> = method(
Array.prototype.lastIndexOf.length,
(a, args) =>
args.length < 2 ? a.lastIndexOf(args[0]) : a.lastIndexOf(args[0], toNumber(args[1]))
args.length < 2
? Array.prototype.lastIndexOf.call(a, args[0])
: Array.prototype.lastIndexOf.call(a, args[0], toNumber(args[1]))
);
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/pop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts';

export const ARRAY_METHOD_POP: Method<Array<unknown>> = method(
Array.prototype.pop.length,
(a, _args, context) => guardIntegrityErrors(context.fail, a, () => a.pop())
(a, _args, context) => guardIntegrityErrors(context.fail, a, () => Array.prototype.pop.call(a))
);
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export const ARRAY_METHOD_PUSH: Method<Array<unknown>> = method(
Array.prototype.push.length,
(a, items, context) =>
guardHostErrors(context.fail, () =>
guardIntegrityErrors(context.fail, a, () => a.push(...items))
guardIntegrityErrors(context.fail, a, () => Array.prototype.push.call(a, ...items))
)
);
3 changes: 1 addition & 2 deletions packages/maple/src/builtins/array/methods/reverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@ export const ARRAY_METHOD_REVERSE: Method<Array<unknown>> = method(
Array.prototype.reverse.length,
(a, _args, context) =>
// The host's own reverse runs no script code and keeps holes where JavaScript puts them
// oxlint-disable-next-line unicorn/no-array-reverse - reversing in place is what this method does
guardIntegrityErrors(context.fail, a, () => a.reverse())
guardIntegrityErrors(context.fail, a, () => Array.prototype.reverse.call(a))
);
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/shift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts';

export const ARRAY_METHOD_SHIFT: Method<Array<unknown>> = method(
Array.prototype.shift.length,
(a, _args, context) => guardIntegrityErrors(context.fail, a, () => a.shift())
(a, _args, context) => guardIntegrityErrors(context.fail, a, () => Array.prototype.shift.call(a))
);
3 changes: 2 additions & 1 deletion packages/maple/src/builtins/array/methods/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { method, type Method } from '../../../values/native.ts';

export const ARRAY_METHOD_SLICE: Method<Array<unknown>> = method(
Array.prototype.slice.length,
(a, [start, end]) => a.slice(optionalNumberArgument(start), optionalNumberArgument(end))
(a, [start, end]) =>
Array.prototype.slice.call(a, optionalNumberArgument(start), optionalNumberArgument(end))
);
7 changes: 6 additions & 1 deletion packages/maple/src/builtins/array/methods/splice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import { guardIntegrityErrors } from '../../../values/guard-integrity-errors.ts'
import { method, type Method } from '../../../values/native.ts';
import { splice } from '../utils/splice.ts';

// `call` is typed by the last overload, which needs a delete count, so `splice(start)` gets its own
const spliceToEnd: (this: Array<unknown>, start: number) => Array<unknown> = Array.prototype.splice;

export const ARRAY_METHOD_SPLICE: Method<Array<unknown>> = method(
Array.prototype.splice.length,
(a, args, context) => {
return splice(a, args, (start, count, items) => {
return guardIntegrityErrors(context.fail, a, () =>
count === undefined ? a.splice(start) : a.splice(start, count, ...items)
count === undefined
? spliceToEnd.call(a, start)
: Array.prototype.splice.call(a, start, count, ...items)
);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const ARRAY_METHOD_TO_LOCALE_STRING: Method<Array<unknown>> = method(
(a, [locales, options], { fail }) => {
const tags = localesArgument(locales);
const intlOptions = intlOptionsArgument(options);
const mapped = a.map((element) => {
const mapped = Array.prototype.map.call(a, (element) => {
if (element === null || element === undefined) {
return '';
}
Expand Down
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/to-reversed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { method, type Method } from '../../../values/native.ts';

export const ARRAY_METHOD_TO_REVERSED: Method<Array<unknown>> = method(
Array.prototype.toReversed.length,
(a) => a.toReversed()
(a) => Array.prototype.toReversed.call(a)
);
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/to-sorted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { comparator } from '../utils/comparator.ts';

export const ARRAY_METHOD_TO_SORTED: Method<Array<unknown>> = method(
Array.prototype.toSorted.length,
(a, [fn], context) => a.toSorted(comparator(fn, context))
(a, [fn], context) => Array.prototype.toSorted.call(a, comparator(fn, context))
);
12 changes: 11 additions & 1 deletion packages/maple/src/builtins/array/methods/to-spliced.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { method, type Method } from '../../../values/native.ts';
import { splice } from '../utils/splice.ts';

// `call` is typed by the last overload, which takes no items, so the one with items gets its own
const toSplicedWithItems: (
this: ReadonlyArray<unknown>,
start: number,
skipCount: number,
...items: Array<unknown>
) => Array<unknown> = Array.prototype.toSpliced;

export const ARRAY_METHOD_TO_SPLICED: Method<Array<unknown>> = method(
Array.prototype.toSpliced.length,
(a, args) => {
return splice(a, args, (start, count, items) => {
return count === undefined ? a.toSpliced(start) : a.toSpliced(start, count, ...items);
return count === undefined
? Array.prototype.toSpliced.call(a, start)
: toSplicedWithItems.call(a, start, count, ...items);
});
}
);
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/unshift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export const ARRAY_METHOD_UNSHIFT: Method<Array<unknown>> = method(
Array.prototype.unshift.length,
(a, items, context) =>
guardHostErrors(context.fail, () =>
guardIntegrityErrors(context.fail, a, () => a.unshift(...items))
guardIntegrityErrors(context.fail, a, () => Array.prototype.unshift.call(a, ...items))
)
);
2 changes: 1 addition & 1 deletion packages/maple/src/builtins/array/methods/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { IteratorWrapper } from '../../../values/wrappers/iterator-wrapper.ts';

export const ARRAY_METHOD_VALUES: Method<Array<unknown>> = method(
Array.prototype.values.length,
(a) => new IteratorWrapper(a.values())
(a) => new IteratorWrapper(Array.prototype.values.call(a))
);
3 changes: 2 additions & 1 deletion packages/maple/src/builtins/array/methods/with.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { guardHostErrors, method, type Method } from '../../../values/native.ts'

export const ARRAY_METHOD_WITH: Method<Array<unknown>> = method(
Array.prototype.with.length,
(a, [index, value], { fail }) => guardHostErrors(fail, () => a.with(toNumber(index), value))
(a, [index, value], { fail }) =>
guardHostErrors(fail, () => Array.prototype.with.call(a, toNumber(index), value))
);
Loading