Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e0c82ad
refactor(evaluator): move error descriptions of nodes into describe.ts
NetWin Oct 23, 2026
b13e606
refactor(evaluator): move budget, frames and error raising into RunCo…
NetWin Oct 23, 2026
ccd7479
refactor(evaluator): move host and intrinsic calls into HostBridge
NetWin Oct 23, 2026
2980f8c
refactor(evaluator): bind patterns and resolve references through an …
NetWin Oct 23, 2026
b2b48c2
refactor(evaluator): split statements into blocks, loops and control …
NetWin Oct 23, 2026
9a18edb
refactor(evaluator): split expressions into member access, literals, …
NetWin Oct 23, 2026
9a2e29d
refactor(evaluator): move calls and closures into calls.ts
NetWin Oct 23, 2026
a3d21bc
docs(evaluator): describe the evaluator layout for contributors
NetWin Oct 23, 2026
c303875
refactor(evaluator): move the language files into evaluator/language
NetWin Oct 23, 2026
7618a03
Merge pull request #18 from NetWin/refactor/evaluator-language
NetWin Oct 23, 2026
0dd3209
ci(release): don't release commits scoped to the playground
NetWin Oct 23, 2026
4fa2106
feat(playground): run scripts against a host context module
NetWin Oct 24, 2026
08ee395
feat(playground): explain Maple with a welcome dialog and examples
NetWin Oct 24, 2026
ff678d0
ci(playground): deploy the playground to GitHub Pages
NetWin Oct 24, 2026
e598949
docs(playground): stop calling the published playground private
NetWin Oct 24, 2026
2864f12
Merge pull request #19 from NetWin/feature/playground-showcase
NetWin Oct 24, 2026
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
29 changes: 29 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,35 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Publishes the playground to https://netwin.github.io/maple/. It runs after the release
pages:
name: Deploy playground
needs: [release]
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
permissions:
contents: read
pages: write # upload the site
id-token: write # prove the deployment comes from this workflow
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v7
with:
ref: master
- uses: pnpm/setup@v3
with:
cache: true
require-lockfile: true
# Pages serves the site below /maple/, so every URL the app builds must start there
- run: pnpm --filter @netwin/maple-playground build --base-href /maple/
- uses: actions/upload-pages-artifact@v5
with:
path: apps/maple-playground/dist/maple-playground/browser
- id: deployment
uses: actions/deploy-pages@v5

# Brings the release commit and tag back to increment, where pull requests branch off, so their
# changelog preview starts from the latest release
sync:
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ oxlint and oxfmt enforce most of the style. Fix what they report; don't silence
- Use the `.ts` extension in relative imports (`./scope.ts`).
- Put tests next to the code as `*.spec.ts` (Vitest). Every interpreter feature and every rejected escape needs a test.
Specs that exercise a whole language chapter through `Maple.run` live in the directory whose behaviour they cover.
- `docs/` is the contract, indexed in `docs/README.md`; change it in the same commit as the behaviour. Contributor
notes such as adding a built-in are in the root `CONTRIBUTING.md`.
- `docs/` is the contract, indexed in `docs/README.md`; change it in the same commit as the behaviour.
- Contributor notes are in the root `CONTRIBUTING.md`: read "Adding a built-in" before adding one, and "Evaluator
layout" before changing anything in `src/evaluator`.

## Playground (`apps/maple-playground`)

Expand Down
38 changes: 38 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,44 @@ The library compiles against `lib: ["ES2024", "ES2025.Collection", "ES2025.Itera
It has neither DOM nor Node type definitions, so a host API cannot be used by accident.
The declared exceptions are the guarded `performance.now()` behind `durationMs`, and `atob` and `btoa`.

### Evaluator layout

The evaluator in `packages/maple/src/evaluator` runs a script by walking the syntax tree acorn parsed from it.
`Evaluator` in `evaluator.ts` is only a switchboard: `evaluateStatement` and `evaluateExpression` charge an operation for each node and hand it, by its node type, to the file in `language/` that implements that part of JavaScript.

| `language/` | JavaScript it implements |
| ------------------ | ------------------------------------------------------------------------ |
| `hoisting.ts` | which names a block or function declares, before its statements run |
| `blocks.ts` | statement lists, hoisting, `var` / `let` / `const` |
| `loops.ts` | `while`, `do…while`, `for`, `for…of`, `for…in` |
| `control-flow.ts` | `if`, `return`, `break`, `continue`, labels, `switch`, `throw`, `try` |
| `literals.ts` | literals, template literals, object and array literals, spread |
| `member-access.ts` | `a.b`, `a[b]`, optional chains, `delete` |
| `references.ts` | reading and assigning variables |
| `patterns.ts` | destructuring into names, as in declarations, parameters and assignments |
| `operators.ts` | unary, binary, logical, conditional and comma operators |
| `assignments.ts` | `=`, compound and logical assignment, `++` / `--` |
| `closures.ts` | creating function values from `function` and arrow syntax |
| `calls.ts` | calls, `new`, tagged templates, running a script function |

Next to `language/` is what every part of JavaScript runs on:

- `run-context.ts`: the run's budget, call stack and run state, and how errors are raised: a script error the script can catch, or a `MapleError` that ends the run.
- `host-bridge.ts`: every call into host functions and intrinsics, which is where values are copied and host exceptions are hidden from the script.
- `scope.ts`, `completion.ts`, `budget.ts`, `describe.ts`: scopes, completions (how a `return`, `break` or `continue` travels up), the operation budget, and the text error messages quote from the script.

Three rules keep the language files apart, so a function can be understood from its own file:

1. A language file is a set of plain functions that take the `Interpreter` (see `interpreter.ts`) as their first argument.
Whenever it evaluates a piece of syntax it does not handle itself, such as the default in `{ size = 10 }`, it calls the interpreter, which dispatches it.
2. A language file may call plain helpers of the language files below it, as `patterns.ts` uses `resolveMember` from `references.ts`, but never of one above, so the imports form no cycle.
3. A function that takes only a `RunContext`, not the interpreter, cannot evaluate anything.

Every language file starts with a comment that explains its part of JavaScript with examples, written for a reader who does not know Maple.

To support a new piece of syntax, add its case to the dispatcher in `evaluator.ts`, implement it as a function in the language file it belongs to, test it through `Maple.run` in the spec in `language/` for that part of the language, such as `control-flow.spec.ts` or `destructuring.spec.ts`, and list it in [language.md](packages/maple/docs/language.md).
Hot paths are measured with `pnpm --filter @netwin/maple-playground measure-run <script.js>`; compare before and after a change to the evaluator.

### Adding a built-in

1. Register the method under its name in the prototype of the receiver type as `method(String.prototype.name.length, (receiver, args, context) => value)`, or a static as `new NativeFunction('isArray', Array.isArray.length, (args, context) => value)` in the `members` of the namespace.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

Maple is a secure JavaScript interpreter written in TypeScript.
It interprets a script itself instead of handing it to `eval`, so the script only ever sees the values you pass in.
Try it in the [playground](https://netwin.github.io/maple/), which runs Maple in your browser.

Installation, usage and the API are in the [library's README](packages/maple/README.md), and the full contract is in its [docs](packages/maple/docs/README.md).

Expand All @@ -17,7 +18,7 @@ This is a pnpm workspace:
| Path | Package | Description |
| ----------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `packages/maple` | [`@netwin/maple`](packages/maple) | The published library (ESM, Node >= 22, peer dep: `acorn`) |
| `apps/maple-playground` | [`@netwin/maple-playground`](apps/maple-playground) | Private Angular app for trying out the interpreter |
| `apps/maple-playground` | [`@netwin/maple-playground`](apps/maple-playground) | Angular app for trying out the interpreter, [live on GitHub Pages](https://netwin.github.io/maple/) |
| `packages/maple-parity` | [`@netwin/maple-parity`](packages/maple-parity) | Private parity tests: Maple adheres to JS standards and (for internal reasons) documents the parity with [JS-Interpreter](https://github.com/NetWin/js-interpreter) |

## Contributing
Expand Down
44 changes: 36 additions & 8 deletions apps/maple-playground/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 🍁 Maple Playground

A private Angular app for trying out Maple. It has editors for code and a JSON context and shows the result, the AST
and the run's statistics live. Maple runs in a web worker, so an endless loop can't freeze the page; a live run is
stopped after 1 s.
An Angular app for trying out Maple. It has editors for a host context and a script and shows the result, the
AST and the run's statistics live. Maple runs in a web worker, so an endless loop can't freeze the page; a live run is
stopped after 1 s. It is live at [netwin.github.io/maple](https://netwin.github.io/maple/).

```sh
pnpm dev # from the repository root, serves the playground on :4200
Expand All @@ -11,26 +11,54 @@ pnpm dev # from the repository root, serves the playground on :4200
`@netwin/maple` resolves to the library's sources (`packages/maple/src/index.ts`), so library edits show up without a
build.

## Host context and script

The two editors stand for the two sides of a Maple integration:

- **Host context** is trusted code, a JavaScript module the playground imports natively in the worker, with the
worker's full access, as your application would run. Its two optional named exports are what the script gets:

```js
export const INSTANCE_DATA = { upperCase: (text) => text.toUpperCase() }; // new Maple({ globals })
export const RUN_DATA = { name: 'Steve' }; // maple.run(script, data)
```

`INSTANCE_DATA` holds read-only bindings shared by every run, host functions included; `RUN_DATA` is the data of one
run, which the script gets a copy of and may change. A missing export stands for `{}`. Any other export, a value that
is not an object or a module that fails to load shows up as **Host module failed** in the output, apart from Maple's
own errors.

- **Script** is the untrusted code Maple runs. It reaches the two exports' keys and nothing else; a key of `RUN_DATA`
shadows the same key of `INSTANCE_DATA`.

## Welcome dialog and examples

A first visit opens **What is Maple?**, a dialog on what Maple is, how it works and how to read the playground; the
header button reopens it. **Examples** replaces the host context and the script with one of the scripts in
`src/app/data-access/examples.ts`, each explaining itself in a comment. The first of them is what a first visit and
**Reset** show. The header also links to GitHub and npm and shows the library's version, read from
`packages/maple/package.json` at build time.

## Settings

The **Settings** popover in the header sets the run limits, `maxOps` (default `1000000`) and `maxDepth` (default
`100`). Like code and context they persist in local storage, and **Reset** restores them.
`100`). Like the host context and the script they persist in local storage, and **Reset** restores them.

## Comparing with js-interpreter

[`@netwin/js-interpreter`](https://github.com/netwin/JS-Interpreter) is installed as a dev dependency so Maple can be
compared with it. Both comparisons call js-interpreter the same way
(`src/app/data-access/js-interpreter-engine.ts`): the script is wrapped in a function so that `return` works, and each
top-level key of the context becomes a global.
key of `INSTANCE_DATA` and `RUN_DATA` becomes a global, run data shadowing instance data as in Maple.
js-interpreter understands ES5 only, so a script using `let`, `const` or arrow functions shows its parse error in the
js-interpreter row while Maple still reports its figures.

### Speed: the Benchmark tab

The **Benchmark** tab in the output pane runs the current code and context in both engines: 3 unmeasured warm-up runs,
The **Benchmark** tab in the output pane runs the current script and host context in both engines: 3 unmeasured warm-up runs,
then 20 measured runs each. It shows the median, the min–max range, how much slower js-interpreter is, and a warning
when the two engines return different results. Each run is timed end to end, including parsing, setting up the
context and converting the result.
when the two engines return different results. Each run is timed end to end, including setting up Maple, parsing and
converting the result; the host module is imported once, before the runs.

The benchmark has its own worker and a 60 s timeout. js-interpreter has no operation limit, so for an endless loop that
timeout is the only stop.
Expand Down
2 changes: 2 additions & 0 deletions apps/maple-playground/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
},
"defaultConfiguration": "development",
"options": {
"host": "0.0.0.0",
"port": 4200,
"prebundle": {
"exclude": ["monaco-editor"]
}
Expand Down
66 changes: 55 additions & 11 deletions apps/maple-playground/src/app/app.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
<header
class="flex h-12 shrink-0 items-center justify-between border-b border-neutral-200 bg-white px-4 dark:border-neutral-800 dark:bg-neutral-900">
<div class="flex items-baseline gap-2">
<span class="text-lg leading-none">🍁</span>
<h1 class="text-sm font-semibold tracking-tight">Maple</h1>
<span class="text-sm text-neutral-400 dark:text-neutral-500">Playground</span>
<div class="flex items-center gap-3">
<div class="flex items-baseline gap-2">
<span class="text-lg leading-none">🍁</span>
<h1 class="text-sm font-semibold tracking-tight">Maple</h1>
<span class="hidden text-sm text-neutral-400 lg:inline dark:text-neutral-500"
>Playground</span
>
<span class="hidden font-mono text-xs text-neutral-400 sm:inline dark:text-neutral-500">
v{{ VERSION }}
</span>
</div>
<button
type="button"
class="flex h-7 items-center gap-1 rounded-full bg-amber-100 px-2.5 text-xs font-medium whitespace-nowrap text-amber-800 transition-colors hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-300 dark:hover:bg-amber-900/70"
(click)="welcome().open()">
What is Maple?
</button>
</div>
<div class="flex items-center gap-2">
<maple-examples-menu (pick)="load($event)" />
<maple-settings-menu
[(maxOps)]="maxOps"
[(maxDepth)]="maxDepth">
Expand All @@ -27,8 +41,31 @@ <h1 class="text-sm font-semibold tracking-tight">Maple</h1>
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
<path d="M3 3v5h5" />
</svg>
Reset
<span class="hidden md:inline">Reset</span>
</button>
<a
[href]="LINKS.github"
target="_blank"
rel="noopener"
title="Maple on GitHub"
class="hidden size-7 items-center justify-center rounded-full text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-neutral-800 sm:flex dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100">
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
class="size-4">
<path
d="M12 .5C5.65.5.5 5.65.5 12a11.5 11.5 0 0 0 7.86 10.92c.58.1.79-.25.79-.56v-2c-3.2.7-3.87-1.37-3.87-1.37-.53-1.33-1.28-1.69-1.28-1.69-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.19 1.77 1.19 1.03 1.77 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.17 1.18a11 11 0 0 1 5.77 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.81 1.19 1.84 1.19 3.1 0 4.42-2.7 5.4-5.26 5.68.41.36.78 1.06.78 2.14v3.17c0 .31.21.67.8.56A11.5 11.5 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z" />
</svg>
</a>
<a
[href]="LINKS.npm"
target="_blank"
rel="noopener"
title="@netwin/maple on npm"
class="hidden size-7 items-center justify-center rounded-full text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-neutral-800 sm:flex dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100">
<span class="font-mono text-[11px] font-bold tracking-tight">npm</span>
</a>
<maple-theme-picker />
</div>
</header>
Expand All @@ -39,18 +76,23 @@ <h1 class="text-sm font-semibold tracking-tight">Maple</h1>
storageKey="maple-playground-panes">
<maple-pane
slot="pane"
title="Context">
<maple-badge slot="badge">JSON</maple-badge>
title="Host context">
<maple-badge slot="badge">JS</maple-badge>
<maple-code-editor
language="json"
[(value)]="contextString">
language="javascript"
[(value)]="hostContext">
</maple-code-editor>
</maple-pane>

<maple-pane
slot="pane"
title="Code">
<maple-badge slot="badge">JS</maple-badge>
title="Script">
<div
slot="badge"
class="flex items-center gap-1.5">
<maple-badge>Sandboxed</maple-badge>
<maple-badge>JS</maple-badge>
</div>
<maple-code-editor
language="javascript"
[(value)]="codeString">
Expand Down Expand Up @@ -112,3 +154,5 @@ <h1 class="text-sm font-semibold tracking-tight">Maple</h1>
</maple-pane>
</maple-split>
</main>

<maple-welcome-dialog (closed)="welcomeSeen.set(true)" />
Loading
Loading