Repository navigation
ZIPP v0.0.24
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 withoutIntlandTemporal, 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 fromweb-torchwhen 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.wasmand itszipp_torch.jsloader, added toweb-python-basethroughaddPythonPackage.SHA256SUMSfor 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)andObject.hasOwn(ta, 0)gave
false. - A function's
prototypemade non-writable stayed writable (#33),
throughObject.definePropertyorObject.freeze. obj.hasOwnProperty(k)ran the built-in for any receiver (#34), as
didpropertyIsEnumerableandisPrototypeOf: 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): afterReflect.setPrototypeOf(fn, C),fn.smissed
C's static. Beside it,Object.create(map).sizeanswered the map's
size where a TypeError is due, an ownsizeand a subclass's
get size()were ignored, and a getter inherited by a Map, Set, Date or
Promise ran with the wrongthis. typeof nameinsidewithignored thewithobject (#42).- A replaced
consolemethod was never called (#43):console.log,
info,warn,erroranddebugwere compiled to a print. - There was no
performanceobject (#44), thoughperformance.now()
worked; it now exists, withnow()andtimeOrigin. - Constructing a derived class read
newTarget.prototypeitself
(#45), which a Proxy newTarget could observe; the base that creates the
object reads it, whensuper()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 = ndeleted the elements pastn, 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/throwwas ignored, acrossfor-of, spread,
yield*andfor await. { ...src }ran setters and could replace the prototype: object
spread now defines the copied properties.- A Proxy's
ownKeystrap ran twice perfor-inlevel; Node runs it
once. toLocaleLowerCasein Turkish, Azerbaijani and Lithuanian gave the
medial sigma at the end of a word ("ΑΣ"to"ασ"; Node:"ας").- Python: a zero-argument
super()in a method whoseselfa 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_anduniform_did not follow
PyTorch's random stream or rounding; float64randlost precision; GRU's
hidden update used another formula;x**3,x**-2,x**0.5and
x**-0.5differed 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_anduniform_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
performanceis a global object withnow()and a read-only
timeOrigin, andconsole's methods are read at each call, so a
program or a host can replace either;console.log = console.error
writes to stderr.performance.nowdoes not check its receiver.- A
for-inover a Proxy calls its traps in V8's order, and the
instanceofTypeError 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 afor-inor a
for-of, or hold afinally; functions with a rest parameter or that
usearguments; code callingObject.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
PythonZIPP_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-compiletakes 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-intandarith-modread
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,
afinally, literals, closures,instanceof, atypeofswitch, a
template, a spread, a key list, afor-inor apop, and functions
with a rest parameter or that usearguments, ran on the interpreter;
they compile now. Object spread runs in a sixth of 0.0.23's time,
instanceofin 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 millionnew 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 otherObjectstatics run from compiled code, and
Object.assigncopies a plain source directly: a million
Object.assign({}, src)take 140 ms where they took 830 in 0.0.23
(Node: 18). - Promises. An
awaitof a settled promise continues in place, a
run of reactions calling one callback shares a register window, and an
intrinsicPromise.allskips its unobservable steps. - Strings and keys. An integer becomes a string in one allocation,
a plainreplacedoes 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 outarray_push_pop, thepopdefect,
which took 915 s. The furthest from Node arearguments(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:
- sets
zippEngineReleaseinlanding/package.jsonto0.0.24; - refreshes
landing/public/playground-runtimefrom the
zipp-wasm-0.0.24-web-pythonasset (sync-playground.mjs --refresh-engine)
andlanding/public/wasmfromzipp-wasm-0.0.24-web, both SHA256SUMS-verified; - updates the version, size and SHA-256 that
landing/src/App.tsxand
landing/README.mdstate about the bundled engine.
Full Changelog: v0.0.23...v0.0.24