Skip to content

ZIPP v0.0.23

Choose a tag to compare

@github-actions github-actions released this 12 Oct 18:44
· 233 commits to main since this release

ZIPP 0.0.23

Fixes for seven wrong results in 0.0.22, smaller WebAssembly engines, a
faster native CLI and a faster Python frontend. The native CLI runs the
JavaScript benchmark rows in 4.9% less time and the Python suite in 0.77x
of 0.0.22's time (1.30x of CPython 3.13, from 1.79x), builtins call
compiled callbacks without the interpreter loop, and the browser modules
are 1.6% to 3.8% smaller on the wire.

Downloads

  • zipp-0.0.23-x86_64-pc-windows-msvc.zip: native CLI for Windows, complete: JavaScript, Python, torch and native GPU.
  • zipp-0.0.23-x86_64-unknown-linux-gnu.tar.gz: the same native CLI for Linux.
  • zipp-wasm-0.0.23-web.zip: JavaScript-only browser engine, TypeScript bindings and Worker host SDK.
  • zipp-wasm-0.0.23-web-lite.zip: ZIPP Lite, the JavaScript core without Intl and Temporal, built for size, with the same bindings and host SDK.
  • zipp-wasm-0.0.23-web-python-base.zip: JavaScript + Python without torch; add torch from web-torch when a page needs it.
  • zipp-wasm-0.0.23-web-python.zip: JavaScript + Python complete, torch built in, with the browser GPU adapter and CPU WASM kernels.
  • zipp-wasm-0.0.23-web-torch.zip: the torch package, zipp_torch.wasm and its zipp_torch.js loader, added to web-python-base through addPythonPackage.
  • SHA256SUMS for every asset.

Every WASM archive includes BUILD-INFO.txt, PROFILE.json and SHA256SUMS.
Publishing requires CI and the full security workflow at the exact tagged commit.

Fixes

Wrong results in 0.0.22, each fixed with a test (those that print
expect Node 24's output):

  • Memory that grows in place never reached the collector. An array
    pushed or index-stored to a megabyte, an object gathering thousands of
    keys or a filling Map allocates no new object, so a loop that built and
    dropped such things ran no collection. In the browser engines the heap
    ceiling ended a script whose live set was a megabyte ("script exceeded
    its memory budget"), or the instance trapped at the end of its linear
    memory; natively, 2,000 calls that each built an 80,000-element array
    held 1.2 GB (Node: about 170 MB). The engine now reads what its
    allocator has outstanding and collects on that growth: the same program
    peaks at 60 MB.
  • var s = "number"; s += x in a loop rewrote the string typeof
    returns
    , so typeof 1 printed number012... afterwards. Shared
    strings can no longer be appended to in place.
  • Native CLI: an array callback's unassigned local kept the previous
    element's value.
    [7, 1].map(function (x) { var t; if (x > 5) t = x; return t; }) gave [7, 7]; it gives [7, undefined]. map, filter,
    forEach, reduce, the find family and sort reused one register window
    for every call of a compiled callback.
  • Native CLI: an array callback kept reading a deleted or redefined
    global.
    After delete globalThis.g it returned NaN where a
    ReferenceError is due, and after a getter was defined on g it read the
    old value.
  • Native CLI: a builtin run directly as a promise job could abort the
    process.
    Promise.resolve().then(Array.prototype.forEach.bind(arr, f))
    panicked (index out of bounds) once f was compiled and called another
    function.
  • Browser engines: calling or constructing a long chain of bound
    functions threw
    "Maximum call stack size exceeded" (300 levels of
    g = g.bind(null, i)); each level was a native recursion under the
    browser profile's cap of 32. The chain is now unwrapped in a loop.
  • BigInt64Array and BigUint64Array sort exactly. Their default sort
    compared elements as doubles, so [2n**60n + 1n, 2n**60n] stayed
    unsorted, as did BigUint64 values above 2**63.

Other changes a program can see

  • Identifiers follow Unicode 17. 12,823 code points that are
    unassigned in Unicode 17 were accepted in identifiers (var \u{558});
    they are rejected, as Node 24 rejects them.
  • A class's static keys from "4294967295" up list among the string
    keys in creation order, as Node lists them.
  • Python str.strip() and str.split() use Python's whitespace: U+FEFF
    is no longer whitespace, and U+001C to U+001F and U+0085 are;
    split(None, n) no longer strips the remainder. hash(str) values
    change.
  • Collections run where none did (memory grown in place, and compiled
    loops that only concatenate strings or call natives), so
    finalization-registry callbacks and WeakRef clearing can come earlier.
  • Step metering in the browser engines: JavaScript programs charge
    the same steps as in 0.0.22. Python programs charge different counts:
    starting the runtime charges about 270 steps more, and loops that read
    properties, build comprehensions, raise and catch, or sort by a key
    charge fewer (2,000 to 4,600 fewer on loops of a few hundred
    iterations). A budget tuned to 0.0.22's Python counts buys a little more
    work.
  • Embedders: zipp_vm::note_large_block and note_every_block let a
    host's allocator report the blocks it hands out, so growth in place is
    collected (one that reports nothing behaves as 0.0.22 did), and
    zipp_vm::embed::parse_f64 parses numbers as the engine does.

WebAssembly size

Module sizes on a Windows build of 20123116, the release's source but for
its last change to the regex compiler (which makes Lite 45 bytes smaller raw
and 113 after Brotli), raw and Brotli-11, beside 0.0.22's:

Module 0.0.22 0.0.23 Smaller by, raw / Brotli
JavaScript 4,876,583 / 1,227,452 4,636,144 / 1,180,609 4.9% / 3.8%
Lite 2,634,413 / 710,436 2,573,530 / 697,741 2.3% / 1.8%
Python without torch 6,519,418 / 1,597,418 6,331,117 / 1,566,620 2.9% / 1.9%
Complete, torch built in 8,443,452 / 1,950,080 8,254,998 / 1,918,013 2.2% / 1.6%

The bytes came out of tables: the time zone transitions and the zone table
are packed by column, the likely-subtags table is packed into fixed-width
keys, identifier characters are read from the regex engine's Unicode
tables, a regex literal's early errors are checked by the parser the VM
already has, decimal text is parsed by the engine's own parser in every
module (so core's 10 KB table is gone), and two sorts go through the
integer sort the engine already has. The torch package is unchanged at
about 0.38 MB.

WebAssembly speed

The browser engines interpret, in a checked profile with no unsafe. On
the size pass's corpus under Node 24 (24 cases; one process a case,
pinned, four configurations), against 0.0.22's modules:

  • The JavaScript module runs it in 0.84 to 0.86 of the time, Lite in 0.84.
    A call that allocates nothing takes 0.11 to 0.18 of the time, a string
    built by appending 0.56 to 0.64, for-in over an object 0.51 to 0.60.
  • Rows that are slower: object allocation (3% to 7% in the JavaScript
    module: every allocation is now reported, so that memory grown in place
    is collected), and in Lite
    the error constructors, delete, matchAll and engine start (2% to 9%).
    Lite's Unicode RegExp compile read 17% slower in that screen and is
    fixed in this release (\p{Letter} compiles in 0.93 of the fourteenth
    pass's time); the screen ran before the fix.
  • The two Python modules were not screened against 0.0.22 for this
    release. The fourteenth pass measured them at 0.92 to 0.93 of 0.0.22's
    time.

Native CLI

The 13 JavaScript benchmark rows take 4.9% less time than in 0.0.22
(paired, 95% interval 4.3% to 5.7%), in an ordinary, not profile-guided,
release build: async-promise-chain and polymorphic-objects 18% less,
json-large 15% less, and every row prints the same bytes.
class-prototype-hot is the exception, 3% slower. The headline ten rows
are at 0.90x of Node 24's time, and all 13 at 0.63x.

  • Builtins call compiled functions cheaply. A compiled callback runs
    over one reused register window in map, filter, forEach, reduce,
    some, every, find, findIndex, sort, Array.from and
    Map/Set forEach, and any other builtin's call of a compiled function
    (a promise reaction, a getter, a replace function) skips the interpreter
    loop: 6M capturing map and forEach callbacks take 79 ms where they
    took 232 in 0.0.22 (Node: 64), s.replace(re, f) over 800k matches 77 ms
    where it took 261 (Node: 62), Array.from(array, f) of 3M elements 59 ms
    where it took 611 (Node: 88).
  • Class construction. A class keeps its constructor's function value
    instead of making it again for every new, a class field is appended
    without a descriptor object, and a loop that constructs is compiled: a
    million constructions of a class with two fields take 274 ms where they
    took 2,059 in 0.0.22 (Node: 34), a derived class 537 where it took 864
    (Node: 38). Construction is still several times Node's time.
  • Generators and iterators. for (v of gen) takes each step without
    its result object (3M steps 170 ms where they took 538; Node: 65), and
    iterator result objects share one key plan.
  • Strings and keyed access. s += t appends in place, a keyed access
    predicts its slot from the shape and the key, and for-in and
    Object.keys reuse a shape's key list. A word count through
    counts[w] = (counts[w] || 0) + 1 takes 377 ms where it took 640
    (Node: 140).
  • Memory. Besides growth in place, compiled loops that only build
    strings now poll the collector: such a loop held 675 MB where Node holds
    30, and holds 30.

Python

The native CLI runs the 36 programs of the Python benchmark suite that
CPython also runs at 1.30x of CPython 3.13's in-script time, where 0.0.22
took 1.79x, and the suite's 44 programs in 0.77x of 0.0.22's time. Eleven
programs now take less time than in CPython (seven in 0.0.22): among them
range_loop, int_arith, global_read, list_index and builtin_len.
The slowest against CPython are str_methods (3.7x), instance_new
(3.7x), try_raise_catch (3.2x) and fstring (3.2x).

Twenty-nine changes, among them: a callee's frame pushed from the call
site's cache, small-int arithmetic and comparisons settled in the dispatch
loop, compiled subscripts, stores and value instructions calling their own
helpers, a raise caught in its own frame without leaving the compiled
loop, a traceback frame recorded while unwinding, plain classes
constructed inline, and generators stepped from compiled loops. The 179
programs of tests/python_corpus print what CPython 3.13 prints, except
the five that need a GPU or a transport.

Validation

Test262 passes with the same nine documented expected failures as before
(95,671 of 95,680 executions). These figures are from one Windows machine;
the measurements, the gates and what was not measured are in the
validation notes.

Publishing

As for 0.0.22, 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.23;
  2. refreshes landing/public/playground-runtime from the
    zipp-wasm-0.0.23-web-python asset (sync-playground.mjs --refresh-engine)
    and landing/public/wasm from zipp-wasm-0.0.23-web, both SHA256SUMS-verified;
  3. updates the version, size and SHA-256 that landing/src/App.tsx and
    landing/README.md state about the bundled engine.

Full Changelog: v0.0.22...v0.0.23