Commit Graph
3121 Commits
Author SHA1 Message Date
Chris Tate 5bb9fc556d Run Test262 tests that include testTypedArray.js (#2031) 2026-10-23 14:59:25 -05:00
Chris Tate adb8690f11 Cache untyped property access in programs that declare classes (reland) (#2017) 2026-10-23 14:47:10 -05:00
Chris Tate 807d281baf Support raw key formats, X25519 keys and crypto.diffieHellman (#2029) 2026-10-23 14:40:52 -05:00
Chris Tate 89d122574f Run Test262 tests that include testTypedArray.js 2026-10-23 14:27:20 -05:00
Chris Tate e7c2462e7a Rule out global constructor names by their first byte (#2028) 2026-10-23 14:27:01 -05:00
Chris Tate 3a5f563e70 Iterate untyped arrays without the generic iterator protocol (#2027) 2026-10-23 14:18:14 -05:00
Chris Tate 693e33742a Support the raw-public and raw-private key formats
EC and Ed25519 keys read and write Node 24's raw formats:

- generateKeyPair(Sync) encodings: raw-public gives the EC point,
  uncompressed or, with type "compressed", compressed, or the Ed25519
  public key; raw-private gives the EC scalar at the curve's size or the
  Ed25519 seed.
- KeyObject.export({ format: "raw-public" | "raw-private" | "raw-seed" }),
  with Node's check of the point type, and Node's error for a passphrase
  with any format other than PEM and DER, which now also covers JWK.
- createPublicKey and createPrivateKey of { key, format,
  asymmetricKeyType, namedCurve }, in Node's argument order, with
  compressed EC points, and createPublicKey of a raw private key.

As in Node, raw-seed and every raw format of an RSA, RSA-PSS, DSA or DH
key throw ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, which generateKeyPair
passes to its callback. Invalid key data throws ERR_INVALID_ARG_VALUE.
Raw X25519, X448, Ed448 and post-quantum keys, EC scalars of 0 or at
least the curve order, and the EC point at infinity, which Node accepts,
throw a coded SC2020 error.
2026-10-23 13:59:28 -05:00
Chris Tate 47636f2fa5 Implement the identity TransformStream natively (#2009) 2026-10-23 13:51:52 -05:00
Malte UblandClaude Opus 5.5 c81ee69da7 Rule out global constructor names by their first byte
A global constructor read as a value in untyped code (zod's
`data instanceof Map`, `Set`, `Date` in getParsedType) resolves its name
at run time: Function, then the Error constructors, then the other
globals, each by strcmp. Every miss in the Error and global tables cost a
call, about a dozen per read.

Each table entry is now skipped unless its first byte matches the name's,
before the strcmp. The tables, their order and the values they return are
unchanged.

zod 3, z.array(Order).parse over 500 orders: 187.3M -> 183.2M
instructions per round (-2.2%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:49:36 -07:00
Chris Tate 6fbac8a9a3 Run typed arrays that the checker types any on their native bytes (#2023) 2026-10-23 13:46:39 -05:00
Chris Tate 40de8bf974 Freeze records that other references still hold (#1963) 2026-10-23 13:39:48 -05:00
Malte UblandClaude Opus 5.5 26d2c8bae1 Iterate untyped arrays without the generic iterator protocol
for...of, spread and destructuring over an untyped array opened the
iterator by calling the builtin values() function through scr_dyn_call
with the array as `this`, and stepped it through the general element
path: retain the source, compute the live length, read the element with
hole handling, release the source.

An array whose iterator method is the builtin values() now gets the
native iterator that call returns directly. A step over a plain array
whose index is an own element, or past the end, answers without the
general path; no getter can run there. Holes, keys(), entries(), typed
array capsules and every other source keep the general path.

zod 3, z.array(Order).parse over 500 orders: 187.3M -> 182.2M
instructions per round (-2.7%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:33:28 -07:00
Chris Tate 9f4e654e7a Evaluate an element assignment's receiver and index before its value (#2018) 2026-10-23 13:32:38 -05:00
Chris Tate 8b6d3329f4 Hold an element assignment's receiver against its index's effects
When the index assigns the receiver's binding (`a[(a = b, 0)] = c[0]!`),
holding the index alone left the receiver read at the store, after the
index ran, so the value landed in the replacement array. The receiver is
now held whenever the index or the value could change what it reads.
2026-10-23 13:00:50 -05:00
Chris Tate c7fd9dcdf0 Merge main 2026-10-23 12:59:08 -05:00
Chris Tate 0af86e2e3e Version two-counter loops over typed arrays
A loop with two counters closing in on each other, such as
`for (let i = lo, j = hi; i < j; i++, j--)`, checked the bounds of every
typed-array element it read or wrote at either counter. Within the body
the low counter only rises and stays below the high one, which only
falls, so every such index lies between the two starts.

Typed-array loop versions now cover these loops. The guard tests that
both starts are exact integers, that the low start is at least 0, and
that the high start is below the length of each view the loop indexes at
a counter. The fast copy then reads and writes those elements without
checks, and the checked copy runs whenever the guard fails. In the fast
copy the guard also stands for the loop's own start test.

The exact-integer test of a start is now one function, shared by the
typed-array version guards and the dense array two-counter guard. The
dense guard, which compared the round trip with `fcmp oeq`, now also
leaves a -0 start to the checked loop.
2026-10-23 12:58:05 -05:00
Chris Tate 49e35b6755 Answer untyped property reads of plain values without the class tables (#2013) 2026-10-23 12:38:38 -05:00
Malte UblandClaude Opus 5.5 cda584260f Run typed arrays that the checker types any on their native bytes
A JavaScript binding referenced by its own initializer's inference
collapses to `any` in the checker while it still lowers to a Uint8Array:
hono's etag digest declares `const nextChunk = new Uint8Array(new
ArrayBuffer(f(chunk)))` beside `chunk = nextChunk`. The typed-array
lowerings key on the checker type and declined such a receiver, and the
dynamic paths required a dyn value, so `nextChunk.set(...)` and
`nextChunk.byteLength` threw SC1090/SC2020 at run time and multi-chunk
etag digests failed.

Method calls and property reads on such a receiver now box it by
reference (the box shares the native storage, so writes stay visible) and
dispatch on the runtime kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:33:05 -07:00
Chris Tate d891a245ab Clone Response and Request bodies by teeing their streams (#2008) 2026-10-23 12:16:46 -05:00
Chris Tate a36ef0b4ca Settle aborts and pull TransformStream uploads as Node does 2026-10-23 12:11:48 -05:00
Chris Tate 4dcb83c418 Free a typed teardown's blocks in the order it met them (#1849) 2026-10-23 12:09:37 -05:00
Chris Tate d8816b4731 Evaluate an element assignment's receiver and index before its value
An element assignment whose value is an optional element read or call,
such as `out[n++] = src[n]!`, held the value in a temporary and then
evaluated the receiver and index inside the store, after the value. So
`n++` ran after `src[n]` was read, and `src[0]` was stored where Node
stores `src[1]`. The receiver and index are now evaluated first whenever
the value could change what they read: when it assigns a binding they
read, or runs code that can assign their `let` and `var` bindings or
fields. A literal, or a binding the value cannot change, is still read
at the store.
2026-10-23 12:05:34 -05:00
Malte Ubl 00fc647ca1 Merge remote-tracking branch 'origin/main' into perf/dyn-plain-property-reads 2026-10-23 10:05:34 -07:00
Chris Tate bd36b77a8f Cache untyped property access in programs that declare classes (reland) 2026-10-23 11:57:22 -05:00
Chris Tate dc1d99835d Merge main 2026-10-23 11:54:20 -05:00
Chris Tate 7eb90051a2 Give corpus scratch paths a per-process name (#2010) 2026-10-23 11:42:17 -05:00
Chris Tate 891a1a46a9 Keep fixed layouts for JavaScript classes next to freezes and unrelated accessors (#2012) 2026-10-23 11:26:42 -05:00
Malte UblandClaude Opus 5.5 a77fe8c21c Decide an untyped property write with one own-property lookup
scr_dyn_object_key_set, behind every untyped property write and every
member of an untyped object literal, looked the key up as many as five
times: a linear scan for an own accessor or read-only entry, a lookup
before the prototype walk, one before the non-extensible check, one to
know whether the key was created, and one more inside the store.

The own property is now looked up once. An own data property is written
in place (an own setter still runs; a read-only or getter-only property
still refuses), and an absent key walks the prototype chain as before
and is then added by scr_dyn_obj_set, which is unchanged.

zod 3, z.array(Order).parse over 500 orders: 199.8M -> 194.1M
instructions per round (-2.8%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:24:47 -07:00
Chris Tate 6828c23b50 Error tee branches when the source errors with no read pending
The tee learned a source error only through a pending read's error
steps, so a source that errored while both branches held buffered
chunks and no read was pending left the branches readable: the next read
returned a chunk where Node rejects. The tee now watches its source's
error, as the streams spec reacts to the source reader's closed promise,
and errors both branches in a microtask (resolving the cancel promise
unless both branches were canceled). A read's error steps only end the
read, and a clone of an already errored body errors both branches the
same way.
2026-10-23 11:20:05 -05:00
Malte UblandClaude Sonnet 5.5 fab4b403e9 Give corpus scratch paths a per-process name
The differential and LLVM differential suites can run the same corpus
program's Node oracle at the same time in one shard. Programs whose
scratch directory or file was derived only from argv[1] collided, and
one oracle removed the paths under the other. Their names now include
the pid, as 1403 and 5567 already do. Printed output never carries the
name.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 09:07:41 -07:00
Chris Tate 5f22ee7146 Run two-counter loops and element copies over number arrays without per-element checks (#1991) 2026-10-23 11:00:35 -05:00
Chris Tate 6e849eeaef Read string-keyed record entries without allocating under ?? and ! (#2007) 2026-10-23 11:00:10 -05:00
Chris Tate 2d4aac0976 Read Web Headers pairs without copying them, and cache their sorted list (#2000) 2026-10-23 10:50:13 -05:00
Malte UblandClaude Opus 5.5 3c2911ccad Implement the identity TransformStream natively
'new TransformStream()' had no lowering, so hono's stream() and
streamSSE() helpers failed to compile their checked-dynamic paths. The
constructor without arguments now builds a native handle: its readable
side is a native ReadableStream with a zero high-water mark, and its
writable side runs the streams spec's WritableStream state machine
(getWriter, abort, close, locked; the writer's write, close, abort,
releaseLock, ready, closed and desiredSize). A write completes when the
readable side reads its chunk, close/abort/cancel propagate across the
pair, and deferred reactions take as many microtask turns as in Node.
Web ReadableStreams can also be weak collection keys (hono keys a
WeakMap by the response stream). A transformer, queuing strategies and
the WritableStream constructor keep the SC2020 refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:47:47 -07:00
Malte UblandClaude Opus 5.5 e84f656172 Clone Response and Request bodies by teeing their streams
Response.clone() and Request.clone() on checked-dynamic handles answered
'res.clone is not a function' (hono's etag middleware hashes
res.clone().body). The runtime now implements the streams spec's
ReadableStreamDefaultTee over native streams and clones the body with it,
so the original and the clone each read every chunk. A used or locked body
throws undici's TypeErrors, the clone's headers are a copy with the same
guard, and a cloned Request's signal follows the original's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:47:43 -07:00
Chris Tate 998a385bde Mark constructed headers canonical only when every value is trimmed
The Headers and Response constructors combine duplicate entries (a: x,
a: "") into one value with the separator, which can end in its space
("x, "). They marked the pairs canonical anyway, so a copy took them
unchanged where Node trims the copied value ("x,"). The constructors
now mark the pairs canonical only when every value is already trimmed;
otherwise copies normalize them pair by pair.
2026-10-23 10:45:26 -05:00
Chris Tate ecbfa766ef Store number arrays of proven int32s in 4-byte slots across direct calls (#1960) 2026-10-23 10:42:51 -05:00
Chris Tate c11253b228 Reject other classes' capsules inline in class-table brand probes (#2001) 2026-10-23 10:35:30 -05:00
Chris Tate 5fd47eac77 Search proven-ASCII strings backwards by byte in lastIndexOf (#2002) 2026-10-23 10:34:16 -05:00
Malte UblandClaude Opus 5.5 1f7677d246 Answer untyped property reads of plain values without the class tables
Every untyped property read or write in a program with dynamic-tier
classes calls a generated helper (%dyn.class.property.N). For a value that
is not a class instance, the helper still called the normalization, the
instance-bag and the static-bag tables, each of which returns the value
itself, then compared the bag with the value and finally read the key.

When every class branch of a helper tests a capsule key or a class key,
its fallthrough is still the access it was created with, and every table
that fallthrough calls has its kind exit, a value that is neither a class
capsule nor a function now gets the same access on itself at the top of
the helper. Helpers for `constructor` and `toString`, which answer
primitives and errors specially, keep their full body.

zod 3, z.array(Order).parse over 500 orders: 235.8M -> 199.8M
instructions per round (-15.3%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:26:28 -07:00
Chris Tate 11426b4fc2 Read string-keyed record entries as stack unions under projections
A keyed read of a string-indexed record whose values are numbers or
booleans (`counts[key]` on a `Record<string, number>`, typed
`number | undefined`) allocated a heap union for every read, also when
the only consumer was `??`, `!` or a comparison with undefined. The
runtime already returns the entry's value and whether it is present;
the box came from wrapping that result.

Such a read is now a stack union source: under a projection, the read
writes the tag and bits of the declared field or index-signature entry
the key names, or the undefined arm for a missing key, into a private
stack box, and allocates nothing. This applies when every hit is a
number or boolean arm of the result union, or an inline field of the
same union, and also to an argument a callee only projects (the helper
that `x!` lowers to). Other consumers keep the heap union.

dictionary-walks allocates no union instead of 363,359 and runs 14.6%
faster; no other runtime benchmark's code changes.

Corpus 7390.
2026-10-23 10:26:22 -05:00
Chris Tate b6e8e149b5 Trim combined header values again when copying after an empty append
Appending an empty value to an existing header leaves the separator's
space at the end of the combined value ("x, "). Node keeps that in the
Headers object but trims it when the value is copied into another
Headers, Request or Response ("x,"). The copy fast path copied a
canonical object's pairs as they were, so the copy kept the space. Such
an append now marks the pairs as no longer canonical, and copies
normalize them pair by pair.
2026-10-23 10:25:37 -05:00
Chris Tate 779893eef8 Address review feedback on int32 arrays across calls 2026-10-23 10:15:37 -05:00
Malte UblandClaude Opus 5.5 a705a3fd09 Reject other classes' capsules inline in class-table brand probes
A class table (property bags, normalization, computed reads) tests the
receiver's capsule key against every compiled class in turn. A capsule
boxed by another class has another key pointer, and each such probe called
scr_dyn_typed_ref_is_key to compare the names. The probe now rejects it
inline when the key length or its last eight bytes differ, and calls the
out-of-line comparison only for a likely match, so capsules boxed with an
equal key elsewhere still match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:04:00 -07:00
Malte UblandClaude Opus 5.5 7b8a07e0f2 Search proven-ASCII strings backwards by byte in lastIndexOf
lastIndexOf built the receiver's UTF-16 index map and converted positions
both ways even when the receiver is proven ASCII, where byte offsets are
UTF-16 indexes (indexOf already has this fast path). A short id such as
"bk_00042".lastIndexOf("_") cost about 200 instructions; the Hono
fixture's list endpoint does it 80 times per request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:04:00 -07:00
Malte UblandClaude Opus 5.5 056dda3633 Read Web Headers pairs without copying them, and cache their sorted list
Headers kept its name/value pairs in a string array and read every element
through scr_arr_get_ref (index checks, retain, release). forEach and the
iterators re-sorted and rescanned the whole list for every step, and every
Response or Headers built from another Headers object re-validated and
re-normalized each copied pair. The Hono fixture spends 12-15% of a request
there.

- Pair loops read borrowed elements of the dense array (sf_pair_at).
- The sort-and-combine list is cached on the Headers object and dropped by
  every mutation, so live iteration keeps Node's semantics.
- get() of a single value returns the stored string.
- Copying a Headers object whose pairs are canonical (built by the
  constructor or a Response) into an empty list copies the pairs; wire
  response headers keep the per-pair path because they may repeat names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:03:59 -07:00
Chris Tate e771de1cfb Check cycle bindings only where early code can reach the reading function (#1882) 2026-10-23 10:02:29 -05:00
Malte UblandClaude Opus 5.5 7c33b55435 Keep fixed layouts for JavaScript classes next to freezes and unrelated accessors
Two rules sent every JavaScript class family to field-creation tracking:

- Any Object.freeze, Object.seal or Object.preventExtensions of a value
  other than a fresh literal. These never create or remove a property, and
  a class instance keeps its integrity state beside its fields: a typed
  call sets the CLASS_ATTRIBUTES word that TypeScript classes use with the
  same fixed layouts, and through an untyped value the runtime refuses the
  operation on every class instance (SC2020). Only Reflect.deleteProperty
  stays a removal of any name.
- Any getter or setter. An accessor only matters when it shares a name
  with a field of the instances that see it, or when a subclass accessor
  can intercept a base constructor's field assignment. Other accessors
  live on the prototype beside the fields.

In zod 3, ParseStatus and ParseInputLazyPath now keep fixed layouts (2 of
the 39 tracked classes; the ZodType family stays tracked because its
constructor creates own properties before _def). z.array(Order).parse
over 500 orders: 270.7M -> 235.8M instructions per round (-12.9%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 07:39:29 -07:00
Chris Tate 0f8dd276d9 Reach a class's methods from its class expression's inner name in static initializers
A named class expression's own name (const C = class Impl { static x =
Impl.m(); }) names the class inside its body as the binding it is stored
in names it outside. The reachability walk followed only stored binding
names, so a static initializer calling a method through the inner name
did not reach that method, and its early reads of the other cycle
member's bindings lost their TDZ checks: the program read the binding
before its declaration ran (a crash), where Node throws ReferenceError.
The inner name now maps to its class expression.
2026-10-23 09:24:28 -05:00
Chris Tate 885a829959 Compile minified npm packages such as marked (#1989) 2026-10-23 09:07:41 -05:00