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
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ oxlint and oxfmt enforce most of the style. Fix what they report; don't silence

- Use kebab-case for file names.
- Use `#private` for state fields only, and spell out `public` / `protected` / `private` on all other members.
- Use [Conventional Commits](https://www.conventionalcommits.org) with a scope: the package, such as `maple`, or `*` for
the whole repository. The version and changelog come from them, so never edit `version` in
- Use [Conventional Commits](https://www.conventionalcommits.org) with a scope naming the area the commit changes, such
as `evaluator`, `json`, `playground` or `parity`, or `*` for the whole repository. Older commits name the package
(`maple`); new ones name the area. The version and changelog come from them, so never edit `version` in
`packages/maple/package.json` or `CHANGELOG.md` by hand.
- Don't add attribution to commits or pull requests: no `Co-Authored-By:` or other trailers, no "Generated with" lines
and no session links.
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ It runs `test` on Node 22, 24 and 26: the minimum from the library's `engines` f
## Commits & releases

Commits must follow [Conventional Commits](https://www.conventionalcommits.org), which commitlint enforces in the `commit-msg` hook.
Every commit needs a scope: the package it changes, such as `maple`, `playground` or `parity`, or `*` when it spans the repository.
Every commit needs a scope: the area it changes, such as `evaluator`, `boundary`, `builtins` or `json` in the library, `playground` or `parity`, or `*` when it spans the repository.
Older commits name the library's package, `maple`, instead; new ones name the area, so the changelog says what changed.

Pull requests target `increment`, the default branch, and are merged with a merge commit or rebased, never squashed, so their commits reach `master` as they are.
A release is a merge of `increment` into `master`, with a merge commit as well.
Expand Down
6 changes: 4 additions & 2 deletions packages/maple-parity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ js-interpreter differs from JavaScript here; Maple does what Node does.
| ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `with`, implicit globals, `arguments.callee` | Every script runs in strict mode, where these are errors. js-interpreter runs sloppy code and has no `callee`. |
| A method called without its receiver: `const p = [].push; p(1)`, `(0, a.push)(1)`, `arr.map("".trim)` | As in strict mode, a plain call has no receiver, so it is a `TypeError`. js-interpreter passes the global object instead. |
| `this` at the top level and in a plain call: `typeof this` is `'undefined'` | As in strict mode, there is no global object to stand in. js-interpreter in sloppy mode passes its global object. |
| `window`, `self`, `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` | Host APIs, not part of JavaScript. A run is synchronous and ends with its result, so nothing can be scheduled either. |
| `String(Math)`, `"" + JSON`: Maple gives `[object Math]`, js-interpreter `[object Object]` | A namespace converts with its own tag, as in JavaScript; js-interpreter tags every object as a plain one. |
| `Object.isExtensible(5)`: Maple gives `false`, js-interpreter `true` | A primitive is never extensible since ES2015; js-interpreter follows ES5. |
Expand All @@ -66,21 +67,22 @@ js-interpreter differs from JavaScript here; Maple does what Node does.
| An error named by an array that holds it: Maple gives `m: m` | `join` cuts the cycle at the array, as in JavaScript; js-interpreter gives `...: m`. |
| `f.bind(null, 1)`: Maple names it `bound f` with the rest of `f`'s length, js-interpreter `fBound`, 0 | A bound function's `name` and `length` are ES2015; js-interpreter follows ES5. |
| `f.apply(null, 1)`, `f.apply(null, 'ab')` | A primitive is no list of arguments, so it is a `TypeError`. js-interpreter ignores a number and spreads a string. |
| `JSON.parse(text, reviver)`: Maple applies the reviver, with the holder as `this` | As in JavaScript; js-interpreter ignores the reviver and returns the parsed value as it is. |

### Maple departs from JavaScript

On purpose: these are what the sandbox leaves out.

| Divergence | Why |
| ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `this`, `new` on a script function, `F.prototype`, `.constructor`, `instanceof F` | Maple has no script-visible prototypes or receivers. An object's behaviour comes from its type. |
| `new` on a script function, `F.prototype`, `.constructor`, `instanceof F` | Maple has no script-visible prototypes. An object's behaviour comes from its type. |
| `new` on a bound function: `new (Error.bind(null, 'x'))()` | Only the listed built-in constructors can be constructed, and a bound function is not one of them. |
| A built-in method called on a receiver of another type: `''.trim.call({})` | A method's behaviour comes from its receiver's type, so it refuses another type with a `TypeError`. |
| `Object.create`, `Object.getPrototypeOf`, `isPrototypeOf` | The same object model: there is no prototype chain to create or inspect. |
| Getters and setters | They would run script code on a plain property read or write, host conversion included. |
| `new String`, `new Number`, `new Boolean`, `Object(primitive)`, `instanceof String` / `Number` / `Boolean` | There are no primitive wrapper objects. They are a well-known trap and would add a value kind. |
| `eval`, the `Function` constructor and its members (`Function.toString`, `Function.valueOf`, `Function.prototype`) | Creating code from a string is the escape the sandbox exists to prevent, so the global is not reachable at all. |
| The global `this`, the global `constructor` | There is no global object: globals are read-only bindings, and the host decides what exists. |
| The global `constructor` | There is no global object: globals are read-only bindings, and the host decides what exists. |

### Not supported yet

Expand Down
74 changes: 74 additions & 0 deletions packages/maple-parity/src/cases/functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,80 @@ describe('method calls', () => {
});
});

describe('syntax:ThisExpression', () => {
it('is the object a function is called on', () => {
expect(parity('var o = { n: 1, f: function () { return this.n; } }; return o.f();')).toBe(1);
expect(parity('var o = { n: 1, f: function () { return this.n; } }; return o["f"]();')).toBe(1);
expect(parity('var o = { n: 1, f: function () { return this.n; } }; return (o.f)();')).toBe(1);
});

it('is undefined for a plain call', () => {
expect(parity('function f() { return typeof this; } return f();')).toBe('undefined');
expect(parity('var o = { f: function () { return typeof this; } }; return (0, o.f)();')).toBe(
'undefined'
);
expect(
parity('var o = { f: function () { return typeof this; } }; var g = o.f; return g();')
).toBe('undefined');
expect(parity('return typeof this;')).toBe('undefined');
});

it('is what call, apply and bind pass, unboxed', () => {
expect(
parity(
'function f() { return this; } return [f.call(5), f.apply("s", []), f.bind(true)(), f.call(null)];'
)
).toEqual([5, 's', true, null]);
expect(parity('function f() { return typeof this; } return f.call(5);')).toBe('number');
});

it('keeps the bound receiver', () => {
expect(parity('function f() { return this; } return f.bind(1).call(2);')).toBe(1);
expect(parity('function f() { return this; } var o = { g: f.bind(1) }; return o.g();')).toBe(1);
});

it('is undefined again in a nested function', () => {
expect(
parity(
'var o = { f: function () { function g() { return typeof this; } return g(); } }; return o.f();'
)
).toBe('undefined');
});

it('lets a method return its object for chaining', () => {
expect(
parity(
'var o = { n: 0, add: function (k) { this.n += k; return this; } }; return o.add(1).add(2).n;'
)
).toBe(3);
});

it('is read and written like any object', () => {
expect(
parity('var o = { f: function () { this.x = 1; return this.x; } }; return [o.f(), o.x];')
).toEqual([1, 1]);
expect(allThrow('function f() { return this.x; } return f();')).toBe('TypeError');
});
});

describe('thisArg of array callbacks', () => {
it('is this in the callback', () => {
expect(
parity(
'var t = { k: 10 }; var seen = []; var f = function (x) { seen.push(this.k + x); return true; }; [1].every(f, t); [1].filter(f, t); [1].forEach(f, t); [1].map(f, t); [1].some(f, t); return seen;'
)
).toEqual([11, 11, 11, 11, 11]);
});

it('passes a primitive unboxed', () => {
expect(parity('return [1].map(function () { return typeof this; }, 5);')).toEqual(['number']);
});

it('leaves this undefined when omitted', () => {
expect(parity('return [1].map(function () { return typeof this; });')).toEqual(['undefined']);
});
});

describe('assign:function property', () => {
it('adds a property to a script function', () => {
expect(
Expand Down
43 changes: 27 additions & 16 deletions packages/maple-parity/src/divergences.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,19 +248,38 @@ describe('Array.prototype.sort', () => {
});
});

// Maple departs from JavaScript on purpose.
describe('syntax:ThisExpression', () => {
// As in strict mode, there is no global object to stand in; sloppy js-interpreter passes its own
it('has no global this', () => {
expect(followsJavaScript('return typeof this;', { sloppy: true })).toEqual({
maple: 'undefined',
jsInterpreter: 'object'
});
});

// The object model: Maple has no script-visible prototypes or receivers. An object's behaviour
// comes from its type, so `this`, constructors and prototype chains are left out, not sandboxed.
it('is undefined in a plain call', () => {
expect(
followsJavaScript('function f() { return typeof this; } return f();', { sloppy: true })
).toEqual({ maple: 'undefined', jsInterpreter: 'object' });
});
});

describe('syntax:ThisExpression', () => {
it('rejects this in a function', () => {
expect(rejected('function f() { return this; } return f();')).toBe(
MapleErrorCode.DISALLOWED_SYNTAX
);
describe('JSON.parse', () => {
// js-interpreter ignores the reviver; Maple calls it with the holder as this
it('applies a reviver with the holder as this', () => {
expect(
followsJavaScript(
'return JSON.parse(\'{"a":1}\', function (k, v) { return k === "a" ? Object.keys(this) : v; });'
)
).toEqual({ maple: { a: ['a'] }, jsInterpreter: { a: 1 } });
});
});

// Maple departs from JavaScript on purpose.

// The object model: Maple has no script-visible prototypes. An object's behaviour comes from its
// type, so constructors and prototype chains are left out, not sandboxed.

describe('syntax:NewExpression', () => {
it('rejects new on a script function', () => {
expect(rejected('function Point(x) { this.x = x; } return new Point(1).x;')).toBe(
Expand Down Expand Up @@ -417,14 +436,6 @@ describe('Function.prototype.length', () => {

// There is no global object: globals are read-only bindings, and the host decides what exists.

describe('this', () => {
it('has no global this', () => {
expect(rejected('return typeof this;', { sloppy: true })).toBe(
MapleErrorCode.DISALLOWED_SYNTAX
);
});
});

describe('constructor', () => {
// js-interpreter's global object inherits from Object.prototype, so `constructor` is a global
it('has no global named constructor', () => {
Expand Down
12 changes: 10 additions & 2 deletions packages/maple/docs/builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ Instance: `length`, `at`, `concat`, `copyWithin`, `entries`, `every`, `fill`, `f

Static: `Array.isArray`, `Array.from`, `Array.of`.

`every`, `filter`, `find`, `findIndex`, `findLast`, `findLastIndex`, `flatMap`, `forEach`, `map`,
`some` and `Array.from` take a `thisArg` after the callback, which the callback sees as `this`.
The comparator of `sort` and `toSorted` and the reducer of `reduce` and `reduceRight` take none
and have `this` undefined, as in JavaScript.

## Number and Boolean

Number instance: `toFixed`, `toPrecision`, `toExponential`, `toLocaleString`, `toString`, `valueOf`.
Expand Down Expand Up @@ -79,8 +84,8 @@ All functions and constants of ES2023 `Math`: `abs`, `acos`, `acosh`, `asin`, `a

`JSON.parse(text, reviver?)` and `JSON.stringify(value, replacer?, space?)`, implemented by Maple
so that wrappers serialize the way their native counterparts would (a Date as its ISO string, a Map
as `{}`). Since there is no `this`, a reviver receives the holder as a third argument,
`(key, value, holder)`.
as `{}`). A reviver and a function replacer receive the holder as `this`, as in JavaScript; a
reviver does not receive the third `context` argument with the `source` text.

## RegExp

Expand Down Expand Up @@ -123,6 +128,9 @@ Set: `add`, `has`, `delete`, `clear`, `forEach`, `size`, `keys`, `values`, `entr
`intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`,
`isDisjointFrom`.

`forEach` of a Map and of a Set takes a `thisArg` after the callback, which the callback sees as
`this`.

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`.

Expand Down
5 changes: 2 additions & 3 deletions packages/maple/docs/divergences.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ one that does; the linked section has the detail.

## Syntax and names

- `this`, `class`, `super`, `async` / `await`, generators, getters and setters, tagged templates,
- `class`, `super`, `async` / `await`, generators, getters and setters, tagged templates,
`import` / `export`, `import.meta` and `new.target` throw `DISALLOWED_SYNTAX`
([Language](language.md#not-supported)).
- An `async` or generator function throws when it is created, not when it is called.
Expand Down Expand Up @@ -49,8 +49,7 @@ one that does; the linked section has the detail.

- Set algebra refuses set-like objects other than Sets and Maps, and `Iterator.from` refuses an
object with a script `next` ([Built-ins](builtins.md#map-and-set)).
- A JSON reviver receives the holder as a third argument instead of `this`
([JSON](builtins.md#json)).
- A JSON reviver receives no third `context` argument ([JSON](builtins.md#json)).
- The legacy `RegExp.$1` statics, the String HTML methods and `Intl` are not available.

## Errors and limits
Expand Down
6 changes: 5 additions & 1 deletion packages/maple/docs/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
A host function is any function found in the globals or in `data` that is not marked as an
intrinsic. A script can call it, store it and pass it around, but never reach the native function.
It has no properties, not even `length` or `name`: those are the host's business. Its arguments and
its return value are copied (see [Boundary](boundary.md#host-function-calls)).
its return value are copied (see [Boundary](boundary.md#host-function-calls)). It never receives a
`this`: called as a method, as in `obj.fn()`, or through `call`, `apply` or `bind`, it runs with
`this` undefined.

A host call costs what any call costs; the work and the time inside the host function are not
measured (see [Limits](limits.md)).
Expand Down Expand Up @@ -55,6 +57,8 @@ interface Sandbox {
program is one more call level and appears as `<nested>` in stack traces. Like any run, it neither
sees nor leaves properties the calling run added to shared built-ins. Data that is not an object
throws a plain `TypeError` to the intrinsic.
- An intrinsic receives no `this`, as a host function does, and `sandbox.call` calls a script
function with `this` undefined.
- An exception the intrinsic throws is treated like a host function's.
- The handle is usable only while the intrinsic runs. Once it has returned, every method throws a
plain `Error`, so a handle or closure an intrinsic keeps cannot run script code outside the call.
Expand Down
21 changes: 13 additions & 8 deletions packages/maple/docs/language.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ Expressions:

## Not supported

Reaching any of these throws `DISALLOWED_SYNTAX`: `this`, `class`, `super`, `async` / `await`,
generators and `yield`, getters and setters, tagged templates, `import` / `export`, `import.meta`,
`new.target`, and `arguments` at the script level, which is no function.
Reaching any of these throws `DISALLOWED_SYNTAX`: `class`, `super`, `async` / `await`, generators
and `yield`, getters and setters, tagged templates, `import` / `export`, `import.meta`, `new.target`,
and `arguments` at the script level, which is no function.

An `async` or generator function throws when the closure is created, not when it is called. For a
function declaration that is hoisting time, so the script fails before its first statement runs.
Expand Down Expand Up @@ -72,11 +72,16 @@ Script functions and built-in functions have JavaScript's read-only `length` and
inferred from bindings included. Host functions, and functions bound from them, have no properties
at all. Every function answers to `toString`, which gives `[function]`, `valueOf`, `call`, `apply`
and `bind` ([Functions](builtins.md#functions)). The receiver these pass on reaches a built-in
method, which checks it as for any method call, so `''.trim.call(' b ')` is `'b'`. A script
function never sees it, since there is no `this`, which is how JavaScript behaves for a strict
function that does not use `this`.

Inside a function that is not an arrow, `arguments` is the strict-mode arguments object.
method, which checks it as for any method call, so `''.trim.call(' b ')` is `'b'`, and a script
function as its `this`.

Inside a function that is not an arrow, `this` and `arguments` are those of a strict-mode function.
`this` is the object of a method call, so `obj.f()` and `obj[key]()` see `obj`, and `undefined` for
a plain call such as `f()` or `(0, obj.f)()`. `call`, `apply` and `bind` pass their receiver as it is,
never boxed, so `f.call(5)` sees `5`, and a bound function keeps its receiver whatever it is called
on. An arrow function has no `this` of its own and sees the one of the function it is written in.
At the top level of the script `this` is `undefined`, since there is no global object. A script
function still cannot be constructed ([Constructors](#constructors)).

Every function, and every namespace such as `Math`, takes the properties a script adds, as in
`fn.cache = {}`. Built-ins, host functions and namespaces are shared by every run, so what a run
Expand Down
Loading
Loading