Skip to content

Release: 0.4.0 🚀 - #14

Merged
NetWin merged 14 commits into
masterfrom
increment
Oct 21, 2026
Merged

NetWin merged 14 commits into
masterfrom
increment

Conversation

@NetWin

@NetWin NetWin commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

Releases everything merged into increment since 0.3.0:

Highlights

  • this in script functions. Functions that aren't arrows now get strict-mode this: the object of a method call, undefined for a plain call and at the top level, and whatever call, apply and bind pass. Arrows take this from the function around them. Array callbacks, Array.from and the forEach of Map and Set take a thisArg. Constructors and prototypes are still unsupported.
  • Data is copied only when a script reads it. Before, a run copied all of data before the first statement, which was where almost all of its time went. Now each top-level key is copied the first time the script reads it, at most once per run. The less of data a script reads, the bigger the gain: a script that reads one or two flat keys of a large record ran about 48× faster in the Run typical scripts about 48× faster by copying only the data they read #13 benchmark and allocated a fraction of the memory. Copying across the boundary is also cheaper, which speeds up object literals, spread, Object.fromEntries and JSON.parse.
  • Opt-in AST cache. new Maple({ astCache: true }) keeps the syntax trees of up to 200 scripts ({ maxEntries } sets a different limit, and the least recently used one is dropped first). maple.clearAstCache() empties it.

Behaviour changes for hosts

  • data must be a plain object. run(code, '{"a":1}') now throws TypeError: data must be a plain object, so hosts that hold JSON text have to parse it first.
  • An unsupported value inside a data key (a class instance, a typed array, nesting deeper than 1000 levels) no longer throws a TypeError before the run. It ends the run with INTERNAL_ERROR when the script reads that key, and a key the script never reads goes unnoticed. A symbol or bigint directly under data still throws before the run.
  • If the host changes something inside a data value before the script first reads it, the script sees the change.
  • A JSON reviver no longer receives the holder as a third argument. The holder is this now, as in JavaScript. A replacer also gets its holder as this.

Tooling

  • The playground's measure-memory is now measure-run. It adds timing, a comparison with js-interpreter, --ast-cache, and a synthetic contact record as its fixture.
  • The Benchmark tab rejects an array context before either engine runs.
  • CONTRIBUTING.md: commit scopes now name the area changed, not the package.

NetWin and others added 14 commits October 20, 2026 21:53
A function that is not an arrow gets strict-mode this: the object of a
method call, undefined for a plain call, and the receiver call, apply
and bind pass, never boxed. An arrow sees the this of the function it is
written in. At the top level this is undefined, since there is no global
object.

Host functions and intrinsics still receive no this. Constructors and
prototypes stay unsupported.
every, filter, find, findIndex, findLast, findLastIndex, flatMap,
forEach, map, some, Array.from and the forEach of Map and Set call their
callback with the thisArg as this. Every other built-in callback keeps
undefined.
A reviver no longer receives the holder as a third argument; it is this
instead, as in JavaScript. Use this where a reviver read the third
argument.
A function replacer is called with the object holding the key as this,
and with a fresh object around the value at the top level, as in
JavaScript.
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.
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.
@github-actions

Copy link
Copy Markdown

📋 Release Preview

Changelog

0.4.0 (2026-10-21)

🚀 Features

  • builtins: pass thisArg to array, Map and Set callbacks (80e5ccb)
  • evaluator: support this in script functions (f90237d)
  • input: accept data only as a plain object (88dfd69)
  • json: give a JSON replacer its holder as this (d1c1e20)
  • json: give a JSON reviver its holder as this (7ed2e5f)
  • parse: cache syntax trees per instance when enabled (e53f816)

🩹 Bug Fixes

  • playground: reject an array context before either engine runs (f037c65)

⚡ Performance Improvements

  • boundary: assign new own properties directly in defineOwn (6728bff)
  • input: convert data keys on first read (ce33aa2)

📖 Documentation

  • contributing: update conventional commit instructions (8cc9c43)

🔧 Miscellaneous Chores

  • playground: add a contact summary script to measure-run (ae94d6d)
  • playground: rename measure-memory to measure-run and add timing (54e170e)

@NetWin
NetWin merged commit c3a89a3 into master Oct 21, 2026
7 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.4.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant