Skip to content

ZIPP v0.0.24

Choose a tag to compare

@github-actions github-actions released this 14 Oct 09:53
· 96 commits to main since this release

ZIPP 0.0.24

Fixes for the eight issues reported against 0.0.23's code (#32 to #34 and
#41 to #45) and for further wrong results found by differential testing
against Node 24, CPython 3.13 and PyTorch; more of JavaScript running
compiled; Python at 1.09x CPython 3.13's time, from 1.31x; torch at 4.2x
PyTorch's, from 17x; and WebAssembly engines 1.0% to 4.0% smaller on the
wire. The native CLI runs the JavaScript benchmark rows in 2.6% less time
than 0.0.23, and the headline ten at 0.88x of Node 24's time, with
ZIPP's median below Node's on every one of the 13 rows.

Downloads

  • zipp-0.0.24-x86_64-pc-windows-msvc.zip: native CLI for Windows, complete: JavaScript, Python, torch and native GPU.
  • zipp-0.0.24-x86_64-unknown-linux-gnu.tar.gz: the same native CLI for Linux.
  • zipp-wasm-0.0.24-web.zip: JavaScript-only browser engine, TypeScript bindings and Worker host SDK.
  • zipp-wasm-0.0.24-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.24-web-python-base.zip: JavaScript + Python without torch; add torch from web-torch when a page needs it.
  • zipp-wasm-0.0.24-web-python.zip: JavaScript + Python complete, torch built in, with the browser GPU adapter and CPU WASM kernels.
  • zipp-wasm-0.0.24-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.

The torch package's wire protocol moved to version 3: use the
web-torch package of the same release as web-python-base.

Fixes

Wrong results in 0.0.23, each fixed with a test (those that print expect
Node 24's, CPython 3.13's or PyTorch's output).

Reported by @takahashim, with thanks:

  • A typed array's elements were not its own properties (#32):
    new Uint8Array(2).hasOwnProperty(0) and Object.hasOwn(ta, 0) gave
    false.
  • A function's prototype made non-writable stayed writable (#33),
    through Object.defineProperty or Object.freeze.
  • obj.hasOwnProperty(k) ran the built-in for any receiver (#34), as
    did propertyIsEnumerable and isPrototypeOf: a Proxy's traps did not
    run, an own or inherited replacement was ignored, and a null-prototype
    object did not throw.
  • A class or function on another object's prototype chain was not
    searched
    (#41): after Reflect.setPrototypeOf(fn, C), fn.s missed
    C's static. Beside it, Object.create(map).size answered the map's
    size where a TypeError is due, an own size and a subclass's
    get size() were ignored, and a getter inherited by a Map, Set, Date or
    Promise ran with the wrong this.
  • typeof name inside with ignored the with object (#42).
  • A replaced console method was never called (#43): console.log,
    info, warn, error and debug were compiled to a print.
  • There was no performance object (#44), though performance.now()
    worked; it now exists, with now() and timeOrigin.
  • Constructing a derived class read newTarget.prototype itself
    (#45), which a Proxy newTarget could observe; the base that creates the
    object reads it, when super() reaches it.

Found by differential programs:

  • Native CLI: a global redefined with Object.defineProperty(globalThis, ...) went on being called as the old function by a compiled loop.
  • arguments.length = n deleted the elements past n, and a length
    written as a string or other non-integer left iteration covering every
    element.
  • Iterators lost their own properties, and a replaced generator
    next/return/throw was ignored
    , across for-of, spread,
    yield* and for await.
  • { ...src } ran setters and could replace the prototype: object
    spread now defines the copied properties.
  • A Proxy's ownKeys trap ran twice per for-in level; Node runs it
    once.
  • toLocaleLowerCase in Turkish, Azerbaijani and Lithuanian gave the
    medial sigma at the end of a word ("ΑΣ" to "ασ"; Node: "ας").
  • Python: a zero-argument super() in a method whose self a lambda
    or nested function captures did not compile, and in a list, set or dict
    comprehension it took the comprehension's iterator as its receiver.
  • torch: optimizer steps replaced the parameter tensors instead of
    updating them in place; randn, normal_ and uniform_ did not follow
    PyTorch's random stream or rounding; float64 rand lost precision; GRU's
    hidden update used another formula; x**3, x**-2, x**0.5 and
    x**-0.5 differed from PyTorch in the last bit or at -0 and -inf; and
    the error for a saved tensor modified in place named the wrong node.
    randn, normal_ and uniform_ now match PyTorch bit for bit for
    float32 and float64.

And one performance defect: pop on a large array took time
proportional to its length
(80,000 pops off a 160,000-element array
took 2.3 s; 46 ms now).

Other changes a program can see

  • performance is a global object with now() and a read-only
    timeOrigin, and console's methods are read at each call, so a
    program or a host can replace either; console.log = console.error
    writes to stderr. performance.now does not check its receiver.
  • A for-in over a Proxy calls its traps in V8's order, and the
    instanceof TypeError for a non-object prototype reads as Node's.
  • More code runs compiled: loops that build literals, create
    closures, destructure, spread, list keys, pop, run a for-in or a
    for-of, or hold a finally; functions with a rest parameter or that
    use arguments; code calling Object.assign, String.fromCharCode,
    the Promise combinators and the other statics; a compiled function
    called from a compiled loop without a frame. Each helper polls the
    collector where it allocates, so finalization callbacks and WeakRef
    clearing can come at different points than before.
  • Same-binary switches added (each turns one change off, for an
    A/B): ZIPP_NO_TIERC_REST, ZIPP_NO_TIERC_ARGUMENTS,
    ZIPP_NO_STATICFN_JIT, ZIPP_NO_ASSIGN_PLAIN, ZIPP_NO_CTOR_STORE,
    ZIPP_NO_INDEX_KEY_FAST, ZIPP_NO_GENERAL_ITER_REGION,
    ZIPP_NO_TYPEOF_SMALL_BIGINT, ZIPP_NO_JOB_WINDOW,
    ZIPP_NO_JOB_INT_WINDOW, ZIPP_NO_OLD_JOBS_SKIP,
    ZIPP_NO_PROMISE_CAPABILITY_DIRECT, ZIPP_NO_COMBINATOR_LEAN, and for
    Python ZIPP_NO_PY_HEADER_DOMINANCE, ZIPP_NO_PY_IC_ALTS,
    ZIPP_NO_PY_METHOD_INLINE, ZIPP_NO_PY_NEW_HIT,
    ZIPP_NO_PY_RAISE_HELPER, ZIPP_NO_PY_SORT_COLUMNS,
    ZIPP_NO_PY_STORE_CALLS, ZIPP_NO_PY_SUPER_LEAN,
    ZIPP_NO_PY_LAZY_GLOBALS.
  • Regex: the step charges of a metered engine change on two lazy-loop
    rows (75 to 49, 59 to 39) and one lookbehind row (32 to 34).

WebAssembly size

Module sizes on a Windows build of 35e6d1dc, the release's source, raw
and Brotli-11, beside 0.0.23's source (5cffc37a) built the same way:

Module 0.0.23 0.0.24 Smaller by, raw / Brotli
JavaScript 4,636,188 / 1,180,196 4,435,704 / 1,133,411 4.3% / 4.0%
Lite 2,573,473 / 697,512 2,450,334 / 671,251 4.8% / 3.8%
Python without torch 6,331,149 / 1,567,394 6,146,540 / 1,524,284 2.9% / 2.8%
Complete, torch built in 8,255,034 / 1,918,625 8,178,282 / 1,898,530 0.9% / 1.0%

The bytes came out of code and tables: functions laid out by signature and
body with redundant sets dropped (9 to 12 KB of each module after
Brotli); the regex crate about a third smaller (one matcher per input
type, lookbehind as right-to-left instructions, one search loop per input
type, Unicode property names and derived tables looked up instead of
built); case mapping without the standard library's conversion tables;
DateTimeFormat patterns, likely subtags, decompositions and time-zone
data packed tighter; emoji sequences front-coded. The torch package grew
from about 0.38 MB to 0.40 MB (2,156,516 bytes raw, 401,594 Brotli) with
the native kernels behind torch's speed, which the complete module carries
too; that is why it saves least.

WebAssembly speed

The browser engines interpret, in a checked profile with no unsafe. On
the size pass's corpus under Node 24 (24 cases, 10 warm-up pairs and 31
alternating paired samples, both orders), against 0.0.23's modules:

  • Lite runs it in 0.986 of the time and the JavaScript module in 0.995.
    regexp-unicode-compile takes 0.63 (Lite) and 0.90 of the time, and the
    error constructors 0.93 to 0.98.
  • No case is slower beyond noise. Lite's arith-int and arith-mod read
    1.11 and 1.12 in the full screen; run alone in fresh processes, three
    times in each order, they read 1.016 and 0.986.
  • torch in the complete module under Node: a small CNN's training run
    takes 20 to 22 ms warm where 0.0.23 took 37, and its first run 120 ms
    where 0.0.23 took 340.

Native CLI

The 13 JavaScript benchmark rows take 2.6% less time than in 0.0.23
(paired, 95% interval 1.9% to 3.0%), the headline ten 2.7% less, in an
ordinary, not profile-guided, release build, and every row prints the
same bytes. async-promise-chain takes 17% less time, json-large and
polymorphic-objects 5% less; map-set-heavy is 2.8% slower (0.7% to
6.8%). Against Node 24 the headline ten are at 0.88x of its time (0.87 to
0.89) and all 13 at 0.62x, and ZIPP's median is below Node's on all 13
rows, async-promise-chain (0.94x) and polymorphic-objects (0.98x)
included, the two behind Node in 0.0.23. Node's start is about 30 ms of
each of its times, ZIPP's 7.

  • More code runs compiled. Loops holding a for-of, a destructuring,
    a finally, literals, closures, instanceof, a typeof switch, a
    template, a spread, a key list, a for-in or a pop, and functions
    with a rest parameter or that use arguments, ran on the interpreter;
    they compile now. Object spread runs in a sixth of 0.0.23's time,
    instanceof in under a third, a destructuring-heavy program in half.
  • Construction. A constructor that only stores its arguments builds
    its object the way a literal is built, and a compiled loop calls a
    compiled function without a frame: two million new Vec(i, 1) take 100
    ms where they took 760 in 0.0.23 (Node, which removes the allocation:
    4), a class-based simulation 155 ms of process time where it took 530
    (Node: 65).
  • Statics. Object.assign, String.fromCharCode, the Promise
    combinators and the other Object statics run from compiled code, and
    Object.assign copies a plain source directly: a million
    Object.assign({}, src) take 140 ms where they took 830 in 0.0.23
    (Node: 18).
  • Promises. An await of a settled promise continues in place, a
    run of reactions calling one callback shares a register window, and an
    intrinsic Promise.all skips its unobservable steps.
  • Strings and keys. An integer becomes a string in one allocation,
    a plain replace does not copy its receiver, and a plain object's
    integer keys are read and written without spelling them.
  • The 43 broad probes (one pattern each, whole-process time) take
    0.73 of 0.0.23's time, leaving out array_push_pop, the pop defect,
    which took 915 s. The furthest from Node are arguments (7.6x),
    closure creation (4.4x), object spread and spread calls (3.3x to 3.4x)
    and Map/Set (2.8x to 3.0x).

Python

The native CLI runs the 36 programs of the Python benchmark suite that
CPython also runs at 1.09x of CPython 3.13's in-script time, where 0.0.23
took 1.31x in the same run (0.0.22: 1.79x): the 30 small programs at
1.00x, the six larger ones at 1.68x. The suite's 44 programs take 0.67x
of 0.0.23's time. Fifteen programs take less time than in CPython (eleven
in 0.0.23): range_loop, float_arith, tuple_swap, attr_read_write,
global_read, int_arith, dict_get, list_index, builtin_len,
str_long_len_index, json_roundtrip, tuple_unpack, hash_str,
list_append and for_list. The slowest against CPython are
str_methods (3.4x), generator_iter (2.8x), try_raise_catch (2.7x)
and instance_new (2.6x).

A store-only __init__ runs without its frame; method lookups and
construction sites answer inline for the class they last saw; sort keys
of one form sort by columns; a subscript, get or len reads a list's,
tuple's or dict's record once; raise compiles to a helper of its own;
loops are planned only where the header dominates the back edge; and
super().m() is cheaper. The 195 programs of tests/python_corpus print
what CPython 3.13 prints, except the five that need a GPU or a
transport.

torch

Over eight training and tensor programs on one thread, warm, ZIPP takes
4.2x PyTorch 2.11's time, where 0.0.23 took 17.0x; a program's first run
is faster than PyTorch's on cnn, mlp, rnn and transformer
(PyTorch's first call pays its own start-up). Random numbers are drawn
natively, small optimizer updates, broadcast-gradient sums, ReLU's
gradient and gradient accumulation run as one kernel each, add, sub, mul,
div and the common matrix products are one engine call, nn modules are
lighter to build, and attention masks are built once per size. The speed
changes leave every value bit-identical; the value changes are the fixes
above.

Validation

Test262 passes with the same nine documented expected failures as before
(95,671 of 95,680 executions). The regression tests for #32 to #34 and
#41 to #45 are crates/zipp-vm/tests/reported_property_bugs.rs and
reported_semantics_bugs.rs. 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.23, 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.24;
  2. refreshes landing/public/playground-runtime from the
    zipp-wasm-0.0.24-web-python asset (sync-playground.mjs --refresh-engine)
    and landing/public/wasm from zipp-wasm-0.0.24-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.23...v0.0.24