Skip to content

ZIPP v0.0.26

Choose a tag to compare

@github-actions github-actions released this 18 Oct 03:28
· 34 commits to main since this release

ZIPP 0.0.26

ZIPP is now introduced as the Zippy Interpreter for Polyglot
Programming
: a portable polyglot runtime, one virtual machine that every
supported language compiles to, with JavaScript and Python supported today.
This release changes how it is delivered and embedded, moves Test262 to the
latest corpus, and continues the interpreter work against QuickJS.

  • Plain downloads. A Windows portable .exe and a per-user .msi
    installer, Linux binaries for x86-64, arm64 and armv7, one .wasm file
    per WebAssembly engine, and an embed kit: no more archives to unpack.
  • Embedding WebAssembly is one file. Every engine .wasm carries its
    own bindings, so zipp.mjs loads any of them, and torch is an optional
    second file.
  • Sandboxed by default. createSandbox() runs a program in its own
    Worker with nothing permitted: storage, files, network (allowlisted, no
    cookies) and host functions are each granted explicitly, with step and
    time limits.
  • Latest Test262, run once and unmodified: 96,229 of 96,230 executions
    on tc39/test262 of 13 October 2026; the arm64 CLI passes the same corpus.
  • Interpreter against Bellard's QuickJS: on 21 diagnostic workloads ZIPP's interpreter now takes 0.931x the time of Bellard's QuickJS's (1.199x at the start of this cycle), and 0.791x on the 17 hostile workloads (0.834x).

Downloads

File What it is
zipp-0.0.26-windows-x86_64-portable.exe The Windows command line; run it from anywhere.
zipp-0.0.26-windows-x86_64-installer.msi The same binary, installed for the current user into %LOCALAPPDATA%\Programs\ZIPP and added to PATH; no administrator rights. Uninstall from Settings > Apps.
zipp-0.0.26-linux-x86_64 The Linux command line, x86-64 (glibc 2.35 or later). chmod +x it after downloading.
zipp-0.0.26-linux-arm64 The same for 64-bit Arm.
zipp-0.0.26-linux-armv7 The same for 32-bit Arm (hard float); interpreter only.
zipp-0.0.26-javascript.wasm The JavaScript engine for browsers, Node, Deno and Bun.
zipp-0.0.26-javascript-lite.wasm The JavaScript core without Intl and Temporal, built for size.
zipp-0.0.26-javascript-python.wasm JavaScript and Python, torch built in.
zipp-0.0.26-javascript-python-base.wasm JavaScript and Python without torch.
zipp-0.0.26-torch-package.wasm The torch package for the -python-base engine.
zipp-0.0.26-<engine>.wasm.br Each WebAssembly module compressed with Brotli (quality 11), 74% to 81% smaller: javascript 1,196,785 bytes, javascript-lite 723,299, javascript-python 1,964,927, javascript-python-base 1,588,216, torch-package 403,240. Serve one with Content-Encoding: br and Content-Type: application/wasm and the browser decodes it; the plain .wasm is the default. Added to the release after publication, with SHA256SUMS regenerated to cover them.
zipp-0.0.26-wasm-embed-kit.zip zipp.mjs (the loader and sandbox) and its types, the host SDK, the engine's TypeScript declarations, the browser GPU adapter, docs and three examples. Replaced after publication by a kit with loader 1.2.0, which also loads a .wasm.br: decoded by the server (Content-Encoding: br), decompressed by the loader where the runtime can (Node, Firefox), or by falling back to the plain .wasm beside it. Its modules.json lists the .wasm.br files and BUILD-INFO.txt names the modules' commit and the kit's source (kit-source=619deb1d); the examples, types, host SDK and GPU adapter are unchanged, and SHA256SUMS was regenerated.
SHA256SUMS Checksums of every file.

The macOS builds that were planned for this release are not included.

Embedding: one file per engine, sandboxed by default

import { loadZipp } from "./zipp.mjs";

const zipp = await loadZipp("./zipp-0.0.26-javascript.wasm");
const sandbox = await zipp.createSandbox({
  limits: { instructions: 50_000_000, timeoutMs: 5000 },
  permissions: { storage: true, files: { "data.csv": "a,b\n1,2" }, network: ["https://api.example.com/"] },
});
const { output } = await sandbox.run(code);
await sandbox.close();
  • How loading works. Each engine module carries the wasm-bindgen glue
    generated for it in a zipp.glue custom section and a zipp.meta record
    of its version, commit and languages. zipp.mjs compiles the module,
    imports that glue (from a blob: URL, or data: in Node), checks that
    the engine reports the build its record names, and starts it. A module
    and its bindings can no longer come from different builds. The release
    workflow strips the sections again and byte-compares the result with the
    module its Node boundary suite tested, so the custom sections change
    nothing the engine runs.
  • What a sandbox permits. Nothing by default: no localStorage, no
    files, no fetch (it does not exist), no host functions, and nothing of
    the page or of Node (document, process, require are undefined).
    permissions.storage gives a private in-memory localStorage with a
    quota; permissions.files a read-only (or writable) file map reached with
    host.callSync("fs.read", path) and kin; permissions.network a fetch
    that reaches only the listed origins or URL prefixes, with no cookies or
    credentials, the Fetch standard's forbidden headers dropped, redirects
    refused unless allowed, and time and size limits; host the functions a
    program may call with host.call. Unknown option names are errors.
  • Limits. limits.instructions is the engine's step budget;
    limits.timeoutMs ends a sandbox's Worker when run or call takes
    longer, the only way to stop a loop that never yields.
  • Python and torch: loadZipp({ engine: "...-javascript-python-base.wasm", packages: ["...-torch-package.wasm"] }).
    A Python program's files are its importable modules.
  • The host SDK (1.3.0) takes wasmUrl alone and loads it through the
    same loader; moduleUrl still selects the separate glue file of 1.2.

The site's new sandbox playground
runs this in the browser, with a switch for each permission and limit.

Checked: the kit's Node tests and sandbox tests (16 checks, in-process and in
worker threads, against a local HTTP server), and Chromium, Firefox and WebKit
(18 checks each: the examples, isolated sandboxes for every engine, the host
SDK, both content types GitHub and ordinary servers send, and the recommended
Content-Security-Policy).

Test262

The pin moves from tc39/test262 4249661 (26 May 2026) to d53ebc8b
(13 October 2026). On the new corpus, ZIPP passes 96,229 of 96,230
executions with zero skips, unmodified; the original DateTimeFormat ECMA-402
shard passes 490 of 490. CI now runs the corpus once against an
expected-failure manifest with one entry, an Annex B test whose final
assertion is ES2017 wording that ES2018 deleted (Node fails it too). The five
corrections and second, patched run the old pin needed are gone: upstream
fixed what four of them corrected.

The new corpus found 236 executions ZIPP failed, all fixed:

  • Iterator.prototype.chunks, windows and includes, the stage 4 iterator
    chunking and includes proposals (218 executions);
  • take and drop reject a limit above 2^53 − 1 (12);
  • Promise.try returns an already-resolved promise of its constructor
    unwrapped, and a subclass gets its own capability (4);
  • the test host's module-source object and import.source of it (2).

The arm64 Linux CLI, built and run on an Arm runner, passes the same corpus (96,229 of 96,230 with the same expected failure), the first full run of its JIT; the armv7 CLI passes the 32-bit-sensitive parts of it under emulation.

Fixes found by our own testing

  • An iterator closing a generator ignored an overridden return: a
    for-of break, destructuring, Array.from's close and an iterator
    helper's return() resumed the generator with its intrinsic return
    instead of calling an own or replaced one (a57c51b4).
  • Frozen and sealed classes stayed writable, a sealed class's static
    could be deleted, and assigning to a function's own accessor property
    replaced it instead of calling its setter (5f86d140).
  • A computed-key class member lost its class's private brand chain
    (0ecf155e); frozen and sealed objects now take the shape of their new
    descriptors (3fd7ff7b).
  • eval code saw a builtin global the program had replaced or deleted
    as the original (b628f56d); builtin writes on a frozen, sealed or
    non-extensible global are pinned (a34c60e6).
  • toFixed, toExponential and toPrecision round exactly
    (07b09f9c).
  • Python: a glob whose last component is ** matches files too, as in
    Python 3.13 (57068197); all 196 corpus programs match CPython 3.13.
  • Indices and lengths past 2^32 were truncated on 32-bit builds, which
    every WebAssembly engine is, 0.0.25's included, as is the new armv7 CLI:
    new ArrayBuffer(8).slice(2, 2**32) was empty and slice(2**32, 7) held
    7 bytes; array-like methods near 2^53 (findLast, slice, indexOf,
    copyWithin, splice and map species lengths) read or passed the wrong
    index; a RegExp's lastIndex past 2^32 was written back as 2^32 − 1;
    charAt, substr, padStart, DataView offsets and BigInt.asIntN had
    the same flaw. A value was converted to the 32-bit usize before it was
    clamped; it is clamped in 64 bits now (2d337999), and a full Test262 run
    on a 32-bit build passes (96,229 of 96,230). CI now runs one (65c1c1d8).
    On 64-bit, "abc".padStart(2**32, "") returns "abc" as the
    specification says, where it threw a RangeError.

Performance

This cycle's interpreter work was measured against Bellard's QuickJS
(2026-06-04), interpreter against interpreter (ZIPP with ZIPP_NOJIT=1),
on canonical profile-guided builds trained on a held-out corpus, in
startup-adjusted process cycles (lower is faster):

Suite Cycle start (46f8cafe) This release's code (35f73b1a)
21 expansion workloads, medium size 1.199x; QuickJS faster on 14 0.931x; ZIPP faster on 10, QuickJS on 10, 1 even
17 hostile workloads 0.834x; QuickJS faster on 10 0.791x; ZIPP faster on 9, QuickJS on 6, 2 even

The work behind it: inline property-cache hits and stores, superinstructions
for common instruction pairs, a second, smaller interpreter loop for simple
functions and loops, dictionaries that leave a hole on delete, Proxy traps
without descriptor objects, per-site private-name resolution, direct
generator resumption, one-dispatch switch cases, exact number formatting,
DataView fast paths, and direct calls for WeakMap, Map/Set and string
methods. QuickJS is still faster on large-heap churn (1.80x), generators
(1.49x), class features (1.47x), accessors and Proxy (1.45x), a hot module
graph (1.66x), closure calls (1.30x), eval (1.29x) and string building
(1.22x).

No new comparison with Node, Bun or Deno, or with 0.0.25's JIT, was run for
this release; the canonical engine capture remains fa8e3b86's
(docs/PERFORMANCE.md).

import torch in the browser. The WebAssembly allocator asked for
memory a few pages at a time, so importing torch grew the memory about
2,700 times, and WebKit copies the whole memory on every grow. The memory
now grows in steps of half its size (at most 64 MiB) (7eb926e9), a
compiled library module is shared between programs instead of copied into
each (f091e2fe), and each compiled function's code is trimmed to its
length (a4099b74). The first import torch in a fresh engine, on one
Windows machine: WebKit from about 32 s to under 0.4 s, Chromium from
0.64 s to about 0.27 s, Node from 0.78 s to about 0.3 s; a later import in
Node from 0.105 s to about 0.09 s; WebKit's first Python run from 1.4 to
1.6 s to about 0.16 s. Four engines that imported torch now hold 215 MB in
Node, where they held 538 MB.

WebAssembly size

Each engine's code, against 0.0.25's published modules, raw / Brotli-11
(Windows build of 57068197):

Module 0.0.25 0.0.26 code Larger by 0.0.26 file, with bindings
JavaScript 4,466,781 / 1,146,621 4,618,917 / 1,184,634 3.4% / 3.3% 4,671,080 / 1,197,420
Lite 2,491,457 / 683,049 2,596,967 / 711,825 4.2% / 4.2% 2,649,124 / 723,155
Python without torch 6,187,748 / 1,535,660 6,342,528 / 1,575,546 2.5% / 2.6% 6,402,163 / 1,588,748
Complete, torch built in 8,220,591 / 1,910,130 8,381,571 / 1,950,561 2.0% / 2.1% 8,441,201 / 1,964,429
torch package 2,156,660 / 401,562 2,162,554 / 403,062 0.3% / 0.4% 2,162,699 / 403,120

The bindings each engine now carries add about 52 to 60 KB raw, 11 to 13 KB
after Brotli: the glue file that used to be a separate download.

Other changes

  • Release workflow. Builds move to release-build.yml, which
    release.yml calls at the tag and which can be dispatched on any commit to
    try a release. Its packaging steps are repository scripts that run the
    same way on a workstation (tools/release/, crates/zipp-wasm/embed-kit/).
  • Build fixes: the sandbox and WebAssembly lockfiles record zipp-vm's
    memchr dependency (bdbdc2b1); the arm64 test build compiles again and
    is warning-free off x86-64 (57374b30).
  • Documentation: a shorter README, with its detail in docs/ pages and
    the previous README archived; the site presents ZIPP as a polyglot runtime.

Publishing

As for earlier releases, the engines the site serves are refreshed AFTER this
release exists, in one commit that:

  1. sets zippEngineRelease in landing/package.json to 0.0.26;
  2. refreshes landing/public/playground-runtime and landing/public/wasm from
    zipp-0.0.26-javascript-python.wasm and zipp-0.0.26-javascript.wasm
    (node crates/zipp-wasm/embed-kit/package.mjs extract, then
    sync-playground.mjs --refresh-engine), SHA256SUMS-verified;
  3. syncs the sandbox playground (node landing/scripts/sync-sandbox.mjs with
    the release's files);
  4. updates the version, size and SHA-256 that landing/src/App.tsx and
    landing/README.md state about the bundled engine.