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 packages/maple/docs/boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ globals), the arguments of a host function, its return value, and the result of
| Sandbox error | – | plain object | – | plain object |
| Function | host function | `undefined` | `INTERNAL_ERROR` | `undefined` |
| Iterator | – | `TypeError` | – | `undefined` |
| `WeakMap`, `WeakSet` | `TypeError` before | `TypeError` | `INTERNAL_ERROR` | `undefined` |
| Anything else | `TypeError` before | – | `INTERNAL_ERROR` | – |
| Value reached twice | shared | shared | shared | shared |
| Cycle | kept | `TypeError` | kept | kept |
Expand Down
12 changes: 11 additions & 1 deletion packages/maple/docs/builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ data key of the same name shadows them:
`undefined`, `NaN`, `Infinity`, `parseInt`, `parseFloat`, `isNaN`, `isFinite`,
`encodeURIComponent`, `decodeURIComponent`, `encodeURI`, `decodeURI`, `escape`, `unescape`,
`String`, `Number`, `Boolean`, `Array`, `Object`, `Math`, `JSON`, `Date`, `RegExp`, `Map`, `Set`,
`Iterator`, `Error`, `TypeError`, `RangeError`, `ReferenceError`, `SyntaxError`, `URIError`,
`WeakMap`, `WeakSet`, `Iterator`, `Error`, `TypeError`, `RangeError`, `ReferenceError`, `SyntaxError`, `URIError`,
`EvalError`, `atob`, `btoa`.

`atob` and `btoa` are web APIs rather than ECMAScript, provided because Node, browsers and workers
Expand Down Expand Up @@ -134,6 +134,16 @@ Set: `add`, `has`, `delete`, `clear`, `forEach`, `size`, `keys`, `values`, `entr
The set algebra takes a Set or a Map as the other operand. JavaScript's other set-like objects are
refused with a `TypeError`, so the host never calls a script `has` or `keys`.

## WeakMap and WeakSet

WeakMap: `get`, `set`, `has`, `delete`.

WeakSet: `add`, `has`, `delete`.

Any object can be a key or an entry, functions and built-ins included. Scripts have no symbols, so
nothing else can. A WeakMap or WeakSet never comes in with the input and leaves a run as
`undefined` ([Boundary](boundary.md)).

## Iterators

`keys`, `values` and `entries` of arrays, Maps and Sets, and `matchAll`, return iterators with
Expand Down
32 changes: 16 additions & 16 deletions packages/maple/docs/language.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function declaration that is hoisting time, so the script fails before its first

Reading one of these names also throws `DISALLOWED_SYNTAX`, unless the script or the host binds it:

- Language: `Proxy`, `Reflect`, `Symbol`, `WeakMap`, `WeakSet`, `WeakRef`, `FinalizationRegistry`,
- Language: `Proxy`, `Reflect`, `Symbol`, `WeakRef`, `FinalizationRegistry`,
`Promise`, `SharedArrayBuffer`, `Atomics`, `WebAssembly`, `Intl`, `globalThis`, `eval`, `Function`
- Server runtimes: `require`, `module`, `exports`, `process`, `global`, `Buffer`, `__dirname`,
`__filename`, `Deno`, `Bun`
Expand All @@ -58,12 +58,12 @@ name. `typeof` never throws for a name that is not bound, so `typeof window !==
## Constructors

`new` is allowed for `Error`, `TypeError`, `RangeError`, `ReferenceError`, `SyntaxError`,
`URIError`, `EvalError`, `Date`, `RegExp`, `Map`, `Set`, `Array` and `Object`. Any other `new`
target, including a script function or a bound function, is `DISALLOWED_SYNTAX`. There are no
primitive wrapper objects: `String`, `Number` and `Boolean` are conversion functions only, so
`new String('a')` is rejected, and `Object('a')` is a `TypeError`.
`URIError`, `EvalError`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, `Array` and
`Object`. Any other `new` target, including a script function or a bound function, is
`DISALLOWED_SYNTAX`. There are no primitive wrapper objects: `String`, `Number` and `Boolean` are
conversion functions only, so `new String('a')` is rejected, and `Object('a')` is a `TypeError`.

`instanceof` accepts those thirteen plus `Iterator` on the right-hand side. Any other right-hand
`instanceof` accepts those fifteen plus `Iterator` on the right-hand side. Any other right-hand
side, a script function, a bound function or `String` included, throws a `TypeError`.

## Functions
Expand Down Expand Up @@ -98,16 +98,16 @@ arrays of its own, even for the same source.

Inside the sandbox exactly these values exist:

| Kind | Representation | `typeof` |
| -------------- | ------------------------------------------------------------------------- | ------------ |
| Primitives | Host primitives: `undefined`, `null`, booleans, numbers, strings | as in JS |
| Plain objects | Host objects created by Maple or by the input copy; own properties only | `'object'` |
| Arrays | Host arrays created by Maple or by the input copy | `'object'` |
| Closures | Maple's own function objects | `'function'` |
| Host functions | Branded wrappers; the native function is not reachable | `'function'` |
| Intrinsics | Branded wrappers, same surface as host functions | `'function'` |
| Wrappers | Branded wrappers around a native `RegExp`, `Date`, `Map`, `Set`, iterator | `'object'` |
| Errors | Maple's own error objects with `name` and `message` | `'object'` |
| Kind | Representation | `typeof` |
| -------------- | ----------------------------------------------------------------------------------------------- | ------------ |
| Primitives | Host primitives: `undefined`, `null`, booleans, numbers, strings | as in JS |
| Plain objects | Host objects created by Maple or by the input copy; own properties only | `'object'` |
| Arrays | Host arrays created by Maple or by the input copy | `'object'` |
| Closures | Maple's own function objects | `'function'` |
| Host functions | Branded wrappers; the native function is not reachable | `'function'` |
| Intrinsics | Branded wrappers, same surface as host functions | `'function'` |
| Wrappers | Branded wrappers around a native `RegExp`, `Date`, `Map`, `Set`, `WeakMap`, `WeakSet`, iterator | `'object'` |
| Errors | Maple's own error objects with `name` and `message` | `'object'` |

Wrappers are instances of Maple's own classes, which a script cannot construct, so a plain object
can never pose as one. The native value inside is never exposed; every method goes through a
Expand Down
6 changes: 6 additions & 0 deletions packages/maple/src/boundary/from-host.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ describe('fromHost', () => {
expect(() => fromHost([1, Symbol('s')], 'data')).toThrow(
new TypeError('Unsupported value at data[1]: symbol')
);
expect(() => fromHost({ cache: new WeakMap() }, 'data')).toThrow(
new TypeError('Unsupported value at data.cache: WeakMap')
);
expect(() => fromHost([new WeakSet()], 'data')).toThrow(
new TypeError('Unsupported value at data[0]: WeakSet')
);
expect(() => fromHost(new Uint8Array(1), 'data')).toThrow(
new TypeError('Unsupported value at data: Uint8Array')
);
Expand Down
15 changes: 15 additions & 0 deletions packages/maple/src/boundary/to-host.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { IteratorWrapper } from '../values/wrappers/iterator-wrapper.ts';
import { MapWrapper } from '../values/wrappers/map-wrapper.ts';
import { RegExpWrapper } from '../values/wrappers/regexp-wrapper.ts';
import { SetWrapper } from '../values/wrappers/set-wrapper.ts';
import { WeakMapWrapper } from '../values/wrappers/weak-map-wrapper.ts';
import { WeakSetWrapper } from '../values/wrappers/weak-set-wrapper.ts';
import { fromHost } from './from-host.ts';
import { toHost, UnconvertibleValueError } from './to-host.ts';

Expand Down Expand Up @@ -154,6 +156,19 @@ describe('toHost', () => {
expect(toHost(new IteratorWrapper([1].values()))).toBeUndefined();
});

it('drops WeakMaps and WeakSets, and refuses them when strict', () => {
expect(toHost({ map: new WeakMapWrapper(), set: new WeakSetWrapper() })).toStrictEqual({
map: undefined,
set: undefined
});
expect(() => toHost(new WeakMapWrapper(), true)).toThrow(
new UnconvertibleValueError('WeakMap')
);
expect(() => toHost(new WeakSetWrapper(), true)).toThrow(
new UnconvertibleValueError('WeakSet')
);
});

it('refuses values that cannot exist in the sandbox', () => {
expect(() => toHost(new Date(0))).toThrow(Error);
});
Expand Down
17 changes: 13 additions & 4 deletions packages/maple/src/boundary/to-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { IteratorWrapper } from '../values/wrappers/iterator-wrapper.ts';
import { MapWrapper } from '../values/wrappers/map-wrapper.ts';
import { RegExpWrapper } from '../values/wrappers/regexp-wrapper.ts';
import { SetWrapper } from '../values/wrappers/set-wrapper.ts';
import { WeakMapWrapper } from '../values/wrappers/weak-map-wrapper.ts';
import { WeakSetWrapper } from '../values/wrappers/weak-set-wrapper.ts';
import type { SandboxErrorInfo } from './maple-error.ts';

/**
Expand All @@ -18,7 +20,7 @@ import type { SandboxErrorInfo } from './maple-error.ts';
*/
export const MAX_ERROR_PROPERTIES_SIZE = 65_536;

type UnconvertibleKind = 'iterator' | 'cyclic value';
type UnconvertibleKind = 'iterator' | 'WeakMap' | 'WeakSet' | 'cyclic value';

/**
* Keys a copy leaves out. Host code that copies by assignment would turn an own `__proto__` into
Expand Down Expand Up @@ -49,9 +51,10 @@ export class NestingLimitError extends RangeError {

/**
* Copies a sandbox value out for the host: a fresh, mutable structure that shares nothing with the
* run and contains nothing executable. Functions become `undefined`. Iterators become `undefined`
* and cycles are preserved, or, when `strict` as for host function arguments, both throw an
* `UnconvertibleValueError`. Throws a plain error for a value the sandbox could not have produced.
* run and contains nothing executable. Functions become `undefined`. Iterators, WeakMaps and
* WeakSets become `undefined` and cycles are preserved, or, when `strict` as for host function
* arguments, all four throw an `UnconvertibleValueError`. Throws a plain error for a value the
* sandbox could not have produced.
*/
export function toHost(value: unknown, strict: boolean = false): unknown {
return convert(value, { copies: new Map(), open: new Set(), strict, acyclic: false }, 0);
Expand Down Expand Up @@ -116,6 +119,12 @@ function convert(value: unknown, walk: Walk, depth: number): unknown {
}
return undefined;
}
if (value instanceof WeakMapWrapper || value instanceof WeakSetWrapper) {
if (walk.strict) {
throw new UnconvertibleValueError(value.tag);
}
return undefined;
}

if (walk.open.has(value)) {
if (walk.strict) {
Expand Down
12 changes: 12 additions & 0 deletions packages/maple/src/builtins/map/map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ describe('Map', () => {
expect(run(`${setup} const [[k]] = m; return k;`)).toBe('b');
});

it('reads each entry like entry[0] and entry[1], so any object is a pair', () => {
expect(run('return new Map([{ 0: "a", 1: 1 }]).get("a");')).toBe(1);
expect(run('return new Map([{}]).get(undefined);')).toBeUndefined();
expect(run('return new Map([{}]).has(undefined);')).toBe(true);
expect(
run('const pair = new Set(); pair[0] = "k"; pair[1] = 7; return new Map([pair]).get("k");')
).toBe(7);
expect(run('try { new Map([1]); } catch (e) { return e.message; }')).toBe(
'Iterator value 1 is not an entry object'
);
});

it('forEach calls its callback with the thisArg as this', () => {
expect(
run(
Expand Down
8 changes: 5 additions & 3 deletions packages/maple/src/builtins/map/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { Builtin } from '../../values/builtin.ts';
import { describeValue } from '../../values/coercion.ts';
import { iterate } from '../../values/iteration.ts';
import { MapWrapper } from '../../values/wrappers/map-wrapper.ts';
import { getMember } from '../method-dispatch.ts';
import { MAP_PROTOTYPE } from './map-prototype.ts';
import { MAP_STATICS } from './map-statics.ts';

export const MAP_BUILTIN: Builtin = new Builtin({
name: 'Map',
length: Map.length,
call: (_args, { fail }) => fail('TypeError', "Constructor Map requires 'new'"),
// `new Map(iterable?)`; each entry must be an array of key and value
// `new Map(iterable?)`; each entry is read like `entry[0]` and `entry[1]`, so any object works
// as a pair
construct: ([iterable], context) => {
const map = new Map<unknown, unknown>();
if (iterable === undefined || iterable === null) {
Expand All @@ -23,13 +25,13 @@ export const MAP_BUILTIN: Builtin = new Builtin({

for (let step = iterator.next(); !step.done; step = iterator.next()) {
const entry: unknown = step.value;
if (!Array.isArray(entry)) {
if (typeof entry !== 'object' || entry === null) {
return context.fail(
'TypeError',
`Iterator value ${describeValue(entry)} is not an entry object`
);
}
map.set(entry[0], entry[1]);
map.set(getMember(entry, '0', context), getMember(entry, '1', context));
}

return new MapWrapper(map);
Expand Down
4 changes: 4 additions & 0 deletions packages/maple/src/builtins/method-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { OBJECT_PROTOTYPE } from './object/object-prototype.ts';
import { REGEXP_PROTOTYPE } from './regexp/regexp-prototype.ts';
import { SET_PROTOTYPE } from './set/set-prototype.ts';
import { STRING_PROTOTYPE } from './string/string-prototype.ts';
import { WEAK_MAP_PROTOTYPE } from './weak-map/weak-map-prototype.ts';
import { WEAK_SET_PROTOTYPE } from './weak-set/weak-set-prototype.ts';

/**
* Every prototype a value can have, tried in order, so arrays find theirs before plain objects
Expand All @@ -28,6 +30,8 @@ const PROTOTYPES: ReadonlyArray<Prototype> = [
DATE_PROTOTYPE,
MAP_PROTOTYPE,
SET_PROTOTYPE,
WEAK_MAP_PROTOTYPE,
WEAK_SET_PROTOTYPE,
ERROR_PROTOTYPE,
ITERATOR_PROTOTYPE,
FUNCTION_PROTOTYPE
Expand Down
4 changes: 4 additions & 0 deletions packages/maple/src/builtins/standard-globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { OBJECT_BUILTIN } from './object/object.ts';
import { REGEXP_BUILTIN } from './regexp/regexp.ts';
import { SET_BUILTIN } from './set/set.ts';
import { STRING_BUILTIN } from './string/string.ts';
import { WEAK_MAP_BUILTIN } from './weak-map/weak-map.ts';
import { WEAK_SET_BUILTIN } from './weak-set/weak-set.ts';

// The library is typechecked without host libs; these web APIs exist in Node, browsers and workers
declare const atob: (data: string) => string;
Expand Down Expand Up @@ -67,6 +69,8 @@ export const STANDARD_GLOBALS: Readonly<Record<string, unknown>> = Object.freeze
RegExp: REGEXP_BUILTIN,
Map: MAP_BUILTIN,
Set: SET_BUILTIN,
WeakMap: WEAK_MAP_BUILTIN,
WeakSet: WEAK_SET_BUILTIN,
Iterator: ITERATOR_BUILTIN
});

Expand Down
8 changes: 8 additions & 0 deletions packages/maple/src/builtins/weak-map/methods/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { canBeHeldWeakly } from '../../../support/can-be-held-weakly.ts';
import { method, type Method } from '../../../values/native.ts';
import type { WeakMapWrapper } from '../../../values/wrappers/weak-map-wrapper.ts';

export const WEAK_MAP_METHOD_DELETE: Method<WeakMapWrapper> = method(
WeakMap.prototype.delete.length,
(m, [key]) => canBeHeldWeakly(key) && m.map.delete(key)
);
8 changes: 8 additions & 0 deletions packages/maple/src/builtins/weak-map/methods/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { canBeHeldWeakly } from '../../../support/can-be-held-weakly.ts';
import { method, type Method } from '../../../values/native.ts';
import type { WeakMapWrapper } from '../../../values/wrappers/weak-map-wrapper.ts';

export const WEAK_MAP_METHOD_GET: Method<WeakMapWrapper> = method(
WeakMap.prototype.get.length,
(m, [key]) => (canBeHeldWeakly(key) ? m.map.get(key) : undefined)
);
8 changes: 8 additions & 0 deletions packages/maple/src/builtins/weak-map/methods/has.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { canBeHeldWeakly } from '../../../support/can-be-held-weakly.ts';
import { method, type Method } from '../../../values/native.ts';
import type { WeakMapWrapper } from '../../../values/wrappers/weak-map-wrapper.ts';

export const WEAK_MAP_METHOD_HAS: Method<WeakMapWrapper> = method(
WeakMap.prototype.has.length,
(m, [key]) => canBeHeldWeakly(key) && m.map.has(key)
);
17 changes: 17 additions & 0 deletions packages/maple/src/builtins/weak-map/methods/set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { canBeHeldWeakly } from '../../../support/can-be-held-weakly.ts';
import { method, type Method } from '../../../values/native.ts';
import type { WeakMapWrapper } from '../../../values/wrappers/weak-map-wrapper.ts';

export const INVALID_WEAK_MAP_KEY = 'Invalid value used as weak map key';

/** A primitive key throws, as in JavaScript: `new WeakMap().set('a', 1)`. */
export const WEAK_MAP_METHOD_SET: Method<WeakMapWrapper> = method(
WeakMap.prototype.set.length,
(m, [key, value], { fail }) => {
if (!canBeHeldWeakly(key)) {
return fail('TypeError', INVALID_WEAK_MAP_KEY);
}
m.map.set(key, value);
return m;
}
);
21 changes: 21 additions & 0 deletions packages/maple/src/builtins/weak-map/weak-map-prototype.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Prototype } from '../../values/prototype.ts';
import { WeakMapWrapper } from '../../values/wrappers/weak-map-wrapper.ts';
import { WEAK_MAP_METHOD_DELETE } from './methods/delete.ts';
import { WEAK_MAP_METHOD_GET } from './methods/get.ts';
import { WEAK_MAP_METHOD_HAS } from './methods/has.ts';
import { WEAK_MAP_METHOD_SET } from './methods/set.ts';

/**
* Keys are compared by identity in the host `WeakMap`, so `wm.get({})` never finds an entry stored
* under another `{}`. Nothing lists the entries, so a script cannot tell whether one was collected.
*/
export const WEAK_MAP_PROTOTYPE: Prototype<WeakMapWrapper> = new Prototype(
'WeakMap',
(value): value is WeakMapWrapper => value instanceof WeakMapWrapper,
{
delete: WEAK_MAP_METHOD_DELETE,
get: WEAK_MAP_METHOD_GET,
has: WEAK_MAP_METHOD_HAS,
set: WEAK_MAP_METHOD_SET
}
);
Loading
Loading