Repository navigation
Run typical scripts about 48× faster by copying only the data they read - #13
Merged
Merged
Conversation
measure-run parses the context once, before any run, so both engines receive a plain object as a host's run call does. A second loop without forced collections reports median, min and max time, and a closing line says how many times faster or slower Maple is than js-interpreter. A synthetic contact record (46 flat fields, address, 450 activities, 300 custom fields) with three small scripts replaces context.json and demo-script.js as the default fixture. runJsInterpreter takes parsed data; the Benchmark worker parses inside its timed runs, as before.
A JSON string is no longer parsed as data, and run types data as object. A host that holds JSON text parses it first; the playground's workers now do, and the Benchmark worker parses once, outside the timed runs, for both engines.
When nothing on the chain answers to a key, assignment creates the same own data property as defineProperty, and no setter can run, so defineOwn assigns directly; every other key, __proto__ and inherited names included, keeps defineProperty. Copying the 75 KB contact record into a run drops from about 0.77 to 0.34 ms and from 1.22 to 0.85 MB allocated, and a 2,000-item filter/map/reduce from 8.0 to 5.6 ms.
Each top-level key of data that holds an object, array or function is copied when the script first reads it, at most once per run; primitives are bound at once. All keys of a run share one conversion, so shared values and cycles stay intact. An unsupported value inside a key now ends the run with INTERNAL_ERROR at the read instead of a TypeError before the run, and one the script never reads goes unnoticed. A one-key read of the 75 KB contact record drops from 0.34 to 0.04 ms and from 0.87 to 0.09 MB allocated.
new Maple({ astCache: true }) keeps the syntax trees of up to 200 scripts,
keyed by their source text, so a script that runs again is not parsed again;
{ maxEntries } sets another limit, the least recently used script is dropped
when full, and maple.clearAstCache() empties it. Trees are deep-frozen before
they are kept, code that does not parse is not kept, and nested runs share the
cache. measure-run gains --ast-cache, which reuses one instance: a one-line
script drops from 0.017 to 0.008 ms, a 10-line script from 0.55 to 0.51 ms.
contact-summary.js builds a CRM summary from the contact fixture and reads every key, activities and customFields included, so it measures a run in which nothing is left unread.
📋 Release PreviewChangelog0.4.0 (2026-10-21)🚀 Features
🩹 Bug Fixes
⚡ Performance Improvements
📖 Documentation
🔧 Miscellaneous Chores |
NetWin
marked this pull request as ready for review
October 21, 2026 18:32
There was a problem hiding this comment.
🟡 Changes recommended
Moderate boundary and AST-cache validation issues remain unresolved.
3 open issues
What changed in this PR
Optimizes Maple by lazily copying accessed data keys and adding optional bounded AST caching.
Changes:
- Adds lazy, shared host-value conversion.
- Adds configurable frozen AST caching.
- Updates playground benchmarks, fixtures, documentation, and tests.
| File | Summary | Review notes |
|---|---|---|
packages/maple/src/support/own-property.ts |
Optimizes own-property creation. | — |
packages/maple/src/support/own-property.spec.ts |
Tests property behavior. | — |
packages/maple/src/spec-helpers.ts |
Updates runner data typing. | — |
packages/maple/src/maple.ts |
Implements lazy data and AST caching. | Moderate: preserve null for maxEntries validation (1 vote).Moderate: avoid masking host getter TypeErrors (1 vote). |
packages/maple/src/maple.spec.ts |
Tests data validation. | — |
packages/maple/src/host-functions.spec.ts |
Updates test helper typing. | — |
packages/maple/src/evaluator/scope.ts |
Supports lazy binding kinds. | — |
packages/maple/src/evaluator/scope.spec.ts |
Tests lazy binding kinds. | — |
packages/maple/src/evaluator/operators.spec.ts |
Updates evaluator test inputs. | — |
packages/maple/src/evaluator/objects.spec.ts |
Updates evaluator test inputs. | — |
packages/maple/src/evaluator/globals.spec.ts |
Updates evaluator test inputs. | — |
packages/maple/src/evaluator/evaluator.ts |
Reports lazy conversion failures. | — |
packages/maple/src/evaluator/destructuring.spec.ts |
Updates boundary cases. | — |
packages/maple/src/evaluator/control-flow.spec.ts |
Updates boundary cases. | — |
packages/maple/src/escapes.spec.ts |
Updates boundary and escape tests. | — |
packages/maple/src/errors.spec.ts |
Updates test helper typing. | — |
packages/maple/src/builtins/string/string.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/set/set.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/regexp/regexp.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/object/object.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/number/number.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/math/math.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/map/map.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/locale.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/json/json.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/iterator/iterator.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/globals.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/function/function.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/date/date.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/boolean/boolean.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/builtins/array/array.spec.ts |
Updates builtin test helpers. | — |
packages/maple/src/boundary/lazy-data.spec.ts |
Tests lazy data conversion. | — |
packages/maple/src/boundary/from-host.ts |
Adds shared host conversion. | — |
packages/maple/src/ast-cache.ts |
Implements bounded AST caching. | — |
packages/maple/src/ast-cache.spec.ts |
Tests AST cache behavior. | Nit: use “an astCache option” (2 votes). |
packages/maple/README.md |
Documents public API changes. | — |
packages/maple/docs/security.md |
Documents isolation guarantees. | — |
packages/maple/docs/boundary.md |
Documents boundary semantics. | Nit: clarify that the changed value “is seen” (4 votes). |
packages/maple/docs/api.md |
Documents caching and data behavior. | — |
apps/maple-playground/src/app/data-access/maple.worker.ts |
Parses worker context before execution. | — |
apps/maple-playground/src/app/data-access/maple-worker-protocol.ts |
Adds context parsing. | Moderate: reject arrays before running either engine (3 votes). |
apps/maple-playground/src/app/data-access/js-interpreter-engine.ts |
Accepts parsed data. | — |
apps/maple-playground/src/app/data-access/benchmark.worker.ts |
Parses context once per benchmark. | — |
apps/maple-playground/scripts/measure-run.ts |
Adds timing and cache measurements. | Nit: change Pnpm to pnpm (1 vote). |
apps/maple-playground/scripts/demo-script.js |
Removes obsolete fixture. | — |
apps/maple-playground/scripts/context.json |
Removes obsolete fixture. | — |
apps/maple-playground/scripts/contact-ten-lines.js |
Adds benchmark script. | — |
apps/maple-playground/scripts/contact-summary.js |
Adds benchmark script. | — |
apps/maple-playground/scripts/contact-nested-key.js |
Adds benchmark script. | — |
apps/maple-playground/scripts/contact-large-key.js |
Adds benchmark script. | — |
apps/maple-playground/scripts/contact-flat-key.js |
Adds benchmark script. | — |
apps/maple-playground/scripts/contact-context.json |
Adds benchmark fixture data. | — |
apps/maple-playground/README.md |
Documents the new benchmark tool. | — |
apps/maple-playground/package.json |
Renames the benchmark command. | — |
AGENTS.md |
Updates benchmark script guidance. | — |
Review effort: Lite
Owner
Author
Security review
Result: no newly introduced vulnerability found. The discovery pass produced no finding at or above the 0.7 confidence threshold, so no false-positive filtering was needed. The riskiest claims were verified by reading the branch sources and by a probe run against them. Areas examined
|
parseContext refused only what is not an object at all and left an array to Maple, which rejects it, while js-interpreter exposed its indexes, so the Benchmark tab reported two outcomes instead of one input error. From the same review: astCache.maxEntries of null is rejected instead of silently taking the default, the boundary doc's sentence on host mutations is completed, an ast-cache test name gets its article, and the measure-run usage line spells pnpm.
Merged
|
🎉 This PR is included in version 0.4.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
A typical script reads one or two flat keys of a large contact record, such as
return company || firstName. On a 75 KB record, that run drops from 0.762 ms to 0.016 ms, about 48× faster, and allocates 0.07 MB instead of 1.24 MB, about 18× less.Almost all of the old cost was copying the whole of
datainto the sandbox before the first statement ran. A run now copies a top-level key only when the script first reads it, and every copy is cheaper. An opt-in AST cache per instance also skips parsing for scripts that run again.Changes
datais a plain object only. A JSON string is no longer parsed asdata, andruntypesdataasobject. A host that holds JSON text parses it first; the playground workers now do.defineOwn. When nothing on the prototype chain answers to a key, the property is created by assignment instead ofObject.defineProperty.__proto__, inherited names and every other key keep the old path. This speeds up every copy across the boundary, object literals, spread,Object.fromEntriesandJSON.parse.new Maple({ astCache: true })keeps the syntax trees of up to 200 scripts, keyed by their source text;{ maxEntries }sets another limit, and the least recently used script is dropped when full.maple.clearAstCache()empties it. Cached trees are deep-frozen, and code that does not parse is not kept.measure-run. The playground'smeasure-memoryscript is nowmeasure-run. It parses the context once, times runs in a separate loop (median, min, max), compares with js-interpreter, and takes--ast-cache. A synthetic contact record and scripts from a one-liner to a 120-line summary come with it.Behaviour changes for hosts
run(code, '{"a":1}')throwsTypeError: data must be a plain object.datakey (a class instance, a typed array, data nested deeper than 1000 levels) no longer throws aTypeErrorbefore the run. It ends the run withINTERNAL_ERRORat the identifier that reads it, with the same message and theTypeErrorascause; the script cannot catch it. A key the script never reads goes unnoticed. A symbol or bigint directly underdatastill throws before the run.data, and what each holds, are taken when the run starts. A change the host makes inside a value before the script first reads it is seen.docs/boundary.md,docs/api.mdanddocs/security.mddescribe the new behaviour.Measurements
measure-runon the synthetic contact record (46 flat fields,address, 450activities, 300customFields, about 75 KB), Maple median of 500 runs.incrementis a fresh checkout of0804400measured with the same script and fixture.incrementreturn company || firstNamereturn activities.length--ast-cacheThe less of
dataa script reads, the larger the gain. A flat-key read no longer pays for the record at all. A script that reads one large key still copies that key, but 2.6× faster than before. The contact summary reads every key, so the large ones are still copied and most of its time is evaluation; even there, Maple goes from 5.2× to 5.9× faster than js-interpreter.The AST cache saves about 9 µs on a one-liner, 47 µs on the 10-line script and 80 µs on the summary.
measure-runruns Node with an enlarged young generation to measure allocation, so the times are for comparing before and after on one machine.Notes for review
defineOwnassigns through a cast thattypescript/no-unsafe-type-assertionrejects, so that line carries a disable comment with its reason.Reflect.setavoids the cast but gave away about half the gain.contact-summary.jsdisableseslint/object-shorthandandunicorn/no-array-sortfor the whole file, so js-interpreter can run it.Checks
pnpm format && pnpm lint && pnpm typecheck && pnpm testpasses: 698 tests inpackages/maple, 738 inpackages/maple-parity.