- toString("latin1"): counts the high bytes eight at a time, copies
words of ASCII bytes, and writes other bytes from a table of their
UTF-8 pairs without branches. ASCII-only results are marked ASCII, and
other results record their UTF-16 length (one unit per byte), so
`.length` does not walk the string.
- Buffer.from(text, "latin1" | "ascii" | "binary"): copies text known to
be ASCII, copies words of ASCII bytes, and decodes eight bytes of one-
and two-byte sequences in one word, storing each output byte without
branches. Longer sequences take the previous path.
- Buffer.from(text, "hex"): reads digit values from a table.
- toString("hex") and toString("base64"/"base64url") mark their results
ASCII, so a later decode, length or index read skips the scan for
non-ASCII bytes.
A typed array such as a number[] reaches untyped code as a capsule of the
native array. A keyed read `xs[i]` there missed the inline cache, which
answers only dyn arrays, and went to the member helper, which converted
the index to a string; the capsule's key lookup then parsed the string
back into a number for every element.
Keyed sites now read an element of a native number[] capsule in place
when the index is an integer below the array's hole-free prefix, and
`length` sites read the native array's length. Other capsule elements
(holes, out-of-range and fractional indices, arrays of other element
types) are answered by the keyed probe through scr_dyn_index_get without
forming the key, or by the helper as before.
Node's afterWriteTick sets afterWriteTickInfo to null before it runs the
callbacks, whichever tick it is. The runtime cleared the queued batch
only when the running tick was that batch, so a write made from an
earlier tick's callback joined a later queued tick with the same
callback and completed ahead of nextTicks queued between them. Any
afterWrite tick now closes the batch, and such a write queues its own.
Node's onwrite queues one afterWrite nextTick for a synchronous write
completion and lets later synchronous completions with the same callback
join it (afterWriteTickInfo.count); writes without a callback share one.
The step then checks 'drain' once, calls the callback once per write and
runs finishMaybe once. The runtime queued a stream tick and a nextTick
entry per write, which ran the callbacks of later writes after nextTicks
queued between them. The queued afterWrite tick now counts the writes
that join it, as Node's does.
Every read or write of a symbol-keyed property on a checked-dynamic value
formatted the symbol's address into a new string to find its storage key,
and every conversion of a symbol to a checked value allocated a new box.
Programs that key hot properties by symbols, such as Effect's type ids
checked on each combinator, spent a large share of their time there.
The storage key is a function of the address, so a 256-entry per-thread
cache by address now returns the existing string, released at exit. A
symbol also keeps a weak pointer to its live box: conversions share it,
and the box clears the pointer when it is freed.
A JavaScript class whose instances track which fields they have copies
the instance's property bag into its view first, attributes included,
and then wrote each symbol-keyed field with an ordinary assignment. Once
the instance was frozen, its symbol entry was read-only and the copy
non-extensible, so any later reflection that rebuilt the view
(Object.isFrozen, a property read) threw "Cannot assign to a read only
symbol property".
String-keyed fields already use scr_dyn_obj_set_field, which replaces a
field's value whatever the attributes copied into the view. The new
scr_dyn_symbol_field_set does the same for symbol-keyed fields, and the
view uses it when it copies the bag or carries integrity attributes.
Each deferred stream step (afterWrite, emitReadable, resume and the
others) queued a ScrStreamTick and a process.nextTick entry, both from
libc calloc and free. They now come from the runtime's small-block
allocator, as do the nextTick entries scheduled through
scr_next_tick and scr_process_write_callback.
The stream unit emitted each event with a newly allocated name string.
The names it emits ("data", "end", "drain", "finish" and the others)
are now immortal static strings, so an emit allocates no name.
- A function whose whole body is `return [a, b]` or `return { a, b }`,
with number, boolean or string fields, is now inlined where its result
is only destructured (`const [head, tail] = split(text, at)`). The
parameters are declared from the arguments, then each field straight
into its destructured binding, in the literal's order; an unread field
still evaluates into a binding of its own. No record is built, and a
slice field read only through its length or char codes becomes a
scalar slice that copies nothing. Arguments may own their value, but
as for numeric records nothing inside an argument may hold a
temporary of its own.
- A scalar slice of a proven-ASCII string computes its range inline:
the bounds clamp over the byte length exactly as scr_str_slice_range
does, and no boundary can split a surrogate pair. Other strings keep
the runtime call, which gains the same ASCII fast path.
- value-records' tuples section, `[head, tail] = split(...)` read only
through lengths, goes from two heap slices per step to none.
`s.unshift("e9", "hex")` on a native stream held as an untyped value
dropped the encoding argument and put the string's UTF-8 bytes at the
front. A byte-mode stream now takes a string chunk with an encoding as the
bytes Buffer.from(chunk, encoding) holds, as Node's
readableAddChunkUnshift converts it, and an unknown encoding throws
ERR_UNKNOWN_ENCODING. Object-mode streams keep the string as the entry.
- The assert comparison methods read as function values (equal,
strictEqual, deepEqual, partialDeepStrictEqual and their negations)
throw Node's ERR_MISSING_ARGS TypeError, `The "actual" and "expected"
arguments must be specified`, when called with fewer than two
arguments, where `[assert.equal][0]()` passed.
- Untyped `instanceof Object` starts at the value's prototype, so
Object.prototype itself, whose prototype is null, is not an Object.
- Untyped `instanceof Function` follows the whole prototype chain through
objects and functions: an object whose prototype is a function, or a
function whose prototype was set to such an object, is a Function.
uintptr_t is 32 bits on wasm32, where the weak-collection hashes shifted it by 33 and 56 bits. The hashes now widen keys, owners and kinds to uint64_t before mixing; 64-bit targets compute the same values as before.
scr_unwind.c is linked into every Linux executable, and its references
to _Unwind_ForcedUnwind and _Unwind_GetCFA pulled the unwinder out of
libgcc_eh (or Zig's libunwind) even for programs that never raise, which
grew a static hello world past its size ceiling. The references are now
weak: programs that raise reference the personality routine from their
landing pads, and its unit links the unwinder in.
A quotient-only BigInt division with a one-limb quotient returns Knuth's
estimate without a pass over the operands when the top limbs prove it:
with rhat * B + u0 - qhat * d0 at least qhat, a - qhat * b stays positive
whatever the lower limbs hold, since they subtract less than
qhat * B^(n-2). Otherwise one pass settles the estimate as before.
The add and subtract loops process eight limbs a pass, which rebuilds
the carry half as often. Left and right shifts, including the operand
normalization of the general division, form each limb from two source
limbs in a restrict-qualified helper with no dependency between limbs,
so the loops vectorize.
A child process has these members only when it was created with an IPC
channel (fork, or an "ipc" stdio entry), and keeps them after the channel
closes and after the child exits. The 'in' test on native handles answered
true for every declared member, so a child spawned without IPC reported
them and entered IPC-only branches. The three names now answer from the
runtime: scr_child_ipc_present reports whether the child was created with
a channel.
Corpus 8290 tests a forked and a spawned child before and after
disconnect and exit.
Dividing BigInts of the same limb count, where the quotient is one limb,
takes Knuth's quotient estimate from the top limbs as they read
normalized and settles it with one pass of a - q * b over the operands,
instead of copying both operands shifted and running algorithm D. The
remainder, when wanted, is that pass's result.
The limb loops for add, subtract, multiply by a limb, and multiply-add
and multiply-subtract by a limb process four limbs a pass, with the
carries in add-with-carry chains (__builtin_addcll and __builtin_subcll
where the compiler has them).
JSON.parse's SyntaxError messages were shaped like V8's but differed from
Node's in several ways:
- Every positional message now ends with V8's "(line L column C)", where
\n, \r and \r\n end lines.
- Positions are UTF-16 offsets, as in Node, not byte offsets.
- An object or array cut off after '{', a property name, a value or an
array element reports the token V8 expected ("Expected property name or
'}'", "Expected ':' after property name", "Expected ',' or '}' after
property value", "Expected ',' or ']' after array element") instead of
"Unexpected end of JSON input".
- After a comma in an object the message is "Expected double-quoted
property name".
- An unterminated string reports the end of the input, and a truncated
\u escape is a "Bad Unicode escape" there.
- A leading zero followed by a digit is an "Unexpected number".
- A misspelled literal names the first differing character, or the end of
the input.
- An unexpected character is named with the source as V8 does: whole when
it is under 21 units, otherwise ten units of context on each side; the
sources "NaN", "Infinity", "undefined" and "[object Object]" name only
themselves; a quote or a number there is an "Unexpected string" or
"Unexpected number".
The new corpus program 7993-json-parse-error-messages pins 76 inputs
against Node, including non-ASCII text before the error, line breaks, and
long sources with split surrogate pairs at the context edges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emitted functions report an exception by returning with the thread's
pending flag set, and every caller tests the flag after each call that
may throw. On Linux executables (glibc and musl), functions reached only
by direct calls now propagate exceptions by unwinding the native stack
instead:
- unwindAbiFunctions selects the may-throw functions that are not async
or generators, constructors, virtually called or overridden methods,
closure targets, the entry, library exports, module initializers or
callees of coroutine bodies.
- A native unwind costs about 2 µs a raise and 0.1 µs a frame, so functions that
exceptions are likely to cross keep the flag: those with a `throw` of
their own, those up to three calls above one, and recursive functions,
counting calls through methods, closures and callbacks.
- Their propagating exits call scr_unwind_raise (scr_unwind.c), a forced
unwind through _Unwind_ForcedUnwind with the exception still in the
active cell.
- Direct calls to them are `invoke`s without a pending check. The
landing pad runs the frame's usual unwind path: it releases the frame's
values and then enters the enclosing try handler or resumes the unwind.
- Remaining plain calls to them (wrappers, adapters, runtime-facing entry
points) become invokes whose landing pad continues on the pending flag,
or resumes when the wrapper is itself invoked. Every other call is
marked `nounwind`, so inlining a raising function at an invoke keeps its
calls plain.
- When emitted text breaks these rules (a raising function's address is
taken, or an invoked function returns with the flag pending), the
module is emitted again with those functions on the pending flag.
Emitted code does not declare scr_unwind_raise `noreturn`, so sanitized
builds do not run the sanitizer's no-return hook, which needs stack
bounds that fiber stacks do not report. Instead, scr_unwind_raise clears
the shadow of the frames it unwinds.
Executables link a static unwinder: libgcc_eh with the host toolchain and
the glibc runtime packs, and Zig's libunwind with Zig and the musl packs.
macOS, Windows, Android, WASI, libraries and debug builds keep the
pending flag everywhere and emit the same code as before.
The throw-chains runtime workload measures exceptions caught 2, 5 and 20
recursive calls above the throw, through direct calls and a callback,
and a parser that backtracks by throwing.
A pipeline whose first stage is a checked-dynamic value now checks it at
run time, as Node's stage ladder does: a stream is piped as before, and a
string or a value with a Symbol.asyncIterator or Symbol.iterator method is
pumped into the next stage through an async generator over its iterator.
A typed Set, Map, generator or async generator object as the first stage
takes the same path. This covers the callback and stream/promises forms,
with or without function stages and pipeline options.
A write whose callback fails synchronously no longer sets needDrain, so no
'drain' follows the error and a pumped source stops at the failed write,
as in Node.
RUNTIME_ABI_VERSION goes from 37 to 42 for scr_stream_dyn_iterable_source.
Reapplies #2017, which #2038 reverted after effect's observability-otlp
fixture crashed in plain builds. The crash was the candidate-buffer
corruption that the previous commit fixes: the cached property sites
change when the collector runs, and with enough environment variables a
pass buffered a string box that was later pooled while still buffered.