Repository navigation
ZIPP 0.0.29
A release for embedders and for large scripts. An embedded script's error
positions are now counted from its own text, and a syntax error says where
it is. A sandbox's host requests have an owner, an abort signal and a
bound, and a permitted redirect is checked before it is followed. A large
script keeps one copy of its source, and zipp js keeps its compiled code
between runs. Most of the fixes come from an audit of 22 October.
-
Positions in embedded scripts are the script's own. The browser
engine compiled a host's script behind its preamble and counted lines
from the top of the joined text, so an app's line N showed as line N+154.
Lines and columns now count from the first character the host passed, as
zipp jsand Node count a file.setScriptOrigin(name, lineOffset, columnOffset)names the script and moves its positions, as Node's
vm.Scriptoptions do, and the preamble's own frames read
zipp:preamble. -
Syntax errors give their position:
SyntaxError: unexpected token ';'followed byat app.js:2:9, with the line and column Node reports. -
Security: a permitted redirect's destination is checked before it is
requested. Withpermissions.network.redirects: true, the embed kit
used to follow a redirect and check the final URL afterwards, so an origin
the allowlist does not name had already received the request. Each
redirect is now checked before the next request is made. -
A sandbox's host requests are bounded and cancellable. At most 64 run
at once. Each request gets anAbortSignalthat fires when the sandbox
gives it up or closes, andclose()settles every run still waiting on
one. Loader 1.3.0, host SDK 1.4.0. -
Large scripts use less memory and start sooner. Each function's
source text is now a range of one shared copy of its script, so
TypeScript's peak working set falls from 349 to 310 MiB. Run again,
TypeScript's 10 MB bundle loads its compiled code fromzipp js's new
cache. It then finishes in 0.83 s instead of 1.14 s, in 191 MiB. -
A class evaluated more than once runs its own
superand its own
name. In 0.0.28, a mixin applied to two bases ran the second class's
super.m()for both. -
Inherited reads, private fields and
x == nullare faster in the
interpreter. Relative to 0.0.28's time:- an inherited-read loop takes 0.83x;
- class features take 0.89x;
- polymorphic objects take 0.90x.
The 78 benchmark programs take 0.99x 0.0.28's time interpreted and 0.98x
with the JIT.
Downloads
| File | What it is |
|---|---|
zipp-0.0.29-windows-x86_64-portable.exe |
The Windows command line; run it from anywhere. |
zipp-0.0.29-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.29-linux-x86_64 |
The Linux command line, x86-64 (glibc 2.35 or later). chmod +x it after downloading. |
zipp-0.0.29-linux-arm64 |
The same for 64-bit Arm. |
zipp-0.0.29-linux-armv7 |
The same for 32-bit Arm (hard float); interpreter only. |
zipp-0.0.29-javascript.wasm |
The JavaScript engine for browsers, Node, Deno and Bun. |
zipp-0.0.29-javascript-lite.wasm |
The JavaScript core without Intl and Temporal, built for size. |
zipp-0.0.29-javascript-python.wasm |
JavaScript and Python, torch built in. |
zipp-0.0.29-javascript-python-base.wasm |
JavaScript and Python without torch. |
zipp-0.0.29-torch-package.wasm |
The torch package for the -python-base engine. |
zipp-0.0.29-<engine>.wasm.br |
Each WebAssembly module compressed with Brotli (quality 11), about a quarter of its size. Serve it with Content-Encoding: br and Content-Type: application/wasm, or let zipp.mjs load it; the plain .wasm is the default. |
zipp-0.0.29-wasm-embed-kit.zip |
zipp.mjs (the loader and sandbox, version 1.3.0) and its types, the host SDK (1.4.0), the engine's TypeScript declarations, the browser GPU adapter, docs and three examples. |
SHA256SUMS |
Checksums of every file. |
Changes in behaviour
zipp jskeeps a large script's compiled code between runs. For a
script of 32 KiB or more, it stores the compiled program in the folder
zipp pyalready uses for Python's:%LOCALAPPDATA%\zipp\cacheon
Windows,$XDG_CACHE_HOME/zipp(or~/.cache/zipp) on Linux. Later runs
load the program from there instead of parsing and compiling it again.
Entries are checksummed and keyed on the source, the grammar goal and the
ZIPP_*settings. Each executable has a folder of its own, holding at
most 64 MB of the most recently used entries. Only the current
executable's folder and the three most recently used others are kept, so
the cache holds at most 256 MB.ZIPP_CACHE_DIR
names another folder; set to empty, it turns the cache off. Smaller
scripts, modules, embedders and sandboxes never use it.- The browser engine's
preambleLinesis 0. Positions need no
correction any more, so a host that subtractspreambleLinesstays
right. class { }andclass extends B { }as statements are syntax
errors, as in Node. A class declaration needs a name; only
export default class { }may leave it out.- Python's
x in b"..."raisesValueErrorfor an int outside 0..255,
as CPython does. It used to answerFalse. - With
permissions.network.redirects: true, at most 5 redirects are
followed. In a browser, whose manual redirects hide their destination, a
redirect fails the request.
Fixes
-
A class definition evaluated more than once ran the newest
evaluation's code. Examples are a class expression in a function called
twice, or a mixin applied to two bases.super.m(),super.x,superin
arrows, field initializers and static methods, a method's use of its own
class's name, and computed field keys all resolved to the latest
evaluation.const M = B => class extends B { m() { return super.m(); } }
applied toAand thenBansweredB's method for both. With a private
field in the parent, the first class's method threw "Cannot read private
member". Each now resolves against the evaluation it belongs to, with the
JIT and without. -
Python's
/on two large ints rounded twice. It also flushed small
results to zero:1 / (1 << 1020)was0.0, and
((1 << 80) + (1 << 27) + 1) / (1 << 80)was1.0. It is now CPython's
algorithm, rounded once, half to even. It agrees with CPython 3.13.14 on
200,000 boundary cases; the old code differed on 26,649. -
Python's
needle in hayon two ASCII strings took quadratic time for
a needle that almost matches everywhere: 263 ms at 256K characters, in
one native call that was charged for the haystack alone. It now uses the
engine's linear substring search and is charged for both lengths. -
Python's
inon arangeand onbytes:x in range(...)asks each member's equality for anything that is not
an int, as CPython does, so a user__eq__or an int subclass is
answered correctly.bytesgained__contains__;bytearrayinherits it.b"bc" in bytearray(b"abc")isTrue.
-
TextDecoder.decodecontinued from stale state when an
options.streamgetter decoded with the same decoder. A fatal error also
forgot that a stream's BOM had been seen, so a later U+FEFF was stripped. -
A torch kernel call over a small view far into its buffer allocated the
whole span below the view: 16 MiB for a 64-byte view at 16 MiB. Only
the bytes the request carries are decoded now. -
zippInstanceUsage()'sretained*totals were described as code that
outlivesEngine.dispose(). That code has been freed with its engine
since #53. The totals are now nameddisposedDynamicFunctions,
disposedDynamicClasses,disposedDynamicFunctionBytesand
disposedDynamicClassBytes, with the old names kept as aliases.
enginesLiveis new. -
A syntax error in an embedding preamble was reported as the guest's
line 1. It now gives the preamble's own line, and a preamble function
that calls nothing is named for the preamble. -
A heap ceiling convicted a script with well under the ceiling
alive. This was in 0.0.28 too. Two things caused it:- after a minor collection, the ceiling was checked at once, without
the major collection that frees old garbage; - near the ceiling, the heap's slot tables doubled to hold garbage, and
they count against the ceiling for the rest of the run.
Phases of 30,000 objects, about 7 MB alive at a time, were convicted at
20 and 24 MiB. A VM called again and again with such a phase was
convicted by its third to fifth call. Near its ceiling, the heap now
grows its tables by a step and runs a major collection first, and only
the check after a major can convict. A heap with no ceiling, like the
zippcommand's, is unaffected. - after a minor collection, the ceiling was checked at once, without
Embedding
New in the zipp-vm crate:
embed::ScriptOrigin { name, line_offset, column_offset }, Node's
vm.Scriptfilename,lineOffsetandcolumnOffset. It is taken by
compile_script_with_originand
compile_script_with_preamble_and_origin.ScriptState::eval_wrapped_in_contextand
eval_wrapped_in_context_richtake the host's expression range inside
its wrapper. Positions then count from the expression, and the wrapper's
frames are left out.zipp_vm::set_code_cache(dir)names the folder for compiled large
scripts, as thezippcommand does.
New in the browser engine and the embed kit:
setScriptOrigin(name, lineOffset, columnOffset), beforeinitScript.- A host function runs with
this.signal. A fetch combines that signal
with the time limit, and fails ascancelledwhen it aborts. An isolated
sandbox gives a request up across its Worker boundary when its own time
limit passes. - At most 64 host requests run at once; the rest wait in order. A request
that ignores its signal keeps its place until it returns, and its late
answer is discarded.usage()reportshostRequestsRunningand
hostRequestsWaiting. close()settles every run or call still waiting on host requests
(closed), aborts the running ones and starts none of the waiting ones.- The host SDK's
host.signalaborts, withdeathCause, when its
generation dies. - Redirect rules:
- each
Locationmust be an http(s) URL the allowlist names; - one time limit covers the request and every hop;
- a 303 (or a 301 or 302 after a POST) continues as a GET without its
body, and the new method must be permitted; authorizationis not sent to another origin.
- each
Performance
Measured on 23 October 2026, native Windows x86-64 (Ryzen 9 9950X3D):
- the published 0.0.28 Windows binary;
- a plain release build of this release's code, as the published Windows
binary is.
Both run with address-space randomization turned off, so image placement
does not move the numbers. The figures are medians of interleaved runs.
Bundled applications. Each library is bundled into one script that runs
a fixed workload. Each figure is the median of 7 runs of zipp js app.js
in its default mode, and the time includes compiling the bundle. The 0.0.28
and 0.0.29 columns have the code cache off, as on a first run. The warm
column has the cache filled.
| Program | 0.0.28 | 0.0.29 | 0.0.29, cache warm | Bellard's QuickJS |
|---|---|---|---|---|
Babel: @babel/standalone transforms about 20 KB of modern JSX twice |
5.04 s | 5.00 s | 4.79 s | 3.72 s |
TypeScript: transpileModule on about 40 KB of classes, generics and enums |
1.16 s | 1.14 s | 0.83 s | 1.03 s |
| Prettier: formats JavaScript, TypeScript and Markdown | 0.56 s | 0.56 s | 0.46 s | 0.44 s |
| marked, lodash, acorn, terser, three.js, immer, RxJS and date-fns | 0.82 s | 0.82 s | 0.75 s | 1.83 s |
| Vue server rendering | 93 ms | 94 ms | 79 ms | 85 ms |
| Preact server rendering | 76 ms | 74 ms | 73 ms | 72 ms |
| React server rendering | 76 ms | 78 ms | 69 ms | fails (no TextEncoder) |
Peak working set, 0.0.28 to 0.0.29 (cache warm in parentheses):
| Program | 0.0.28 | 0.0.29 (warm) |
|---|---|---|
| TypeScript | 349 MiB | 310 MiB (191 MiB) |
| Babel | 250 MiB | 240 MiB (173 MiB) |
| The eight libraries | 212 MiB | 188 MiB (175 MiB) |
| Prettier | 158 MiB | 159 MiB (117 MiB) |
QuickJS holds 34 to 118 MiB on the same four.
The 78 benchmark programs (median of 3 runs each; every output
identical):
- Interpreted (
ZIPP_NOJIT=1, which leaves the regex JIT on, as in
0.0.28's notes): 0.990x 0.0.28's time, faster on 47.
The largest gains are inherited reads (0.83x), class features (0.89x),
polymorphic objects (0.90x), a local whose type keeps changing (0.92x), a
small reconciler (0.93x) andFloat64Arraywrites (0.93x). - Slower interpreted, each confirmed over 9 runs: an eight-way
switch
loop (1.09x),arguments.length(1.05x) and a two-argument call loop
(1.04x). The rest are within 4%. - With the JIT: 0.984x 0.0.28's time, faster on 48 and within 5% on
the rest. - Against Bellard's QuickJS in the same runs, in that lane: the
interpreter takes 0.864x QuickJS's time and is faster on 45 of the 78
programs. 0.0.28 took 0.873x and was faster on 40.
What changed in the interpreter:
- An inline-cache site's first way answers inherited reads and private
fields as part of its own-property hit. An object's explicit prototype is
read from a dense copy beside the prototype table. - The private-name slow paths are out of the dispatch loop. Private-name
records are pruned at a minor collection only once they have doubled. x == nullandx != nullare decided without readingx's heap slot.
A constant that is not a string is answered at its load.- A site that stopped caching fills again on every 64th slow visit, which
helps code that makes a new class per call.ZIPP_NO_IC_PROBATION=1turns
it off.
The interpreter figures in
docs/PERFORMANCE.md
now come from a strict lane, with the regex JIT off as well: 0.683x
Bellard's QuickJS over 30 rows. The earlier lane left ZIPP's regex JIT on.
The README's and that page's figures describe the profile-guided 0.0.28
capture of 21 and 22 October 2026.
Publishing
As for earlier releases, the engines the site serves are refreshed AFTER this
release exists, in one commit that:
- sets
zippEngineReleaseinlanding/package.jsonto0.0.29; - refreshes
landing/public/playground-runtimeandlanding/public/wasmfrom
zipp-0.0.29-javascript-python.wasmandzipp-0.0.29-javascript.wasm
(node crates/zipp-wasm/embed-kit/package.mjs extract, then
sync-playground.mjs --refresh-engine), SHA256SUMS-verified; - syncs the sandbox playground (
node landing/scripts/sync-sandbox.mjswith
the release's files, which brings the site'szipp.mjsto loader 1.3.0)
and rewrites the site's Brotli copies (npm run compress:engines); - updates the version, size and SHA-256 that
landing/src/App.tsxand
landing/README.mdstate about the bundled engine.
Full Changelog: v0.0.28...v0.0.29