github PerryTS/perry v0.5.1664

4 hours ago

Highlights

This is the first release since v0.5.1520 (2026-09-11). The per-PR entries follow under "All changes". Numbers below come from the linked fragments or from measured results; PRs without a fragment are cited by number only.

What is new in this release

Faster, smaller programs. On the js-compiled benchmark set, all 22 programs compile and print the same output as node, with this release and with v0.5.1520. Median run time, v0.5.1520 to this release: nbody 9368 to 364 ms, matmul 27916 to 182 ms, generators 20724 to 394 ms, array-sort 3300 to 102 ms, crc32 2804 to 524 ms. Compiled binaries shrank from about 17 MB to 6.5 MB, and hello world starts in 0.9 ms instead of 2.6 ms. No perry benchmark in that set is more than 10% slower than before (the worst is mandelbrot at +7%).

Startup. Small programs start faster: the runtime builds its object layouts when the first such object is born, maps its fixed caches on first use, and builds per-thread state in place. On macOS arm64, hello world takes 2.33 ms instead of 2.95 ms and uses 5.3 MB instead of 6.0 MB peak RSS (#13345); a further 57,000 instructions (8.5% of hello on x86-64) were removed (#13484), and Linux no longer parses /proc/self/maps for the stack bounds (#13456).

Memory. Large strings, arrays and objects are allocated in a young large-object space. Measured in the PRs: string building -22% RSS and JSON parsing -25% instructions (#13479), file reading -39% RSS and -74% instructions (#13463), and a second file-reading pass with -60% instructions and -27% RSS (#13392).

Whole-program analysis that scales. The compiler's analysis of what the program does (used to remove checks and resolve calls at compile time) now works per module, with contracts between modules. It is the default (#13132) and finishes on large programs where the whole-program version did not: fastify, the TypeScript compiler, and opencode (6,877 modules, 31 GiB for the whole compile, #13355). It proves more read and call sites than before (tsc +4.8%, TypeScript compiler sources +6.9%, Effect schema +13.5%, #13297).

WebAssembly and WASI. perry compile --target wasip1 produces a core module that imports only wasi_snapshot_preview1 and runs on plain preview 1 runtimes (#13343). It supports inbound TCP, signals and stdin (#13376), perry-stdlib and the Node module crates (#13356), and optional outbound networking (PERRY_WASM_NET=1, #13386) and page discard (PERRY_WASM_DISCARD=1, #13417). --target wasi produces a WASI 0.2 component; the native component host runs WASI 0.2 and 0.3 and no longer depends on tokio (#13189, #13364). Installed releases ship both the wasm32-wasip1 and wasm32-wasip2 runtime archives (#13394). Compiling for WASI needs LLVM 22.1.5 for native WebAssembly exception handling.

Real programs. The scoreboard of 43 real programs matches node on every row on this release (checked 2026-10-31), including zod 4 and three.js math, whose compiles no longer run out of memory or loop in the per-module analysis (#13579, #13608). fastify, zod, the TypeScript compiler, Effect and Next.js 16 compile and run; fastify handles request bodies through req.pipe (#13275), and zod 4.4.3 no longer miscompiles generateFastpass (#13270). Programs no longer abort at startup on Linux kernels built without transparent huge pages (#13309). node:trace_events namespace imports work (#13607), and the Windows, dns, net and zlib modules received further fixes listed under "All changes".

Upgrade notes and breaking changes

Native npm bindings were removed; the real package is compiled from source. If your project imports one of these, the package must now be installed in node_modules (no perry.compilePackages entry is needed). The old bindings were hand-written Rust copies that differed from upstream, for example jsonwebtoken.verify accepting forged tokens, decimal.js division returning 1, and commander never running .action() callbacks.

Native GC roots (statepoints) are the default (#7366). PERRY_RS4GC=1 is no longer needed; PERRY_RS4GC=0 goes back to the shadow stack on targets that support it. Targets whose frames the runtime cannot walk keep the shadow stack.

The GC call-effect tables are no longer compiled into the compiler. perry reads a .gc-effects sidecar that ships beside the prebuilt runtime archive and checks it against the archive's SHA-256 on every compile (#12735, #12660, #12674). A workspace build with a missing or stale sidecar stops with an error naming scripts/gc_call_effects/sidecars.sh <dir>. An installed perry without a sidecar compiles with every runtime call treated as a safepoint and prints one warning. After building the runtime, run that script on the output directory.

No runtime rebuild at compile time. perry links the prebuilt runtime and no longer runs cargo during a compile (#12684, #12694, #12701, #12704). An installed perry links the prebuilt full-feature stdlib and no longer pulls every optional subsystem into every program (#11598).

Environment variables.

  • Removed: PERRY_GC_ZEAL and PERRY_GC_ZEAL_ALLOC_KB; use PERRY_GC_SCHEDULE_SEED, PERRY_GC_SCHEDULE_RATE=1 and PERRY_GC_SCHEDULE_ALLOC_KB (#7741). Removed: PERRY_LL_PREOPT_OPTNONE_INSTRS (#8583).
  • Changed defaults: the 16 MB nursery cap and the evacuating scavenge are on by default (PERRY_GC_SCAVENGE=0 turns both off). The fragment measures peak RSS about 69% lower on the gc_ratchet probes (#7377). Old-page defragmentation is opt-in (PERRY_GC_OLD_DEFRAG=1, #7922).
  • New: PERRY_REQUIRE_FAITHFUL_BINDINGS=1 makes preferring a partial bundled binding over an installed node_modules copy a hard error; otherwise it prints a note per package. perry.compilePackages may be "auto", "all" or true, and auto-compile is the default when it is omitted (#7137).

Compiler behavior.

  • Native code is always built with -O3. Perry no longer drops a large module to -Os/-O0 or marks a function optnone (#8421).
  • Toolchain output (Cargo, clang, Swift, linker) is hidden unless it fails or you pass --verbose (#8421).
  • The default number of concurrent codegen-unit workers is half the logical CPUs, clamped to 2–8, on non-Windows (#8593). Large bundles use more memory at once.
  • RegExp runs only on perry's own engine (Perex), with no regex/fancy-regex/regress fallback (#10142). The v flag is complete (#10372).

JavaScript behavior that changed to match Node.

  • Error subclasses no longer have an own, enumerable name. It stays on the prototype until you assign it, which changes JSON.stringify, Object.keys, spread and util.inspect output (#9511).
  • Native handles (Headers, Blob, Request/Response and similar) are objects in every coercion again (#12540).
  • Fractional and very large TypeScript numeric enum values are kept as JavaScript numbers (#12788).
  • instanceof, class method calls and super follow the live prototype chain, including after setPrototypeOf (#12409, #12466, #12784).

Rust and FFI embedders.

  • perry-ffi is published to crates.io, and its ABI changed (#8122).
  • perry-ffi 0.6 is breaking: one typed JS body ABI body(callee, this, a0, …), alloc_closure takes a js_function_info!, register_closure_arity is removed (#11637).
  • tokio is gone from the workspace. spawn_async and spawn_blocking_with_reactor are retired, and turnloop is the only event loop (#11402).
  • The ObjectHeader shrank from 56 to 48 bytes for a two-field object (#8122), and the perry-ext-streams crate was removed; Web Streams stay in perry-stdlib (#12439).
  • UI: setPadding(widget, top, left, bottom, right) is the canonical call; widgetSetEdgeInsets stays as a deprecated alias (#9962).
  • Contributors: CI pins the Rust toolchain to nightly-2026-08-20 (#8550).

Object model

Objects, classes and prototypes use compiler-owned layouts. Property reads, method calls and stores are resolved from the layout, and the older inline caches, memo tables and certificates were deleted (#12703). Related PRs:

  • builtin and node prototypes are sealed layouts (#12675, #12690, #12708, #12716) and stream/EventEmitter instances have static layouts (#12689, #12721)
  • class layout families resolve across the import graph (#12782) and across modules for subclasses (#12800)
  • namespaces, literal builders and CommonJS export calls use nominated layouts (#12802), and constructors with many stores compile smaller (#12804)
  • inherited reads, in and stores work on prototype chains of any length (#12698)
  • super calls follow live constructor prototypes (#12784) and native constructors keep own properties on sealed layouts (#12801)

Dynamic code (eval, new Function, dynamic require) is a single summary node over escaped values rather than disabling analysis for the whole program (#12707).

Garbage collector

  • Statepoints are the default root mechanism (above). The runtime call-effect tables moved out of the compiler into per-archive sidecars (#12660, #12674, #12735), and prod-profile archives prove 27 more call-effect rows (#12754).
  • Dropping runtime frame pointers in favor of an unwind-table Error.stack is measured at 2.5–3.8% faster on every row (#12706, #12714).
  • A moving-GC root-reload bug was fixed (#12712), and many rooting fixes are in the per-PR list. A static root-dominance checker gates the rooting changes.
  • Windows and macOS have their own effect tables, and runtime changes are checked on those targets before they land.

Compile speed and memory

  • Parallel code generation takes the Next.js compile from 19 to 5 minutes (#12711).
  • A front-end prefetch (#12705) moved the tsc front end from 43.9 s to 35.9 s with byte-identical objects (#12728).
  • The whole-program analysis is computed faster (#12686). The per-module object cache and the completed-build cache share one compile-environment identity (#12582), and cached units are checkpointed atomically (#9997).
  • Compile memory is released across phase boundaries (#12813), class analysis is bounded by graph distance (#12814), and lowering inputs and function source text are released or shared instead of cloned (#12916).
  • Compiling opencode now fits in about 34 GB, down from 140 GB: one walk per IIFE body and one shared type-alias map (#12890).
  • Large links go through a response file on every host, so very big programs no longer hit command-line length limits (#12913).
  • Compile wall time and peak RSS measured on 2026-10-17: tsc 286 s / 9.7 GB, effect 31 s / 3.8 GB, schema-record 30 s / 3.9 GB. perry compile --report-size writes a per-crate and per-symbol size report (#8576).

Node and ecosystem compatibility

  • opencode compiles and runs again (opencode --help prints the same as the bun build): compile memory (#12890), inline LLVM metadata in split modules (#13598), Effect tagged errors and extends Error across modules (#12897, #12898, #12899), class-expression instances (#12919), and the macOS link (#12913).

  • Subclasses of EventEmitter, streams, Map/Set and Promise run their overrides and methods correctly, through typed and untyped receivers and through await (#12815, #12888, #12891).

  • zod and marked can be used in the same program (#12882). { ...a, ...b } where b came from { ...undefined } no longer crashes, which marked.parse hit (#12843).

  • commander programs run 14.7% faster (#12894).

  • instanceof is correct for subclasses of classes returned from IIFEs or factory calls, including namespace-imported ones (#12906). import G = Ns.Member now declares G (#12849).

  • A macOS duplicate-symbol link error is fixed, as is a regex literal combined with worker_threads (#12887).

  • perry compile no longer hangs in Collecting modules... on pnpm workspaces with symlink cycles (#12895).

  • New parity fixes: net.SocketAddress and BlockList input validation, domain, async_hooks (AsyncLocalStorage.disable, AsyncResource receivers), events.on close handling, SubtleCrypto static calls, outgoing HTTP header values, and worker_threads error diagnostics. See the per-PR list.

  • Next.js 16 App Route serving is byte-identical to node (#12673, #12678, #12692) and is tracked by a pinned 16.3.x production fixture (#10007).

  • Namespaces, including enum + namespace merging, compile with direct calls (#12775, #12802).

  • events, stream, http (IncomingMessage, ServerResponse), worker_threads (no multi-second stalls, #12448; terminate() stops any loop, #12762), perf_hooks, dgram, WebSocket upgrade (#12460) and node:test mocking (#12747) received parity fixes. See the per-PR list.

  • Bun: Bun.Transpiler/Bun.build subsets (#9602), bun:sqlite (#8525), SQL (#9634), zero-copy bun:ffi views (#8329).

  • On the 2026-10-31 check, all 43 scoreboard programs printed the same output as node. On the 2026-10-17 measurement, rows above 2× node by instructions were: fastify, schema-record, qs, zod, commander, effect, property-prototype. tsc is 1.72×; the rest are under 1.8×.

Platforms

  • Security: the TLS root store and webpki are updated to reject the compromised eMudhra certificate chains (#12930).

  • The release pipeline builds and publishes on all 8 platforms, and every build and publish job checks that it is on the exact commit being released.

  • Linux musl x86_64/aarch64 release packages build again (#9584, #9298, #12811). Android UI apps link perry-stdlib (#11290, #12768).

  • macOS UI builds produce signed .app bundles (#10240). Windows gets fork() IPC over named pipes (#8318), libuv-shaped errnos and node:net err.errno (#10385, #10403), file:// drive letters (#12545) and fs.fchown parity (#12525). visionOS heap guards were fixed (#12247).

  • perry compile --target wasi builds a WASI 0.2 component and --target wasip1 a preview 1 module (see above). Target detection covers OpenHarmony aliases (#12400).

Known gaps

  • Promise/timer ordering differs from node in one case that is not yet root-caused.
  • Typed arrays larger than 1 GiB are not supported, and allocation-exhaustion behavior differs from node.
  • On the competitive suite at 2026-10-19, 218 of 242 rows pass, with no regressions against the previous release.
  • console.log of a string containing ' keeps single quotes (node switches to double quotes), and a RegExp.prototype.exec result prints without its index, input and groups properties.
  • Binary size: the zod build is about 1.1 MB (+11%) larger text after the Symbol.hasInstance/super fix (#12784).

All changes

Per-unit facts reach their fixpoint when two units pass each other's closures (zod v4's compiled validators): a unit forwarding another unit's function now names the functions described inside it whose owners publish a contract, instead of copying their descriptions whole. Before, the descriptions nested one level deeper every round and d_zod4's facts pass ran out of memory (40 GB in 81 s).

import * as traceEvents from "node:trace_events" now exposes createTracing
and getEnabledCategories on the namespace object. The ESM namespace module
forwards the API manifest's public names, and node:trace_events had no
manifest surface, so the namespace held only default and
traceEvents.createTracing(...) threw "createTracing is not a function" (native
and WASI; it failed the WASI process_id acceptance test). The manifest now
declares the module and its two exports, and a unit test checks that every
export the runtime implements for a Node builtin submodule is forwarded by its
ESM namespace.

Fix split-module native construction of indexed-owner loop regions. The
native IR reader accepted only the !{!"NAME"} metadata operand of
llvm.read_register, so a function whose typed-array owner region declares
its noalias scopes (llvm.experimental.noalias.scope.decl) failed with
"unsupported metadata operand" and the whole build refused to link
(opencode's @aws-sdk/core protocols module). The reader now builds any
inline metadata tuple of strings and tuples through the uniquing MDNode API,
on both the line and typed call paths, and attaches inline !alias.scope /
!noalias tuples on loads and stores, which it used to drop. Comma and
bracket splitting skips quoted strings, since scope domains embed function
names. A reader test holds the result equal to LLVM's own parse, and the
indexed-owner emitter test builds its live output through the reader.

Fix the wasm32 (WASI) runtime build: the compile-time check of the
PerryLayoutKey.off offset used the 64-bit value (14) on every target, but
with 32-bit pointers the field sits at byte 10, so perry-runtime failed to
compile for wasm32-wasip1/wasip2 and the release build-wasi leg went red. The
offset and the key size are now stated per pointer width (14/16 on LP64,
10/12 on ILP32), matching the descriptor constants beside them, and a codegen
test checks the emitted %PerryLayoutKey LLVM type has the same offset and
size under both data layouts. Native targets are unchanged.

The release wasip1 step now admits perry_host.discard_pages in its import
check (--allow-perry-host, as the test workflow already does): the one
wasip1 archive has carried the opt-in page-discard member since it landed,
so the check rejected the archive it was meant to ship.

CreateDataProperty (public class fields, and the other runtime callers of
the operation) defines from a Property Descriptor record instead of building a
four-key JS descriptor object and decoding it back through
Reflect.defineProperty. A class field on a callable class instance (a bound
function given the class prototype, as ArkType's Callable builds) declines
every shape-owned define and paid both steps: 24k instructions per field,
1.57 G instructions of the Supabase CLI's --version. It is now about 10k.
Because nothing is read back through %Object.prototype%, an inherited get
or set there no longer turns a class field into an invalid accessor
descriptor; test_gap_class_field_define_record pins that and the field
attributes on callable and plain receivers.

Register PERRY_FACTS_CONSUMED (the codegen-consumed marks of the facts
dump) with the compile environment as a diagnostics-only variable, like
PERRY_FACTS_DUMP: it records which sites read a program fact and changes
no emitted code, so it is not a build-cache input. Fixes the environment
audit test on main.

PERRY_LINK_PAD, a measurement input for startup A/Bs: whitespace-separated objects placed on the link
line right after the compiled objects and before the runtime archive, on every final link perry runs
(executable, dylib, proven tiny program, WASI component), through one reader, link::link_pad_args().
PERRY_EXTRA_LINK_ARGS keeps its place (appended last) and meaning. Unset, the link line is unchanged.

The startup A/B harness's link-pad control (scripts/a1/linkpad.py, scripts/a1/irdiff.sh) now pads in
front of the runtime. The linker lays input sections out in command-line order, so a pad between the
compiled objects and the runtime archive keeps every runtime byte of .text, .rodata (including the
merged .rodata.cst*/str1.1 blobs), .eh_frame (one filler FDE), .gcc_except_table, .data.rel.ro
(anchored at the end of the RELRO region), .data and .bss at the same address in both arms; the pad
that is linked last remains only to equalize section ends and the heap start when the runtime itself
differs. The planner relinks the cached objects until the markers coincide; the proof
(<out>/<witness>.layout.txt) compares markers, section ends, segment starts, the image end and the
addresses of every runtime symbol both binaries define. Rows that were left by the previous control (ld.so
check_match over runtime .rodata names, libc compares over runtime literals) now disappear when the
runtime is unchanged. perry_* record sections and .eh_frame_hdr stay unpadded; both arms must be built
from a perry that has PERRY_LINK_PAD.

The stack-overflow guard now works when the stack limit is unlimited (ulimit -s unlimited): runaway recursion throws a catchable RangeError: Maximum call stack size exceeded instead of dying with SIGSEGV. The main-thread stack bound is derived from the kernel's own stack mapping, and an unlimited (or address-space-wide) limit uses the kernel's default 8 MiB stack size.

Per-unit facts reach their fixpoint when several units forward copies of one literal shape that no unit of the module births: the copies join, ascending from the last round, instead of the last forwarder of a round replacing the others. Before, a reader solved between two forwarders never read what the round left and was solved again every round (the Supabase CLI never finished its facts pass).

Per-unit facts: a method called on what a described parameter reaches
(context.factory.createIdentifier(..), where the callers' values sit beside
the parameter's marker: a closed function, a run method's parameter) is a
read of the method's key, not an escape of the receiver. The call runs on the
values beside the marker, whose contracts carry it to their units. Before,
the escape made every caller report what it passed, and the census broke the
passed class, its method and the method's result layout. A closed-function
fixture now converges in 2 rounds instead of 4 with no broken layout; the
whole program is unchanged and per unit never proves more than whole.

Facts dump (PERRY_FACTS_DUMP only): ledger follow-ups. Every unknown value contract names the first reason it is unknown and the unit that published it (why on the value-output, argument-output and contract-input records; the reason takes no part in contract equality or hashing). The wrapper offsets of CommonJS modules are kept when the dump is on (W records), so the ledger maps wrapped-coordinate sites back to source. The per-unit census rounds capture no diagnostics: each unit is solved once more at the fixpoint, level by level as a round would, and its records stream to <dump>.diag (appended to the dump) instead of being held every round, so a program of tens of thousands of units fits. PERRY_FACTS_CONSUMED=<file> (full compile) lists the sites codegen read a program fact for. A ⊤ whose chain has no recorded seed names where it broke. With the dump off nothing changes: native images and fact fingerprints are unchanged.

Raw native registry ids can no longer be mistaken for heap receivers by the named-get probes — and that is now pinned by a test. The production half of this fix is already in main: the native_get raw-word arm classifies the handle band before it reads anything (native_get.rs), and js_object_get_field_by_name reaches named dispatch for those ids. What was missing is the witness, which is why the originating PR stayed open. The new test asserts that a raw id — 1, the fetch band start, HANDLE_BAND_MAX - 1, and the same ids passed through an honest Handle tag — cannot prove an ordinary data slot (try_data_get_by_name answers None) and that js_object_get_field_by_name answers undefined for them, while an ordinary object still reads its key through both paths. It passes against main unchanged, which is the evidence that nothing was left to port.

Per-unit facts: a store into another unit's literal reached through a result
(get().p = o, then a third unit writes what b() reads back) no longer
leaves the stored object's field read-only per unit while the whole program
has it written. Two gaps made the reader read ⊤ (or a stale null) for a
field its owner had described, so the reader's store reached no object:

  • an owner's literal layout named only by its value contracts was never
    published under its display, so the only entry readers took
    (Gen::other_layout, DECISIONS 106) was a forwarder's copy from an earlier
    round, which the forwarder read back and forwarded again. The owner now
    publishes it, described, as DECISIONS 132 does for classes;
  • joining two descriptions of one layout (ClassContract::join, across
    rounds and owners, and widen_stand_in inside one solve) turned a field
    the two describe differently into an unknown value. The join now keeps
    what each side says (plain facts and field functions as a value contract),
    and the reader builds every description it is given.

Red on main in three forms (a direct store, a DECISIONS 148 method parameter,
a decision-13 closed parameter); tests in unit_mode/shared_values.rs. Per
unit also proves more where the join made fields ⊤: the CommonJS fixtures
prove all whole-program sites (22/22, 28/28; were 7 and 13).

On x86_64 Linux, creating an Error no longer costs time proportional to the number of functions in the program. Each captured frame was classified as JS or runtime by a linear scan over every function's metadata (BundleFunctionNames::contains_key), so a large program paid that scan for every frame of every Error: the Supabase CLI's --version spent 43.2 G of its 52.5 G instructions capturing the stacks of 636 Errors that its libraries create while loading.

Codegen now places every body it publishes a named function record for in its own output section, perry_js_text, and the capture tells a JS frame by whether its code lies between the linker's __start_perry_js_text/__stop_perry_js_text: two compares per frame, decided by where the code lives, with no lookup structure. The captured frames are the same as before: only named JS bodies are in the section, and only they count against Error.stackTraceLimit.

The link passes -z start-stop-gc, so referencing the section's bounds keeps no function alive that --gc-sections would otherwise drop. With --function-order, ranked JS bodies take their rank inside the JS text section (perry_js_text.sorted.<rank>), which the generated linker script gathers at its front; the hot run is now two runs, the runtime's at the front of .text and the JS one at the front of perry_js_text, and both are huge-page backed. Other targets are unchanged: their frame-pointer walk already skips runtime frames.

A shape Region can no longer settle on a reservation too small to hold the native birth layouts. Region::new halves its reservation when the address space is limited, and its floor was 2 * DYNAMIC_BASE_ROWS — the process-wide chunk space, which ends before the rows agent creation actually seeds: WEAKMAP_BIRTH_LAYOUT and WEAKSET_BIRTH_LAYOUT sit past it. Under a constrained address space the Region could accept 4,194,304 rows against a native page ending at 4,227,072, leaving the agent unable to publish the layouts it needs before any user code runs. The floor is now the larger of the chunk-space requirement and that page, rounded up to a whole directory page, and the retry loop ends by trying the exact floor rather than assuming a power-of-two descent lands on it. Extracting the loop into reserve_with makes the halving testable with an injected allocation failure: one test drives a world where only the native page fits and asserts the floor lands there, the other asserts that a world where nothing that small succeeds fails loudly instead of running with a Region that cannot hold the birth layouts.

V8 heap snapshots now include the image strings they can reach. The snapshot enumerates live objects by walking the nursery and old-gen allocators, and image strings — real GC-headed GC_TYPE_STRING cells marked STRING_IMAGE_MARKER — live in linker-owned memory that neither walk can enumerate. Live literal values and every edge pointing at them were silently missing. A snapshot-local pass now follows the traced slots of the nodes already collected and admits tracked string images into the snapshot's own node index, before labels and edges are assigned, so a string referenced twice becomes one node with an edge per reference. No runtime registration or new root scanner is added, and the cells are read, never rewritten. The regression asserts exactly one node for a string shared by two array slots and at least two edges to it; with the pass removed the string is absent (left: 0, right: 1).

Array.prototype.toSorted returned a stale pointer whenever its comparator allocated. Both entry points captured their arguments before the clone allocation, so a moving minor — inside js_array_alloc, or inside the sort's callback into JS — left them reading and returning forwarded pointers; with the common (a, b) => a.localeCompare(b) comparator the returned array was dead. They now root their inputs with RuntimeHandleScope and take the relocated pointer back from across_const, and they return the sort helper's live result instead of the pointer captured before the sort. The regression runs both paths under forced evacuation with a comparator that calls gc_collect_minor(), asserts the source actually moved, and validates that the returned address is a live tracked GC_TYPE_ARRAY that is not itself forwarded before comparing payload values — a forwarding stub can make a generic read look correct while leaking the stale return. Without the rooting that liveness assertion fails; with it, both sorts return their sorted copy.

Prevent main-thread direct messages from cloning payloads or detaching transfer buffers when the destination is missing or stopped. Preserve worker-origin forwarding order and release sender ownership when cloning throws.

The handle-band rejection in default_object_prototype_for_owner now has a regression test. That function reads a receiver's object header, so it is only valid for a heap address; 9ae3fec0a1 added the guard that proves it (is_plausible_heap_addr, i.e. !is_handle_band(addr) && is_valid_obj_ptr(addr)) and landed without a test, because no input was known to reach it with a handle id. The new test asserts that every small-handle id (0, 1, the common registry end, the fetch and proxy and stream band starts, and HANDLE_BAND_MAX - 1) is rejected before the read, and that an ordinary object still resolves the default prototype. With the guard removed the test does not merely mismatch — the process aborts (SIGABRT) on the id-band read, which is what makes it a real witness.

Symbol.for no longer dereferences raw bits on magnitude alone. The raw-StringHeader arm accepted the address if is_plausible_heap_addr passed, and a subnormal double's bits are heap-plausible: Symbol.for(1e-310) read a StringHeader out of unowned memory and registered whatever it found as the key, instead of applying ToString. The arm now also requires try_read_tracked_gc_header_of_type(bits, GC_TYPE_STRING) — evidence that the address is an owned allocation of kind string — and everything else falls through to js_string_coerce, the full ToString. Regression coverage asserts the fixture is both heap-plausible and unowned before checking Symbol.for(n) === Symbol.for(String(n)), and keeps the legacy raw-pointer callsite working (its raw and boxed forms resolve to the same symbol). Without the guard the first test aborts the process rather than mismatching, which is the point: the read is of memory the runtime does not own.

The FNMT fallback root certificate was truncated by one byte, so the fallback provided no FNMT trust anchor at all. The first entry of FALLBACK_ROOT_CERTS — used on hosts whose system store has no RAIZ FNMT-RCM — decoded to a DER of 1322 bytes instead of 1323, which every TLS stack rejects. Nothing caught it because the suite asserted only the PEM's shape (armour present, no test-CA names), and a mangled base64 body satisfies that. A new test parses every embedded fallback root as X.509 DER and fails on the offending index with the parser's reason: against the truncated body it reports expected 1323, actual 1322 at DER byte 1322.

Changed

  • The full-corpus test262 run compiles module cases and supplies a minimal $262 host (gc, detachArrayBuffer), so 1,681 previously skipped cases are judged. The full-corpus baseline moves to 91.6% (48814/53293).

Every Rust file over the 2000-line cap is now under it, because each was split along a responsibility rather than trimmed to fit. scripts/check_file_size.sh (a lint step) was red on nine files between 2002 and 2264 lines. Each is now a module that owns one job plus the modules for the jobs it was mixing: block.rs (2002 → 1609) keeps only the IR emitter, with FpFlags, EhScope and RegCounter in block/; node_stream_event_emitter.rs (2002 → 1772) keeps the emitter surface, with event delivery in emit.rs; canonical_keys.rs (2264 → 1872) keeps the trie, with its views, census and record types in views.rs/stats.rs/records.rs; layout_desc.rs (2031 → 1897) keeps the perry_layouts section, with the scattered perry_atoms code in atoms.rs; gen_rest.rs (2015 → 1951) keeps the exhaustive 124-arm match, with the model-node builders it calls in nodes.rs; and create_families.rs (2028 → 1070) keeps fact collection, with its records, site derivation, emission and proto-export registry in models.rs/site.rs/emission.rs/proto_exports.rs.

Two couplings the splits exposed are fixed rather than papered over: LlBlock had been reaching into RegCounter's private byte_owner_root_slots (once with borrow_mut), so those operations now live on the type; and node_stream_event_emitter.rs is included as crate::node_stream::event_emitter, so its super:: meant node_stream — extracting a child module silently repoints every super::X, which is why fifteen paths inside emit.rs say super::super::X. The rule this work follows, and the per-file inventory, are written down in odd/tasks/file-size-decomposition.md. No behaviour changed: perry-codegen's lib suite is 2622 passed / 13 failed / 1 ignored on this branch and on main, with the failing test names set-identical, and the moved tests pass in every touched crate.

Fix worker message transfer-list normalization for direct and options-based iterables. Preserve iterator errors and transactional ArrayBuffer detachment, keep Worker constructor admission separate, and ignore BroadcastChannel's extra argument.

Report short-string Array.from mapper arguments as strings, matching heap strings instead of misclassifying their inline representation as numbers. The runtime regression compares the actual TypeError messages for empty, one-byte and five-byte mappers in both representations.

Facts dump (PERRY_FACTS_DUMP only): a complete per-site ledger input. The lowering writes <dump>.census, one line per source member read, element read, call, new, member write, update and delete with the HIR variant it lowered to, so folded and specialized sites keep a source identity. Member writes (PutValueSet, PropertySet, IndexSet, updates) carry dump-only source spans and get site records. Each analysis mode adds a <census> unit: a HIR walk per site kind compared with the recorded sites, and a record for every HIR site no analysis walked. Every open operand of a site names the first unknown value on its proof path from a closed list of causes (unit edge, escaped parameter/receiver/field, intrinsic prototype store, opaque builtin, computed key, module namespace, external input, dynamic code, ...), with its root node, root function and seed text; legacy verdict records name the obligation they failed. With the dump off nothing is captured: native images and fact fingerprints are unchanged.

A queued async-resource destroy now waits for the enclosing execution scopes to emit their after hook, and only for the resources those scopes actually own. A timer's callback checkpoint can run before that callback's own after, so an interval that cleared itself from inside its callback — or any collection that queued destruction during a nested scope — could deliver destroy for a resource whose before/after pair had not closed yet, and a nested scope could observe a resource destroyed while it was still executing. drain_gc_destroy_queue no longer drains the queue wholesale inside an active scope: it retains the queued entries whose serial is still on the execution stack (plus those whose async job domain is not eligible yet) and delivers the rest, so ready resources unrelated to the active scope are not held behind it. Regression coverage is the node-suite case test-parity/node-suite/async_hooks/hooks/interval-cancel-checkpoint-lifecycle.ts: the interval clears itself inside its own callback, and process.nextTick and queueMicrotask running inside that scope both observe the interval's destroy count still at 0; the lifecycle is exactly before>after>destroy, no resource destroys twice, and a sibling setTimeout cancelled in the same scope still destroys exactly once.

Register the one test file that existed on disk and ran in no registry.

check_test_registration.py reported DARK TEST test-files/test_gap_gc_numeric_callback_contract.ts. The file arrived with f9f0a9c8c0 (#13444, admitted closure capture/return ranges) and that commit touched neither test-parity/gc_repsel_corpus.txt nor the checker's exclusion list, so it was an oversight rather than a deliberate exclusion.

Strictly, the gate's headline overstates it: scripts/run_gap_tests.sh compiles every test-files/test_gap_*.ts, so the test was already running in the gap suite. What it was missing from is the second harness, the GC x representation-selection stress matrix, which is where a callback-contract test earns its keep. Registered there; gc-repsel-corpus goes from 124/125 to 125/125.

Verified, not assumed: the release build used to check it finished clean; the gap suite filtered to this test reports parity 100.0% with 0 skipped; and the repsel matrix, filtered and run with the pinned Node 26.12.1, reports PASS=1 UNVER=6 XFAIL=0 FAIL=0, exit 0. The matrix refused the first attempt outright with ORACLE MISMATCH: node 26.10.0 but .node-version pins 26.12.1, which is correct behaviour — a wrong oracle marks tests node_fail and drops them from the gate silently, so a green there would have meant nothing.

One measurement worth recording rather than hiding: this 53-line test does not allocate enough to trigger a copying minor or a relocation, so all six GC arms report UNVER (output matched, arm inert) for it at 8 MB pressure and still at 1 MB. Its cells in the matrix are therefore green for the weaker reason — output parity — and not because it exercised the collector. That is a property of the test's allocation volume, not of this registration, and the arms are made live by the rest of the corpus; it is recorded here so the next person does not over-read those cells.

Per-unit facts: new C() and new f() of a class or constructor function from another unit are no longer read as that class's instance when its constructor can return an object (return expr); the read stays unproven instead of describing the wrong value.

Report shape_descriptor_census drift as a verdict instead of a Python traceback.

The census detects drift by raising CensusError (and AssertionError from its sabotage tests), and main() let both escape. Every real detection therefore arrived as a stack trace with the finding buried in the middle, and — because the census stacks dozens of independent assertions — each run reported only the first one, so the layers behind it were both invisible and hard to reach. This is the gate at its least useful: it did detect the drift correctly, and nobody could read what it detected.

main() now returns an exit code and catches exactly CensusError and AssertionError, printing ::error::shape_descriptor_census: <finding> on one line. Anything else still propagates: an unexpected crash is a bug in the checker and must keep its traceback rather than be dressed up as a verdict. --emit-baseline is unchanged and still exits 0 with valid JSON.

This commit deliberately does not touch the census's data. The census is currently red for more than one reason at once: its baseline still records object_header_size_bytes, which the A1 object-header work replaced with object_slot_base_bytes and object_meta_slot_offset_bytes, and assert_authority_surfaces holds 77 assertions of which the first four have already been observed failing in sequence. Reconciling those is an audit of the census — each assertion has to be verified as still-preserving its guarantee, not merely made green — and it needs its own work unit. Making the failures readable is the prerequisite for doing it at all.

Verified in both directions. With the fix, the current red tree reports ::error::shape_descriptor_census: exact callsite census changed for raw_member_callsite_multiset; added=[], removed=[…] — one line, zero tracebacks, exit 1 — where it previously printed a CensusError traceback. Driven with a deliberately unexpected failure (ZeroDivisionError), the traceback is still printed, so the catch is not swallowing bugs.

Fixed

  • Read MessagePort and BroadcastChannel fields as property values instead of invoking them during lowering, preserving handler identity and receiver-bound calls without a method-name whitelist.

Drop the two GC_TYPE_OBJECT registrations that check_gc_header_constants was still requiring, and that no longer name a declaration.

The gate verifies that each registered compiler-side restatement of a GC header constant is declared in the file that registers it. Two entries claimed crates/perry-codegen/src/target_layout.rs and crates/perry-codegen/src/lower_call/method_override.rs each carried their own GC_TYPE_OBJECT. Neither does any more, and nothing is missing: crates/perry-codegen/src/lib.rs:67 declares pub(crate) use perry_abi as runtime_abi, so crate::runtime_abi::GC_TYPE_OBJECT is perry_abi::GC_TYPE_OBJECT (= 2, crates/perry-abi/src/lib.rs:305). The per-file copy was the thing being checked, and it is gone because the constant now has a single source — which is the outcome the check existed to push toward, so the entries retire rather than get re-pointed. method_override.rs also stopped comparing the gtype byte directly; it bakes GC_OBJECT_METHOD_GUARD_MASK_I32 (0x0800_80ff), whose mask still carries the gtype byte.

Verified in both directions. As committed the gate reports OK — 17 compiler-side header restatements agree with perry-runtime and its self-test runs and passes; before the change the self-test refused outright with self-test FAILED: the tree is already red, so the red tree was also blocking the checker's own coverage. Driven deliberately, re-registering one of the two constants fails with registered constant GC_TYPE_OBJECT no longer declared.

Nothing else moves: the seventeen surviving restatements, the seven declared constants they are compared against, and the C header agreement are untouched.

Fixed the perry-runtime unit tests, which stopped linking after named builtin imports landed. A function-info record declared through a macro from another crate got a linker name with different spacing from the one its importers used; record names are now spelled from the body path's identifier segments, so they no longer depend on which macro carried the declaration. The GC test fixtures now build closures with a function-info record, as the runtime always does, and debug builds assert that invariant where the native export checks read it.

Make the registry lifetime audit green again after six address-keyed registries were replaced with better mechanisms, and drop the five allowlist entries that outlived them.

python3 scripts/registry_lifetime_check.py was exiting 2, not 1: only 184 registries enumerated (floor 187). That is the gate's own parser-liveness assertion — it fires when the enumerator stops matching, so the failure said "do not trust this scan", not "here is a violation". It hid a second, ordinary defect behind it: five stale allowlist entries, which only surfaced once the floor stopped aborting the run.

The six registries are gone because the code got better, not because a regex broke. Diffing --list between e91af6efe6 (where the floor was last set, 190 enumerated) and today (184) shows exactly six removals and nothing added: EXPORT_SINGLETONS, NAMESPACE_SINGLETONS and DEFAULT_OBJECT_SINGLETONS in node_submodules/mod.rs (97e40e8826, replaced by per-submodule slots behind a single pointer), plus UNCLONEABLE_OBJECTS and UNTRANSFERABLE_OBJECTS (cc1da0e683, unified into one clone-marker mechanism) and ENVIRONMENT_DATA (bc1f7ec92e, inherited through one clone graph) in worker_threads.rs.

MIN_REGISTRIES therefore moves 187 -> 181, keeping the same three-entry parser-liveness margin the file's floor history establishes (181 = 184 - 3), with the six removals and their commits recorded beside it so the next reader can re-derive the number. MIN_REMOVING is deliberately untouched: 120 registries have a production removal path against a floor of 118, so lowering it would only weaken the gate.

The five allowlist entries removed are the three node_submodules tables and the two worker_threads clone-marker tables — the ones the gate named as no such registry lacks a removal path any more. ENVIRONMENT_DATA needed no entry because it never lacked a removal path. The allowlist goes from 69 to 64.

Verified in both directions. As committed the gate reports OK (184 registries; 120 with a production removal path; 64 allowlisted) and the self-test passes. Driven deliberately, both guards still fire: raising the floor above reality exits 2 with only 184 registries enumerated (floor 200), and re-adding one stale allowlist entry exits 1 with stale allowlist entry … EXPORT_SINGLETONS.

Per-unit facts now finish on esbuild's __commonJS helper when the unit that defines it also calls it, as oxfmt's bundled acorn does (ct = (e, t) => () => (t || e((t = { exports: {} }).exports, t), t.exports) assigned inside an init function). Before, the facts pass ran rounds forever and the compile never finished.

The cause was in a unit's contract check, not in a contract rule. When the check handed out a new closure context of a function that was already a contract function, it escaped that context but did not count it as a change. The run unknown code makes on that context (here the inner closure's call through e) was created and then never solved, so the unit stopped with work it had just added still pending. Whether that happened depended on the unit's own previous contract of ct: reading the sound contract (e called, t escapes), the function was already a contract function and the unit published the narrow one; reading the narrow one, it was not, so the change counted and the unit published the sound one again.

The check loop now solves again while the model holds unsolved work (the worklist, pending instances, marker links), whether or not a check site counted its change, so no site can stop a unit early by forgetting to report one. The test a_helper_reading_its_own_contract_back_reaches_the_sound_fixpoint times out on the old loop and converges in 3 rounds with t escaping.

On the corpus the proven site counts are unchanged (qs_parse 344/1768, commander 44/1175, fastify 1699/22842, tscwork 51608/122969, tssrc 20028/87017). The other facts change only in units that had stopped with work pending: 6 sites that read as dead now have receivers (still unproven), 4 sites change their reason, one ajv literal in fastify gains a written field gen and a second writer of data, and the rest are diagnostic records. Codegen acts on none of these.

Fixed

  • Keep Worker filename kinds distinct from eval source during compilation and validate the original value before data transfer, using one runtime entry selector for compiled and unresolved file paths.
  • Prepare Worker argv with Node-compatible conversion and observable option-read ordering, publishing one rooted child-local argv array without changing main-process arguments.

Make the thread-exit custody gate green again by reconciling scripts/thread_exit_address_globals.json with the collector and namespace work that moved under it, and by re-auditing the one contract that needed it (#11471).

Four independent defects, none of them a live bug:

  • Nine stale entries in gc/instruments.rs. #13443 moved those counters behind the crate-local Counter wrapper (static STEP_MAX_US: Counter = Counter::new()), so their declarations are no longer AtomicU64 scalars and rule 2 no longer enumerates them. The exemptions outlived their subject.
  • symbol.rs:NEXT_SYMBOL_ID — its entry is gone because the gate now discharges it mechanically: every mention is a load/fetch_add and nothing borrows it.
  • turnloop_pool/mod.rs CANCELLED and REFUSED — two pool lifetime totals, initialized to zero, written only by fetch_add(1) and read by load. They were not discharged mechanically because both names also appear as property-name identifiers inside the realm_decl! block for node:dns / node:dns/promises in object/namespace_create.rs (CANCELLED = "ECANCELLED"). Rule 2 is crate-wide and name-based on purpose, so it keeps them as manual candidates; the two no_heap_address verdicts are the review that asks for, and they record the collision so the next reader does not have to rediscover it.
  • arena/region.rs:REGIONS — the allocation_lifetime contract needed a real re-audit, not a re-hash. #13463 changed the registry's value from a bare end offset to BackingExtent { end, alignment } and #13463/#13479 made arena/young_large.rs a boundary caller by allocating large-object backings through region::map. The custody claim still holds: region::unmap removes the start key from REGIONS before any physical release, with a debug_assert_eq! on the extent length between the two, and young_large's release path (release_arena_block → region::unmap) keeps removal ahead of release as well. young_large.rs is now a pinned source, so the next change to it forces the same re-audit.

Verified in both directions. As committed the gate reports 1349 candidates, 174 counter-only, 478 classified by entry, 0 pending_fix group(s), 0 problem(s) and the self-test passes. Driven deliberately, each fix is load-bearing: re-inserting one stale entry fails with no such candidate static, dropping the new young_large.rs pin fails with unpinned allocation-lifetime boundary caller, and removing the CANCELLED verdict fails on the unclassified candidate.

Re-lock the native-handle ledger after two table removals, and drop the stale fn_metadata classifications that were failing the gate's own self-test.

scripts/native_handle_ledger.txt still budgeted three L1 tables to crates/perry-runtime/src/node_submodules/mod.rs, but 97e40e8826 replaced those three address-keyed maps (EXPORT_SINGLETONS, NAMESPACE_SINGLETONS, DEFAULT_OBJECT_SINGLETONS, each a RefCell<HashMap<_, *mut …Header>>) with per-submodule slots behind a single pointer. The file declares no handle table today, so the row was stale and the gate failed on it.

The same regeneration lowers crates/perry-stdlib/src/worker_threads.rs from 5 to 3. That reduction was checked against its commit rather than taken from --update: cc1da0e683 deleted UNTRANSFERABLE_OBJECTS and UNCLONEABLE_OBJECTS, two RefCell<HashSet<u64>> handle-id sets, which is exactly two L1 tables. Both ledger changes only lower ceilings, as the file's header requires.

scripts/native_handle_ledger.py then failed its own self-test on a second and independent defect: NON_HANDLE_TABLES still classified builtins/fn_metadata.rs REGISTRY and OVERRIDES as non-handle tables, but e91af6efe6 replaced both modules' tables with stable function-metadata identity, and the file now declares no static at all. Both entries are gone.

Verified in both directions. As committed, the gate and its self-test exit 0 and the ceiling totals match the tree, so the debt fell advisory is silent. Driven deliberately, the guards still fire: re-adding one classification makes the self-test exit 1 with stale classification entries, and deleting a live ledger row makes the gate exit 1 with tables 3 exceeds ceiling 0.

Internal: per-site memos and compiled-code slot helpers carry the slot byte offset (A1 memo contract); no user-visible change.

perex 0.1.18: a capture search on the lazy automaton takes plain transitions in a loop of their own and applies a thread's tag events inside it (a capture state holds one thread, so its tag row sits at a fixed offset); an unbuilt transition goes to the slow step directly. 32-regex: perex capture search 2,986 -> 1,902 Ir per exec, whole exec loop -17.5%.

Test only: the memberless class role descriptor test expects the names-pooled
declaration flag that every emitted declaration now carries.

PERRY_EXTRA_LINK_ARGS now reaches every final link perry runs, through one reader: the
executable and WASI links as before, and now also the dylib link and the proven tiny-program
link. The tiny-program path used to refuse the variable and fall back to the normal pipeline,
so setting it changed which program was built. Builds that do not set it are unchanged.

The startup A/B harness (scripts/a1/irdiff.sh) gains a link-pad control, on by default
(IRDIFF_LINKPAD=0 turns it off). Rows with equal call counts whose cost follows addresses
(mimalloc page map, address-sorted freed ranges, libc compare alignment) moved whenever one
arm's sections were smaller. scripts/a1/linkpad.py links an object of unreferenced,
retained (SHF_GNU_RETAIN) .text/.rodata/.gcc_except_table/.data.rel.ro/.data/.bss
input sections last into each arm. It relinks until every paddable section ends at the same
address in both arms, so later sections, segment starts and the heap start coincide. The
readelf proof is written per witness. Sections the runtime walks record by record (perry_*,
.eh_frame, stack maps) are never padded.

Compiling the same program twice now emits the same code. The private generator
frame numbered the bindings of an inlined resume body in the iteration order of a
hash set seeded per process, which changed the capture slot offsets, the IR and the
register allocation of the user code from one compile to the next (43-generators
alternated between 1.71 G and 2.43 G cycles). The bindings are now numbered in
ascending original-id order. Add a test that compiles a generator program several
times and requires identical IR.

Re-record the raw thread-local allowlist so the policy check passes on main
again: two per-GC-cycle statistics counters (gc/leaf_service.rs,
gc/progress.rs) are recorded as cold, and two files whose raw declarations
were converted (gc/telemetry.rs, node_submodules/mod.rs) drop to their
current counts. No runtime change.

Class registration finds each declared prototype member's position once and carries it to the readers of that member, so the declared-method, kept-value and accessor lookups no longer search the declaration by name (and byte-compare names) again. Equal literal names of a declaration share one pointer in the emitter's string pool, which the runtime reads from a declaration flag to dedupe members without comparing text. The class member order ranks each name once instead of once per sort comparison, and the coloured-atom list is ordered by the program's own colour numbers instead of by name text.

Internal migration of the remaining runtime, stdlib and extension object-header reads to the A1 accessors (type predicates, cell meta accessors, slot bases, allocation widths); no user-visible change.

Internal migration of the GC and arena object-header reads to the A1 accessors (type predicates, cell meta slots, birth widths); no user-visible change.

Async functions build their result promises on a leaf allocation arm (no parent,
no hooks), keep temporaries that live and die inside one resume state out of
the heap frame (no slot store, no write barrier), and admit settled
continuations with a shorter check. Microtask order and every observable are
unchanged; the async benchmark drops about 21% instructions.

A class whose parent is another unit's class no longer counts a store of an inherited birth key as a key add (per-unit facts counted it against the subclass's own keys only, before the parent settled), so a constructor-built hierarchy keeps fixed_layout in per-unit mode as in whole mode. A proven dispatch (receivers known to be one of the facts' classes) on a class whose layout the facts prove fixed now calls the proven-this clone directly, without the keys/flags/ShapeId recheck; stable-but-unfixed classes, contract builds and interpreted-code builds keep it. With the early inlining of facts-closed targets, 40-classes drops from 151.5 to 111.0 instructions per update.

A method body that the whole-program facts prove to be the direct callee of a closed dispatch site (a receiver known to be one of at most two classes, resolved to a body) now joins the pre-statepoint inliner when it fits the guarded-specialization budget, as a proven-receiver clone already did. The call boundary of a facts-closed virtual call such as an inherited describe() over a Circle/Rect array disappears; nothing is admitted without the proof, and unproven or mutated programs keep their call boundary. 40-classes drops about 7% of its instructions per update on its own; the larger win needs the construction-phase facts.

Tie the six %IteratorPrototype%-family frontier pins in scripts/gc_runtime_root_holders.json to the registered scanner that reaches them.

scripts/gc_runtime_root_holders.py only counts a holder as covered when the scanner is defined in the same file as the declaration. The eight ..._ITERATOR_PROTOTYPE_PTR_SLOT atomics live in crates/perry-runtime/src/object/iterator_prototypes.rs while object/mod.rs::scan_object_cache_roots_mut visits all eight through their RealmAtomicI64 handles, so every one of them reads as uncovered. The inventory's answer for that shape is a frontier pin carrying the scanner's exact registered name: the pin stays valid only while that registration exists, and deleting it invalidates the pin instead of leaving it matched.

The two newest slots already had the annotation, added with them. The six older ones were pinned before that form existed, so they were plain debt — a deleted scanner registration would have left them matched and the gate green, which is the silent hole this list exists to prevent.

Verified against the gate's own apply_frontier with scan_object_cache_roots_mut removed from the registered set: the six previously reported nothing and now report stale, alongside the two that were already annotated. With the registry intact the gate is unchanged (OK — 1459 declarations, 484 classified, 304 pinned). No runtime behaviour changes, no Rust source is touched, and the inventory file was not reformatted.

Added

  • The ES2025 Iterator global: Iterator.prototype (the shared iterator prototype) with spec iterator helpers as real methods, Iterator.from, Iterator.concat and Iterator.prototype[Symbol.dispose]; generators inherit from Iterator.prototype. Test262 built-ins/Iterator goes from 79 to 434 of 513, and the full-corpus baseline to 90.15%.

Fixed

  • Iterator helpers follow the spec (counter argument, TypeError on non-callable callbacks, closing the underlying iterator, take/drop limit validation) and no longer answer helper names on objects that do not inherit from Iterator.prototype.

Async functions resume in place with the awaited payload handed back by value
from the settled-continuation probe (a declined probe answers with the TDZ
sentinel), instead of a bool plus a second read of the iter-result cell, and
Promise.all skips the per-element resolve and then probes for plain native
promise elements (native promise, no metadata). Microtask order and every
observable are unchanged; the async benchmark drops about 11.7% instructions.

Reconcile scripts/gc_runtime_root_holders.json with the collector refactors that landed after its last audit, and advance the two source-pinned contracts it had invalidated (#13495).

The gate was failing on main in four independent ways, none of them a new GC risk:

  • 11 stale frontier pins. Their holders either became COVERED by a registered scanner (RECONSTRUCT_CENSUS, REMEMBERED_SET_RECONSTRUCTED, TEST_ARMED_OVERRIDE, GC_LAST_OLD_RECLAIM_IN_USE_BYTES, FFI_MUTABLE_ROOT_SCANNERS, FFI_NAMED_MUTABLE_ROOT_SCANNERS, and the three ITER_RESULT_* cells, since consolidated into a covered ITER_RESULT) or moved module (GC_STATS, CLASSIFIER_VERIFY_SUPPRESSED). Coverage and a rename are the receipt: the debt pin is deleted, and the two still-uncovered holders are graduated out of the debt list with a researched verdict instead.
  • 8 stale holders entries. #13443 folded seven per-counter TLS declarations (FORWARDED_STUB_MEMBERSHIP_RECOVERIES, BLOCK_PERSIST_FORCE_MARKS, ADOPTED_BLOCKS, HOLE_REBUILD_BLOCKS_SKIPPED, BLOCK_SKIP_RECLAIMED_{BLOCKS,OBJECTS,BYTES}) into GcStats fields behind one GC_STATS slot, so those declarations no longer exist. alloc_census.rs:CREDIT is reached by a registered scanner today.
  • 5 new rule-T holders, each classified rather than pinned as debt: GC_STATS: RefCell<GcStats> and CLASSIFIER_VERIFY_SUPPRESSED: Cell<bool> are not_a_gc_pointer (counters, a duration ring and a cursor; a boolean latch), and LAST_SKIPPED_BASES, DFA_CAPTURE_ANSWERS, DFA_CAPTURE_PASSES are test_only (all three sit behind #[cfg(test)]).
  • 13 source pins whose content moved under efd2738fbe (#13443) and the #13431/#13397/#13457/#13463/#13471/#13472/#13479 slices: 10 in the PASS1_MARKED window and 3 under the INTERN_TABLE weak-cache contract.

The two pin re-audits are the part that is not bookkeeping. PASS1_MARKED is a Vec<usize> of real GC header addresses, deliberately untraced, populated at the end of mark propagation and consumed at sweep entry inside one run_to_completion; the contract is that nothing between the boundaries allocates a GC object, relocates, collects or runs a JS callback. Both boundaries and both predicates are unchanged, and the window's phases were read rather than assumed: BlockPersistence only marks, and the only relocating subphase of AtomicFinalize — MinorPrelude — is entered solely from the minor branch of the ephemeron clear, while a full cycle goes WeakProcessing → DisableBarrier → Sweep. #13443's const-generic DIAGNOSTICS split gates only trace_phase_* recording and the read-only honest-pointer and minor-unmarked verifiers. INTERN_TABLE still has exactly two production writers: the publish_intern_entry funnel, which refuses publication for the whole duration of every open HeapChange, and the full clear from HeapChange::begin and gc_realloc before a GC_TYPE_STRING cell can move or be released. Every other slot site in the crate is a read, and #13431's new AtomTable::probe_from/seed_if_absent writes the separate immutable-image ATOMS table.

No runtime behaviour changes: this commit edits gate data only. The inventory file was not reformatted; the round-trip through its JSON encoding is byte-identical.

Small Map lookups (up to eight entries) scan their keys without an early exit, so where a key sits no longer decides a branch; a lookup over a few entries in varying order no longer mispredicts once per call.

Fix better-sqlite3's Statement#columns() and Statement#safeIntegers() reading undefined.
The extension served run/get/all/raw and, since #13365, every documented data property, but not these two through either the typed native table or handle dispatch — so a caller got no clean "not a function", only whatever its own code did with undefined. ActiveRecord's SQLite adapter inspects declared column types to decide whether to read big integers, and stmt.columns().some(...) threw TypeError: Cannot read properties of undefined (reading 'map'). columns() now reports one record per result column with better-sqlite3's five keys in its order (name, column, table, database, type), taking the origin names from rusqlite's columns_with_metadata() so a real table column says where it came from. safeIntegers() is a per-statement flag, shaped like the existing raw_mode, that makes INTEGER columns read as BigInt; it returns the statement so the call chains, an explicit false turns it back off, and an absent argument means on. Both are named in STMT_METHODS, so a bare typeof stmt.columns yields the bound method.

Class member names carry the compiler's UTF-8 fact; registration no longer validates them (startup Ir: Richards −64,254, fastify −77,544, commander −30,600, commander --help −30,605).

Built-in submodule singletons (node:timers/promises, node:diagnostics_channel,
node:test, ... export closures, namespace and default objects) live in one
slot record per submodule, indexed by the submodule's ordinal and the export's
ordinal, instead of std SipHash maps keyed by the addresses of the submodule and
export name strings. A lookup is two indexed loads; its cost no longer depends
on where the link put those strings.

HTTP/2 sessions now behave like Node on the wire for SETTINGS and PING. A SETTINGS frame carries only the options the caller named (a default server and client send an empty frame), any HEADER_TABLE_SIZE is honoured, localSettings change when the peer acknowledges and the handshake acknowledgement emits localSettings, and the settings and ping callbacks receive the measured round trip. ping() validates its payload and callback, and a PING acknowledgement with no PING outstanding fails the connection with GOAWAY(INTERNAL_ERROR). The turnloop HTTP/2 core now receives PUSH_PROMISE and can send one; a client leaves push enabled and declines every promise with RST_STREAM(CANCEL) until pushed streams have a JavaScript surface.

Every program starts about 57,000 instructions faster (8.5 % of hello on
x86-64): the per-thread runtime state is now built where it lives. It was
built in stack temporaries and moved into its heap allocation, which cost four
copies of 46 KB in all plus a 16 KB stack fill, and made startup's
instruction count depend on stack alignment. The allocation now comes
zero-filled, so the two inline caches, which are empty as zero bits, are not
written at all.

Internal migration of codegen object-header accesses to the A1 header-form API; no user-visible change.

Realm header words now come from one derivation, perry_abi::realm_header,
fed with each birth's declared facts: the realm manifest producer and the
runtime births of realm cells call it instead of choosing GC header bits on
their own. The last 44 builtin declarations moved from *.realm.json
companions to their births, and the companions and their transport are gone.
Runtime births that changed: realm string arrays (process.argv,
process.execArgv, module.globalPaths, navigator.languages,
http.METHODS, Module.wrapper, module.builtinModules,
tls.rootCertificates) are born with their all-pointer layout instead of
reaching it by promotion or falling to the tag-scanned layout; empty realm
arrays (Array.prototype, domain._stack) no longer claim the raw-f64
representation; vm.constants is an ordinary frozen null-prototype object
as in Node; zlib.codes is written from one ordered table;
module.isBuiltin and module.flushCompileCache are born
non-constructable; and a runtime-internal non-enumerable key now publishes
its holder's descriptor flag.

Internal migration of the object directory to the A1 header accessors (predicate and proof macros, runtime-birth sizes); no user-visible change.

Fixed

  • Enforce Node Worker restrictions before process-wide operations, expose disabled callable metadata and hide unavailable process internals without adding another worker identity flag.
  • Keep computed Worker title assignments live through the existing title provider while leaving existing named-import behavior unchanged and avoiding Linux process-name updates from workers.

Maps keyed by program input (static member names, native export names, JSON
property bytes, unwind sites) now hash with one per-process random key block
that mixes in no address, instead of ahash::RandomState::new(), which folded
the addresses of a static and of the stack into every seed. Flood resistance is
unchanged, since the keys still come from the OS once per process; probe order
and key-compare work no longer depend on link layout or the environment. The
timer handle table, keyed by our own monotonic ids, no longer seeds at all.
Wide JSON objects share the process key instead of drawing one per object.
The measurement-only fixed-hash-seeds feature (its marker and the flag in
scripts/a1/build-arm.sh / irdiff.sh) is removed: the default hasher state is
now layout-independent on its own.

Fixed

  • Share private worker clone/transfer markers with global structuredClone, ignore primitive marks and preserve builtin clone exemptions.
  • Store marker identity in one GC-rooted weak table so live marks survive moving collections without retaining dead targets or copying a marker set for every message.

Add a measurement-only runtime feature, perry-runtime/fixed-hash-seeds (off by
default, never shipped): the startup-path ahash maps (fast_hash::AHashState:
native callable exports, static name maps, the JSON parser, timer ref states and
the unwinder's site cache) seed from fixed constants instead of
RandomState::new(), whose seed mixes in static and stack addresses. Exact-Ir
diffs then stop moving with the linker's layout. The default build keeps
ahash::RandomState::new(): an unpatched and a patched default build have
identical total Ir on hello, commander --help, fastify and Richards.

scripts/a1/build-arm.sh builds an arm with the feature and
scripts/a1/irdiff.sh refuses arms built without it.

Fixed

  • Route worker environment-data callbacks through Perry's typed hook mechanism, removing raw callback casts and unclassified global pointer slots without changing their call behavior.

Lay the foundation for the object-header work (A1) as a no-op on main: every
program compiles and runs exactly as before. GC_TYPE_OBJECT stays 2 and no
second object type exists yet.

Add the object-header access API (object/header.rs: MetaObj,
object_type, object_slot_at, object_meta_if_present) and the
gc_type_is_object / object_has_meta_word predicates, which both answer
t == GC_TYPE_OBJECT today. Route the 45 meta-record creation sites through the
typed funnel object_meta_ensure(MetaObj, MetaReason); MetaReason has one
variant per creation reason and excluded_by() names, without a wildcard, the
whole-program fact that keeps a meta-less family away from it. Sites that
cannot yet prove their owner is meta-form are marked A1-UNPROVEN.

Layout keys carry their slot's absolute byte offset (PerryLayoutKey.off, in
existing padding; emitted for the coming reader migration), and
PerryLayoutDesc.gc_header_word carries the idle GcHeader image of a layout
birth, emitted by codegen and pinned by const-asserts against target_layout.
perry_alloc_layout stores that image instead of composing it. The descriptor
is padded to 128 bytes so the descriptor-section index stays a shift (any other
size makes startup pay a longer division).

Add scripts/check_object_header_access.sh (count mode), the exact per-function
Ir diff tool scripts/a1/irdiff.sh, and the subprocess death-test harness with
a1_internal_error (a test proves the helper fails when the child survives).

Whole-program facts link an import() site to the modules its specifier resolves to (through the importing module's dynamic import entry, as the dispatch does) and keep ⊤ for the fallback. A relative specifier used to match no module, so the imported module's exports kept only their local callers and a literal import("./m.js") could miscompile (printed 5 for 105).

Fixed

  • Connect captured and CommonJS worker_threads.getEnvironmentData and setEnvironmentData calls to the existing implementation.
  • Inherit a creation-time environment-data snapshot in workers using one structured-clone graph with workerData, preserving cycles, shared references and transfer rollback on failure. Clone failures stop traversal before later getters run.

Fixed

  • Restrict family-method probes to layouts whose static method index contains the requested name, avoiding guaranteed misses for unrelated class families.
  • Mix fresh symbol identities once at creation to spread their hash bits in mixed symbol/string property tables. Preserve symbol identity, enumeration order, and move stability.
  • Reuse registered-name reads and successful inferred-name writes during method construction, retaining canonical image-name precedence.

Fix node divergences and stale tests on main.

  • Built-in iterator receivers: the shape-store key-add funnels now honor the
    #9019 reserved-field floor, so it.foo = 1 on a Map/Set/array/string
    iterator no longer overwrites its backing-collection field.
  • A Map or Set constructed by a derived class records its prototype; spread,
    Array.from and for-of copy its entries only when that prototype is the
    intrinsic one, so an overridden [Symbol.iterator] drives the iteration.
  • Array.from(array) walks the array directly only while the array's
    iteration is intrinsic, including %ArrayIteratorPrototype%.next.
  • Iterator GetMethod reads a key's storage slot, not its position, so an
    accessor redefined on %ArrayIteratorPrototype%.next is called.
  • await and promise resolution read a promise's own constructor (and the
    intrinsic then's SpeciesConstructor read) as PromiseResolve requires.
  • A promise reaction enters its promise's async scope only while a promise
    hook observes promises; otherwise it runs in the enclosing execution
    context, as in Node (executionAsyncId() before any hook is enabled).
  • The entry's tick/microtask checkpoint follows Node's module format: the
    extension or the nearest package.json "type", then module syntax.
  • zlib.brotliCompressSync(buffer, options?) in the API manifest.
  • Stale perry-hir and perry-codegen test fixtures; runtime warnings;
    PERRY_PIP_SABOTAGE registered as a build-cache input.

Fixed

  • Validate truthy non-string worker_threads.Worker names before reading them as strings, preventing constructor crashes and preserving transferable data on rejection. Falsy names retain their default behavior.
  • Validate and consume Worker transfer lists even when workerData is undefined, including unreferenced ArrayBuffers.

Support young, individually owned non-moving arena storage for pointer-free large leaves, with minor-collection reclamation and promotion in place.

Retire known old-generation page spans by canonical directory chunk, dropping wholly removed chunks directly and resetting only surviving chunks’ removed slots. Preserve deferred-registration ordering, promoted-run discard and sparse removal semantics while avoiding temporary page-number vectors and replacement writes into allocations being freed.

Whole-program facts walk only the function bodies that can run: a body is analysed once a reached transfer calls it or its value escapes, so code the program never invokes no longer leaks escapes into the rest (tssrc whole-program proven 20,003 -> 21,868 sites). Per-unit facts are unchanged.

Class methods the whole-program facts prove to return a Number, and field reads they prove hold only Number, Boolean, null or undefined, now lower + and arithmetic as a plain fadd instead of the dynamic helper, and stores into proven slots write the raw value directly; the finite-number test stays on F64 lanes. 40-classes executes 27.2% fewer instructions.

Realm facts for 3,118 of 3,162 builtin declarations now live at their
fn_info! sites and in realm_decl! blocks beside the births that create the
objects, instead of in checked-in *.realm.json companions. The runtime build
producer reads those source declarations; the manifest it emits is identical to
the previous one after normalizing declaration source and line, and the hello
and lad-07-json binaries are byte-identical. Two optional declaration facts
were added: a never_move birth fact and an integrity = frozen callable fact.

String building is cheaper again: split births each piece without a per-piece root and reload when the allocation provably cannot collect, join walks its pieces with a heap-string-first dispatch and inline small copies, string-array length loops are admitted as one call-free region, and concatenations of two to five operands call count-specialized entries. Allocation order, GC schedule and memory use are unchanged.

Canonical key lists are identified by their trie node, carried by every
holder (the canonical handle, the receiver's keys view and the shape record,
where it is packed beside the element kind in the existing 128-byte record)
instead of being looked up in an address-keyed map. The (address, count)
index of the canonical trie is removed; a carried node is validated by one
read, so a pruned or reused node can never answer for another list. The
prefix of a carried list is reached through parent links with no hashing.
Symbol keys hash their symbol serial rather than their address, so the edge
table no longer depends on link layout and a moved symbol still reaches its
edge. Worker shape seeds re-derive each list in the worker's own trie.
Startup Ir drops on every startup witness, and the canonical code's Ir no
longer moves when the image layout is padded.

Static GC scan plans: an object born on a class or literal family layout is
scanned by the copying minor and the full mark from a plan stored in its layout
descriptor (PerryLayoutDesc.gc_plan, emitted by codegen from the family's slot
count and field representation) instead of a plan rebuilt from the shape for
every object. The walk is the plan's pointer-slot mask, the meta record and the
arguments tail; the shape's keys edge and the old-carrier notes are skipped for
a family record (a static record whose keys the shape table already roots), and a
minor skips the prototype word its young log already roots. A full mark still
emits the prototype identity once per trace. Other objects and descriptors
without a plan take the generic walk unchanged. In test and debug builds every
planned walk is cross-checked against the generic walk, and a planted mismatch is
caught.

Linux main-thread stack bounds come from the kernel stack layout and RLIMIT_STACK instead of glibc parsing /proc/self/maps, removing about 130k instructions from every startup that allocates.

The Map get-then-set idiom (m.set(k, f(m.get(k))), or const cur = m.get(k) followed by m.set(k, ...)) now probes the Map once: the get reports the entry slot and the set stores into it when nothing ran in between, the key bits match and the value is a plain number. Any call between them (user code, collection, delete, clear, growth) makes the set an ordinary js_map_set. 51-pipeline -11.1% instructions.

Continue a settled self-next await in place instead of round-tripping the microtask FIFO. When the running job is the activation own step dispatch, the result promise is reusable, no async_hooks or V8 promise hooks are active, the awaited value is a primitive or a native fulfilled promise with no recorded metadata, the microtask queue is empty and the job domain is not cancelled, the step loops back with the awaited result; every other case takes the unchanged FIFO publication, so microtask order, hooks, AsyncLocalStorage and rejection timing are identical.

Pack the seven iterator-result thread-local cells into one record.

Method calls on a receiver with a static class (an annotation, or this inside a method) use the facts-proven class-id switch when the whole-program facts prove the receiver one of cased implementor classes, exactly as untyped receivers already do; otherwise the guarded typed path is unchanged. 40-classes executes 14.3% fewer instructions.

Regex slice 5 (Perex 0.1.17): capture groups are read off the lazy automaton on one-pass matches. An anchored capture search (one-start programs, and the pass after find for the rest) records each transition's capture-slot effects once and answers with no evaluator; a transition that keeps two paths alive declines to the evaluator. The automaton's storage is now a separate old GC_TYPE_REGEX_CACHE leaf reached by one traced edge from the program cell (new RegexProgram descriptor), allocated, at the size Perex gives the program (dfa::initial_words), by the program's first search that needs automaton states, and reallocated at the size Perex reports when the program thrashes; searches the first-character descriptor decides allocate nothing, new storage is reset (one word) rather than zeroed, and program construction no longer zeroes or sizes any cache. A program whose capture pass found two paths alive is marked in its storage and not asked again. 32-regex: -46.9% instructions, -54.7% cycles.

String concatenation is cheaper: literal-aware concat folds image literals without re-reading headers, operand checks are inlined at the call site, and array push of freshly built strings uses a pointer-only gate. Allocation and GC schedules are unchanged.

Generator resumes on a private frame keep the state, completion kind and done
controls in registers for the length of one resume and write them back on every
exit, instead of re-reading the owner through the frame for every access. The
operation is an integer argument, the try-scope handle height is read once per
resume, a transition to a known state branches straight to that state instead of
through the dispatch switch again, and the sealed private caller supplies the
constant sent value, which removes its barrier test. The executing flag, the
sent slot and every user slot stay in the frame. No cache, side table or knob.

Cover return and throw injection mid-loop, try/finally and in-generator catch,
early return from for-of, nested frames and moving-collector stress in gap
tests, and assert in codegen tests that every exit writes the controls back.

wpfacts: per-unit facts publish callable identities to closed callees and the arguments their runners pass (DEC223, DEC224). A function handed to a known closed callee keeps its identity through that callee's contract; each unit that runs it publishes the join of what it passes, which stands beside the parameter markers, while an undescribed position, an escape beyond publication, a unit over the work bound or a census break keeps the parameters unknown. Passed closed module functions publish through DEC19, an absent ordinary caller contract is no input, escaped results beside unknown normalize independently of arrival order, and contract-published functions stay described beside unknown results. Facts dumps gain publication-route records and a verdict per function copy. A class accessor thunk stores its code address without touching the closure layout, so linking no longer decides whether a startup closure takes a layout transition.

Use the actual Map brand and canonical stored-key identity to keep existing-key reads and replacements out of normalization, insertion and subclass fallback frames, retaining exact GC slot barriers and complete fallback semantics.

Reduce settled-await scheduling overhead using actual activation ownership and Promise brand, state, metadata and effect proofs. Preserve the existing FIFO jobs, Promise hooks, contexts, activation identity and incremental root insertion barriers. Avoid copying empty AsyncLocalStorage contexts while retaining the canonical native job domain.

Bind the existing private-frame slot barrier to the uniform live IR census and its sabotage matrix.

Share the native enqueue body between both admission routes to avoid duplicating barrier and context scaffolding.

Use one scanned Task publisher for observed and unobserved awaits. Preserve producer notifications and reload the rooted result across a reentrant host wake.

Use the validated zero body mask to prove an empty immutable shape body list without reading its cold extension; nonempty lists retain exact identity checks.

Optimize private resume bodies for execution instead of the default application size policy, while honoring explicit minsize requests.

Preserve emitted callback identity across declaration units and admit immutable numeric captures once at guarded Number-loop entry. Private scalar returns now feed the loop proof; unknown values, mutations and mixed inputs keep the complete original loop. Known callback bodies skip redundant entry resolution.

Move required GC marking, pointer census, remembered-set reconstruction and rewrites into collector-owned modules. Keep GC verification and telemetry behind the diagnostics installer, and check diagnostic implementations across module boundaries in the link-absence gate.

Function names and retained source now use explicit image metadata owners and deterministic entry ordinals. Reflection and inference follow the function info directly, avoiding address-keyed metadata work at startup. Stack resolution reads the same canonical records, unloadable images retain copied metadata, and internal raw-pointer registration exports are removed.

Reduce redundant program-cache lookups when constructing regular expressions, preserving compiled-program sharing, independent instance state and synchronous syntax validation.

Use Perex 0.1.16 to reduce evaluator dispatch over original input storage, reuse scoped repeat records and derive exact work-quantum boundaries once while preserving complete captures, work limits, pause behavior and storage relocation.

Allow a pending parent-mortality probe to promote young blocks in place after its current trace meets the existing survival criterion, retaining evacuation rollback and GC stress exclusions.
Reuse exact promotion-walk census facts in an immediate full collection for traced promotions as well as untraced promotions.
Use bounded covering-word ASCII validation in the canonical metadata decoder and inline its shared read adapters, preserving UTF-8 fallback, ownership and lookup precedence.
Refresh adopted header facts after minor mark clearing so traced census reuse preserves whole-block reclamation.

Fix two copying-GC unit tests to establish the heap-change scope required when debug assertions are enabled.

Fix every --target wasip1 and --target wasi compile panicking with runtime_abi.tsv: bad return in "js_zlib_brotli_quality\ti32\tf64".
The wasm32 runtime ABI table listed the new js_zlib_brotli_quality with an i32 return, which the table reader does not accept (it expects i32s or i32u), and the table is parsed on every wasm compile. The table is regenerated with scripts/runtime_abi_check.py --emit-wasm-abi, which writes i32s.

Reduce Map string-key lookup work with bounded word content hashing and direct indexed candidate comparison, preserving exact content equality across string representations and mutation.

Use the existing Map string index to admit canonical string lookups and validate indexed candidates from their stored-key proof, preserving mixed keys, collisions, order, mutation and moving GC.

Preserve proven canonical Number results through arithmetic and static property stores, avoiding redundant coercion and pointer bookkeeping while retaining guarded dynamic and BigInt paths.
Reuse successful method-name publication during class method creation, preserving image-name precedence and avoiding a redundant metadata lookup at startup.

Optimize ordinary Number remainder with the shared checked integer lowering, preserving fractional, overflow and signed-zero behavior.

Fix a garbage-collection crash when opening file streams from the previous stream's completion callback. Directly created file streams now register their retained callbacks and objects as GC roots without requiring the fs namespace to be materialized.

Fix a panic when wasm programs dynamically construct a class whose constructor takes 16 or more parameters.
The portable fallback of the all-f64 call trampoline stopped at 16 arguments (this plus 15 parameters) and panicked with unsupported arity above that, which a Fastify-based HTTP service compiled for --target wasip1 hit at startup with 18. On wasm an indirect call of an exact all-f64 signature is correct for any count, so the fallback now dispatches up to 64 arguments (this plus 63 parameters) and still fails closed beyond that. Native targets are unchanged. --target wasi gets the same fix.

Improve numeric string loops by proving primitive ASCII storage and pristine string methods once before entering a call-free region, while retaining Unicode and override behavior. Avoid redundant JSON duplicate-key scans for matched unique shape prefixes and use exact signed 32-bit number formatting when applicable. Inline the shared validated metadata reader to avoid an extra call during function-name reflection and inference. Static string atom seeding reuses its vacant lookup slot instead of probing twice, preserving collision checks, growth and first-atom identity. Boxed string/Number concatenation admits the exact u32 formatter domain and reuses its digits for heap results as well as inline results.
Preserve the original deferred-collection entry poll before admitting raw string storage, so allocation-free regions do not postpone collection and raise peak memory.

  • Remove the vendored Brotli backend patch and restore the published Brotli 9 dependency. Keep the independent synchronous option and decoder integration; short custom dictionaries at quality 11 remain a known parity failure.

  • Forward synchronous Brotli dictionary and quality options through native dispatch and compiler bindings, and preserve decoder dictionary diagnostics.

Reduce regular-expression VM overhead by borrowing validated program sections and class records and retaining repeated-atom phases within a trial. Preserve capture answers, work accounting, relocation, and lazy-automaton behavior. Use the reusable Perex 0.1.15 release from crates.io.

Avoid image-layout-dependent libc comparison paths when comparing property keys, using bounded word reads over their actual bytes.

Extend private resume frames to closed async activations retained by the existing microtask queue and to protocol-only nested generator consumers. Preserve generic traced slots, moving-GC owner roots, barriers, rejection/finally behavior and microtask order; retain standalone frames whenever ownership or effects are unproven. Inline private slot stores and preserve cloned control/callback facts.

Improve whole-program own-field proofs by checking private receiver objects independently of escaping or modified siblings that share their layout, while preserving conservative layout contracts and prototype/accessor checks. Use numeric-element flow consistently for specialized array operations so callbacks do not receive unrelated intrinsic methods. Add confinement, mutation, escape and array-element regression controls, including the former PAR4 precision fixture. Keep unknown class construction confined to its fresh results, preserving constructor effects and real alias publication instead of escaping every existing birth of the layout.

Programs that use Date no longer select Temporal for computed lookups on unrelated array or scalar enum values. Preserve private-field generator cursor types, enum member shapes, and unassigned rest-array facts during compilation. Dynamic Date method names and unknown receivers continue to select Temporal.

Extend the optional facts dump with source spans, emitting analysis units, independent receiver/result kinds and layouts, raw callable alternatives, bound arguments, and function/class declaration identities. These observational records are captured only when the existing facts dump is requested and never feed proofs or code generation.

  • Guard boxed class-method receivers with a fused tag/heap-range check, so positive subnormals take JavaScript method lookup instead of being dereferenced as raw addresses. Keep admission separate from the packed identity when selecting compiled methods, preserving all shape, descriptor and prototype checks. Apply the boxed-receiver rule to generic method observations too.

Select a complete Windows Perl for native GC builds and record source-pinned reviews of private WASI discard globals that collide with unrelated test assertion names.

Record the WASI discard allocator pool ownership and scalar diagnostics in the thread-exit custody audit.

Keep string-copy GC witnesses live across loop polls, accept an empty post-collection live heap, and compile separate instrumented executables for GC benchmark trace and relocation checks while preserving ordinary-runtime timing.

Add the opt-in PERRY_WASM_DISCARD=1 build of the --target wasip1 output, which imports perry_host.discard_pages so the runtime can hand freed, page-aligned memory (shrunk resizable ArrayBuffer tails, large freed blocks, arena blocks) back to the host; the default build still imports only wasi_snapshot_preview1. The reference preview 1 host implements the import with a --no-discard knob and call counters, and the import checker allows it by name and signature under --allow-perry-host.

Follow the current collecting property-read entry points and all three descriptor-key visitor paths in GC audits. Add missing-edge and missing-dispatch sabotage checks, and refresh reviewed source pins for birth-color bounds, arena teardown, and numeric string concatenation without raising audit ceilings.

Run the full runtime test lane with the same Cargo-backed unwind support as PR tests, preserving all unit, integration, and doc tests. Remove an ignored inline attribute from the exported string equality entry point so strict warning builds pass.

Named imports from Node.js builtins now link the selected export, reducing executable size for common TypeScript code such as import { readFileSync } from "fs". Calling an imported function directly follows the same lowering as a namespace member call, and passing it as a value preserves function identity, constructor behavior, and updates made through syncBuiltinESMExports().

Reuse formatted Number digits when string concatenation exceeds inline storage, and mix numeric Set key hashes before bucket masking to avoid long probe chains for integer doubles. Preserve mixed-key equality, insertion order, string metadata and ordinary allocation/rooting paths.

Avoid temporary reflected-name allocations in class method initialization and preserve the string Set hash path before numeric hashing. Correct the forced image-store audit fixture expectation.

Resolve native module accessor overrides through their getters and preserve the canonical fs.promises value when restoring its property descriptor.

Fixed

  • scripts/test262_subset.py --full-corpus now stages _FIXTURE files next to each case, so 275 dynamic-import cases no longer fail on a missing module; the Test262 baseline moves to 89.24% (47,560 / 53,293).

Call the non-Unix signal-count leaf directly so Windows GC-effects analysis does not require an optimized-away indirect callback. Unix retains installer-selected signal polling.

Refresh rooted array addresses across collecting operations and scope non-collecting reads, removing 40 raw-handle audit sites. Keep the runtime TLS teardown visible to optimized GC-effects analysis, classify compact page-offset stores, and split archive cleanup tests to restore the file-size gate.

Use canonical layouts to omit empty copying scans and repeated object iterator, mask and shared-keys classification work. Consume immutable compiler image words directly at fixed births, and make the existing nursery bump bound enforce idle birth color, preserving black-birth seeding, forwarding, pointer widths, survivor accounting, statepoint roots and remembered-set coverage. Short string and property-key equality use in-bounds machine-word comparisons to avoid alignment-sensitive libc dispatch.

Fix WASIp1 release packaging to validate the optional networking archive member against the existing perry_net function/signature allowlist. Default linked programs still reject these imports unless networking is explicitly enabled.

Keep primitive number coercion and dynamic addition frameless by separating their existing plain-double fast paths from the full conversion bodies. Preserve boxed values, string and BigInt addition, and observable ToPrimitive behavior.

Make WASI filesystem validation portable to filesystems that reject non-UTF-8 names at creation: require the native EILSEQ error and verify that directory enumeration still returns every valid entry. Filesystems that admit such names still exercise the WASI IllegalByteSequence path.

Restore strict Rust warning checks by removing unused imports and redundant parentheses, matching helper visibility and platform/feature gates to their callers, and retaining test-only hooks only in test builds. Make the WASI FIFO fixture portable across Unix targets and declare enough static layout rows for the 16 KiB-page shape-retirement fixture.

Array sorting can now prove pure numeric comparator order from return signs and control flow, including closed helpers, named functions, key projections and numeric coercions. Unresolved effects and inconsistent ties retain callback sorting; live numeric and object admission checks remain unchanged. Wasm sort ABI metadata also retains signed direction parameters.

Fix release validation drift in the Brotli decompression API signature, GC store/address audits, and per-test counters. Make retired-shape coverage exercise a full OS page on macOS, preserve the CommonJS file-size limit, and repair workspace audit fixtures under symlinked temporary directories.

Added

  • Nightly Test262 coverage: scripts/test262_subset.py --full-corpus scores every Test262 case by Perry alone (comparable to other engines' published figures), and the full-tier test262 / test262-aggregate jobs ratchet it per test against test-compat/test262/baseline.json (88.84%, 47,346 / 53,293).

Fixed

  • Self-validated async Test262 cases that failed through $DONE were scored as passes.

Reduce string construction overhead by formatting numeric and inline-string concatenation operands on the stack. Reuse the converted bytes when allocation needs collection, with heap operands rooted and reloaded before copying. Preserve UTF-16 lengths, raw-byte provenance, WTF-8 surrogate repair and the established heap-string fast path. Avoid formatting numbers twice when their digits exceed inline-string capacity. Check registered function-name presence through a validated borrowed view instead of allocating and discarding an owned string.

Repair the image-birth unit audit for release builds and remove a duplicate test-only environment exclusion and handle the filesystem root in the relative-URL test fixture.

Speed up Map/Set lookups by specializing indexed numeric Set probes, deferring Map string collision probes, and unrolling small Map identity scans while preserving mixed-key and SameValueZero behavior.

Avoid allocating a temporary function-name String when registration only checks whether a valid name already exists; preserve owned/image precedence and UTF-8 validation. Use the runtime pointer hasher for the canonical function-address name table. Allow inferred names to replace undecodable owned metadata when no valid name exists.

Make the image birth-fact knockout test follow the configured release/debug store-audit mode.

Release builds ship perry-cross-wasm32-wasip1.tar.gz beside perry-cross-wasm32-wasip2.tar.gz (part of #13259), so perry compile --target wasip1 works from an installed release without building the runtime. scripts/wasi_release.py and scripts/wasi_release_check.py take --target wasm32-wasip2|wasm32-wasip1 (default wasm32-wasip2, whose bundle is unchanged). The release workflow builds the preview 1 runtime, checks that it imports only wasi_snapshot_preview1, and packages it. Each WASI-enabled host archive qualifies the release compiler with the packaged bundle (smoke, acceptance and pinned-Node suites with --flavour p1, every module checked for preview 1 imports) and stages the runtime under wasm32-wasip1/release/ next to wasm32-wasip2/. A release source that predates the wasip1 packager ships only the wasip2 bundle.

  • Schedule independent proven numeric guards before arithmetic comparisons, transport checked integer recursive returns, and reuse admitted numeric capture values across private closure entries.

Read native UTF-8 input in larger bounded I/O/scan windows, use independent wide ASCII reductions on supported CPUs, and arm the existing precise GC poll when completed bulk input owes reclaim. Keep results rooted across an active collection drain, avoid heap buffers for singleton entering-page indexes, and batch canonical page-directory traversal, registration and removal by existing chunk boundaries. Preserve dense-page fast paths and the loop-poll kill switch.

Exercise the shape store audit body directly in the image birth-fact knockout fixture, including release tests where the optional mutator hook is disabled. Keep the missing-mark rejection assertion and all production audit settings unchanged.

crypto.Certificate is now the legacy constructor function, as in Node
(#13322). new crypto.Certificate() through a namespace import threw
TypeError: is not a constructor, and typeof crypto.Certificate was
object. It now works with or without new, and keeps verifySpkac,
exportPublicKey and exportChallenge as static methods.

Prepare reachable realm cell emission and source-derived function-info exports, including Array's owning static and manifest link. Production realm emission and startup population removal remain pending.

Fixed

  • README and benchmarks/README.md state measured binary sizes: ~34 KB for hello world (no runtime linked) and ~5.5 MB stripped for a small program that uses the runtime on macOS arm64, replacing the stale ~330 KB / ~1 MB claims.

Add the opt-in PERRY_WASM_STATIC_RELOC=1 to build wasm32 objects with the static relocation model instead of PIC.

Programs that index arrays, read ordinary data records, or call filesystem methods directly now avoid linking unrelated Web Streams, compression codecs, Temporal, and diagnostics support. Dynamic access to FileHandle methods, escaped filesystem namespaces, and Bun/JSON APIs still selects the support those operations require.

Add the opt-in PERRY_WASM_NET=1 build of the --target wasip1 output, which imports three perry_net functions so a program can make outbound TCP connections to names and addresses without blocking the event loop while they resolve and connect, and use dns.lookup (which, as on --target wasi, waits for the host's answer); the default build still imports only wasi_snapshot_preview1. The reference preview 1 host implements the imports with an asynchronous resolver and a --dns knob for the suite, and the import checker now allows each import by name and signature.

Reduce RegExp exec result work when a nonescaping result is used only for null checks and one numbered capture. Preserve method lookup order and runtime builtin/brand checks, and poll inline capture output by bounded work instead of once per tiny string.

Also make builtin RegExp exec reject Symbol arguments and Symbol-valued primitive coercions, as required by JavaScript ToString.

  • Repair release validation setup: drop the obsolete standalone events archive prerequisite, recognize glibc matrix fields independently, support the vendored coverage installer, and install Rust components before concurrent tests.
  • Update vulnerable cryptography and zstd dependencies and adapt elliptic-curve key generation, encoding, and signatures.
  • Refresh the WASI runtime ABI and reviewed ownership inventories, restrict macOS memory reporting to macOS, retain the GC stack-map analysis entry, and expose runtime test names on abort.

Lower closed, nonescaping synchronous generator consumers through a private owner-plus-offset resume frame. Keep the canonical standalone path whenever escape, lifetime, callback or protocol effects are unproven, and preserve moving-GC tracing, parent barriers, exceptions and iterator close.

Complete the source-owned Temporal realm declarations and add a reflective coverage check that rejects missing manifest cells. Verify that Temporal reachability closes through the generated catalog while remaining partial.

Read UTF-8 files directly into owned string storage using bounded file-size hints, chunk validation and one final length/encoding publication. Preserve short reads, pipes, descriptor positions, changing files and embedded assets; decode malformed UTF-8 lossily and honor text encodings. Prove the successful native UTF-8 read result and its immutable string length without relying on TypeScript annotations.

Keep binary file reads on Buffer dispatch when chained, preserve option getter order and promise rejection behavior, and include paths on asynchronous read failures as Node does.

Isolate the deserializer feature-selection fixture from dynamic-code reach left by earlier compiler tests, preserving the plain-program negative check.

Performance

  • Sort proven pure numeric orderings with reusable payload storage for dense i32 arrays, and admit non-NaN floating-point values and plain own numeric object keys to stable radix sorting. Preserve comparator effects and unsupported receiver/key semantics through the existing fallback.
  • Return retired array-growth payload pages to the OS while preserving forwarding aliases, and initialize dense replacement slack after retiring the old backing to reduce peak resident memory.
  • Initialize fresh array layouts from their allocator-proven brand and reset header, avoiding repeated receiver checks during startup.

Performance

  • Select a stable radix sort for proven pure ascending or descending numeric comparisons on dense, writable arrays of exact i32 values. Keep comparator sorting for observable effects, exotic receivers, NaN, negative zero, and unsupported numeric keys.

Update the static-publication refusal fixture to use the retained internal seed helper. Verify linked canonical atom records separately from descriptor views while requiring both to share the same image string.

Add source-owned realm declarations, a shared build-time manifest and HIR builtin reachability with explicit full-realm boundaries. Add a reflective realm dump/comparison tool. This phase records facts and diagnostics without emitting realm data into compiled programs.

Add inbound TCP, signals and standard input to the --target wasip1 runtime on standard wasi_snapshot_preview1 imports, with a reference preview 1 host (test-wasi/p1-host) that the required wasi-execution job now builds and runs.

Typed numeric functions, captures and scalar methods expose checked integer remainder to LLVM, allowing constant divisors and guarded integer operands to specialize without changing JavaScript signed-zero or exceptional-value semantics. Avoid LLVM poison for the minimum signed integer modulo minus one.
Numeric local assignments also consume the existing all-writes/guarded scalar proof to omit redundant string-alias checks.
Typed closure captures share the ordered Number admission tier with arguments, preserving the boxed-int32 and generic fallback paths.

Whole-program facts: a prototype taken as a value is now the prototype the
runtime has. o.__proto__, o["__proto__"], Object.getPrototypeOf(o) and
Reflect.get(o, "__proto__") read the receiver's prototype (a literal's is
Object.prototype, a class instance's its class prototype); they used to read
nothing, or the literal's internal layout, so a store through them was missed.
A computed key is still never taken to be "__proto__". A number key now
reads a class prototype's numeric members (d[n] missed C.prototype[0]),
and a computed call with a number key (table[node.kind](node, cb),
arr[i](...)) calls the target's own and program members, never an
Object.prototype method such as __defineGetter__. Per unit, a
defineProperty / delete on Object.prototype in one unit now makes every
chain through it HARD in the other units too, as the whole program does, and
another unit's computed-key accessor on Object.prototype no longer turns
every fact off.

Carry allocation-derived typed-array owner, extent and metadata-effect proofs across declaration units and consume them in indexed loop regions. Admit fixed-bound nested numeric loops, certified numeric leaf calls, transparent HIR blocks and specialized byte stores while preserving guarded dynamic exits for unresolved storage, ranges and effects.
Verified numeric regions also opt their containing body out of the default size policy, allowing LLVM to schedule counted loops for execution speed without changing JavaScript arithmetic flags or adding a tuning threshold.
Keep finite arithmetic intervals through numeric regions and lower ToInt32 through signed-i64 truncation when those intervals prove it safe; nonfinite and larger values retain the complete conversion.

perry-runtime builds for wasm32 again: the signal-completion hook slot added
by #13312 names turnloop_proc, which does not exist on wasm32, so it is now
gated the same way as its installer and its only caller.

Format the WASI component generator's guarded future/stream regression cases.

Enable experimental async component resources with committed result ownership,
borrow lifetimes through suspension, and cancellation cleanup for compound
values. Package AsyncLocalStorage constructors and methods in the WASI runtime,
preserving native exports and removing obsolete Android stubs. Public async CLI
generation and guest future/stream values remain guarded.

Improve numeric typed-array loops with entry guards for live owner headers, scalar values, and integer index intervals. Admitted reads and writes share a call-free numeric region, including compound updates; unsupported effects and failed guards retain the original loop.

Reduce async task context copying and generator iteration bookkeeping. Task dispatch moves owned context snapshots while preserving AsyncLocalStorage restoration and guest task identity. Linux runtime handle scopes use direct TLS addressing, and default generator resumes write their pointer-free sent value directly into the canonical frame.

Repair release checks for static layout publication conflicts and aliased native class property reads.

Typed numeric function, closure, and receiver bodies now use the exact checked-integer remainder helper before falling back to floating remainder. This avoids unconditional libm calls for integer-valued operands while preserving fractional, non-finite, and signed-zero results.

Published functions that later reach unknown code also let that code retain their results. Per-unit facts now escape those results, including when publication came through a literal field, while preserving the result contract of known closed callers. A named function import read before its contract is published keeps its unknown value and tracks the owner function beside it, so handing it to unknown code reports escapes of its returned layouts to their owners.

A summary that only copies an unchanged parameter to its return now gives each caller its own argument back. This preserves function identity through helpers such as Struct.lambda without adding contexts, and prevents whole-program facts from merging unrelated callbacks. Spread calls and summaries with additional result sources retain their conservative transfer.

Add Turnloop-backed WASI 0.2 and 0.3 providers to the optional native component engine, with capability-scoped commands, files, clocks, randomness, sockets and HTTP. Keep the existing synchronous embedding API and the standalone WASI Preview 1 output profile. Vendor the required Turnloop provider repairs and preserve the repository-wide Tokio ban.

Add concurrent native component calls, typed Promise imports/exports, future/stream transport, ownership receipts and retained invocation contexts. Experimental generated async guests support interleaved calls, moving GC and cooperative cancellation; public async output and resource-bearing async guest profiles remain guarded while their remaining ownership contracts are qualified.

Include native host platform qualification, component-engine/WIT packaging, Windows object/archive discovery fixes, and executable acceptance harnesses. Document the Linux-qualified profiles and remaining macOS/Windows, async resource and final distribution gaps; historical receipts retain their exact source and artifact identities.

Bind generated guest layout images before allocating adapter frames, and resolve lazy Wasm runtime layouts through their constructor inventory. Fix Windows provider feature selection and native socket-option queries.

WASI executables now emit static atoms, layout descriptors and canonical seed key lists as image data, using linker section bounds to discover program records and avoiding startup key-list minting for imageable keys.

Remove the transitive Tokio dependency introduced by wasm-bindgen-futures
0.4.79 through printpdf's browser target. Pin that bridge to its last Tokio-free
release, 0.4.78, and resolve the matching wasm-bindgen packages together.

The dependency ban now checks every repository Cargo manifest and lockfile,
including nested fixtures, inactive target configurations, renamed dependencies
and future tokio-* packages. Offline checks run before Cargo, and full metadata
checks use --locked. An independent CI preflight runs on every PR and core run;
the final gate requires explicit success, including on docs-only PRs. Existing
Tokio-free WASI component work remains on its separate branch.

Native static layouts reuse canonical key lists from the program image, including class attributes and slot maps. Realm object and function headers now match heap births, and worker evacuation shares immutable lists while copying realm cells.

Fix WASI seed-table linking and first-use registration of runtime prototype layouts.

Avoid hashing default attribute entries when building dynamic canonical key lists.

Keep compiler layout spans ahead of the checked WASI runtime-descriptor enumeration when integrating current main.

A --target wasip1 program can use perry-stdlib and the Node module crates: the wasip1 runtime archive is built with the stdlib, the http, undici, websocket and zlib crates and the TLS-consuming modules bundled in, and the link installs only the features the program selects. zlib on wasm32 uses a Rust implementation of the same C ABI (native keeps zlib-ng), so dictionary streams, including mid-stream params() switches, behave as they do natively. --target wasi is unchanged.

Fixes

  • Per-unit whole-program facts finish on opencode (6,877 modules) again. On
    main the facts stage passed 43 GB in its third round and never converged
    under the 52 GB heavy cap; it now converges in 5 rounds with a 22.4 GB
    facts-stage peak (31.1 GiB for the whole compile). Six mechanisms, each
    fixed by rule:
    • A cell reader wrote back what its seed delivered: every reader of cjs:*
      (2,871 CommonJS wrappers) published the registry's 1,929 writes as its
      own. A unit's cell write is now what it writes itself; the cells still
      grow monotonically (DECISIONS 197).
    • Per unit, a CommonJS record's parent link read the registry union
      cjs:*. The requiring module's record is another unit's value: ⊤, as
      all other units' code is.
    • global:* was published ⊤ for global writes the unit only replayed from
      other units (Model::global_written_own).
    • Per unit, a computed require read the registry union. Its result is
      another module's exports, another unit's value: ⊤.
    • Every unit with dynamic code in reach re-escaped every other unit's thrown
      stand-in arrays through cell thrown (27M cell writes in round 3). A
      value another unit threw escapes in its thrower; the reader holds a
      stand-in only (Model::thrown_in, taken at seed time).
    • Function and class contract names used a shortened module path, so two
      versions of one package (jose 6.0.11 / 6.2.9), or files of one name in
      two directories, published one contract name twice. Their 13 readers
      were re-solved every round, so the solve never converged. Displays now
      name the full module path.
  • PERRY_FACTS_REPORT=1 prints a census line per dependency level and per
    round of the per-unit driver (what is held and published, and the RSS).

Whole-program facts: a module namespace now resolves a named key by name, as
ECMAScript ResolveExport does: the module's own export of that name, else that
name of each export * source (never default). A name imported through a
barrel module of export * lines used to hold every export of every source,
so each call through it ran every function the barrel re-exports. The
TypeScript sources (tssrc), which import through _namespaces/ts barrels,
passed the facts work bound and got no facts at all; they now converge (work
200M+ -> 28.4M, solve 94 s -> 8 s) and prove 19,908 of 84,731 sites.
Computed-key reads of a namespace still see every export.

Whole-program facts no longer conclude that a program may run Function(...)
when it reads a function out of an object under a computed key
(const fn = table[node.kind]; fn(node)). The facts collapse the builtin
prototypes into one object, so the constructor found on a plain object
literal's chain was "Object or Function", and calling it counted as dynamic
code: every argument escaped, and so did everything reachable from it. On
TypeScript's checker that put 17,274 sites behind "E14 dynamic code",
although TypeScript runs no Function or eval at all.

The constructor on a prototype chain is now the one that chain has: only a
function's chain reaches Function.prototype. An object literal's is
Object, an array's is Array. A builtin prototype that the program sets as
a value (__proto__, Object.setPrototypeOf, Object.create) may still be
Function.prototype. A computed key on an object literal also reads
Object.prototype's methods. A call of such a value runs each method's
effect on its receiver: valueOf returns it, and __defineGetter__ with a


These notes are truncated. This release carries 4090 changelog
fragments and the full text exceeds GitHub's 125,000-character limit for a
release body. The complete set is in changelog.d/ at v0.5.1664.


CI test gate skipped by the maintainer: Full-tier CI on main red for days (deny-warnings, wasmtime advisories). Verified: test build 41237952249 green; scoreboard 43/43; js-compiled 22/22;
opencode OK.

Don't miss a new perry release

NewReleases is sending notifications on new releases.