Chains of global one-character replacements (the HTML-escape idiom) run
as one scr_str_replace_chars call. It searched for the next match eight
bytes at a time and started the search over after each replacement.
It now reads 16-byte blocks with SSE2 or NEON (8-byte words elsewhere on
little-endian hosts) and replaces every match of a block before reading
the next one. The table of characters and replacements is kept for the
next call with the same immortal operands, which are the literals the
compiler passes, instead of being rebuilt from the replacement text on
every call.
- The number and whitespace scanners keep the cursor in locals and write
p->pos once: the runtime builds without strict aliasing, so a loop on
p->pos stored it after every byte.
- Typed arrays gather parsed elements in a 32-slot run and append them
with scr_arr_push_many instead of one push per element.
- The string scan shared by parse and stringify takes the index of the
first quote, backslash or control byte from the marked word on
little-endian hosts, and checks the last 1-7 bytes of a string as one
word, so a short plain string needs no byte loop.
- Typed record parsing computes once per record schema and parse which
field names hold no byte JSON would escape, instead of testing the
name on every direct key match.
- The mantissa converts to double through int64, and the positioned
error report stays out of line.
mentionsTypeParameter walked union constituents and type arguments
without remembering the types on its path. A parameter typed with an
alias that reaches itself through its own type arguments, such as
`type Tree = string | Tree[]` in `function f<T>(x: T, ...trees:
Tree[])`, recursed until the compiler ran out of stack. The walk now
skips a type it has already entered; a type parameter reached through
any other path is still found.
A dynamic method call on a class instance whose method name the program
also reads through a prototype (`X.prototype.map` anywhere) uses the
observed prototype slot: the callback descriptor read the method from the
instance's property bag, on the assumption that the bag answers every
read through the class prototype. An instance whose bag was built before
the class had prototype data reads as a fresh empty object, so the read
answered undefined and the call threw "... is not a function" (fast-check's
`integer({ min, max }).map(...)` once its result reaches the call through a
dynamic value).
The descriptor now reads the bag when the bag holds the name, own or
through its prototype, and otherwise the class's prototype data.
The TypeScript 7 parity lane checks every corpus program against a
recorded preflight and order baseline. 6950-string-field-fast-paths.ts
arrived without one; this records it from the current native lane.
When the Math functions evaluate their arguments into temporaries before
converting (an earlier argument whose ToNumber can run valueOf), a spread
argument's array is now copied where it is evaluated. Before, the
temporary held the array itself and its elements were read after the
conversions, so a valueOf that changed the array changed the values
passed, while JavaScript reads a spread's elements during argument
evaluation.
Number() of a string with an optional sign and 1 to 15 digits returns
the integer directly; every other string keeps the full StringToNumber
grammar, out of line.
split() with a separator collects its first batch of pieces before
creating the result, so the result is an inline array of exactly that
size, filled in place. A split's default limit skips the uint32
conversion, and pieces of a proven-ASCII string are created ASCII,
including the scratch pieces of split loops.
indexOf() from the start of a proven-ASCII string searches bytes
directly, and slice() of one skips the UTF-16 index map. The one-byte
case of the byte search reaches memchr without a stack frame.
- Port messages that a 'message' listener posts while more worker
messages are queued are delivered after those messages and before
setImmediate callbacks, as in Node. The ports' poll phase waits while a
batch's next message is queued, and the loop pass continues once more
after the batch when a port has something to deliver.
- A mailbox counts its queued events per kind as they are posted and
taken, so starting a batch reads the count instead of scanning the
queue under the lock. Messages that alternate with a worker's captured
stdout or stderr start a batch each, which made delivery quadratic:
20000 such messages took 5.6 s to drain, and now take 0.5 s (Node 0.57 s).
- The continue hook passes on that the batch's next message is at the
head, so the dispatch that follows does not lock the mailbox again.
- Corpus 6212 checks the order of worker messages, a MessageChannel
message posted by the first listener, and setImmediate against Node.
6211's costs are now taken from its CI timings.
markGray's edge visitor kept a stack frame on every call, though most
edges end at an object that is skipped or already gray. Those now return
without one, and the first visit and skipped cross-generation edges
tail-call out of line, as the scan visitors already do.
The walks no longer call the dense-array walker for an empty array. The
routing of nursery candidates past bypassed levels inlines its bypass
test, and the end of each pass tests its work vectors' capacity inline.
What is collected, and when, is unchanged.
A generic class instance now binds a type parameter to its checker type
only when a conditional type in the class depends on it, and that checker
type keys the instance. Before, every type argument was bound while only
literal arguments of those parameters were in the key, so two instances
such as Holder<R, "a"> and Holder<R, "b"> with a `T[K]` field shared the
first instance's layout. The other parameters get no checker binding, so
member types that depend on their literal identity stay unresolved, as
before.
Detection now follows the type aliases and interfaces a member's type
refers to, so a conditional wrapped in a union alias, such as
`(K extends "required" ? T : T | undefined) | null`, also separates the
instances.
Readable.push and Writable.write with a union chunk (Buffer, string and
null arms) read the chunk's tag and payload from a tagged union box. A
union with one reference arm and null, such as `string | null` or
`Buffer | null`, is the arm pointer itself, so `push(line)` with
`line: string | null` and `push(done ? null : text)` read past the
string and crashed. Both now read the tag and payload with unionTag and
unionPeek, as the other union consumers do. unshift and end refuse
union chunks and are unchanged.
tls.createServer validated minVersion and maxVersion with its own loop,
which threw ERR_TLS_INVALID_PROTOCOL_VERSION for null. It now uses the
same check as tls.connect, where null and undefined select the default
bound, as in Node.
charAt and s[i] on a string proven ASCII at an exact in-range index now
return the character from a module-level table of the 128 one-character
strings, emitted with the helper as immortal literals with the same
capacity word and key hash as the runtime's single-character statics.
An exact integer index (a counted-loop counter) skips the double
conversion. Every other string or index calls scr_str_char_at as before.
An array's element ops are a registry id in `spare` instead of a pointer
field: descriptors get their id on their first array (emitted descriptors
are writable for it), and a program with more element types than the id
holds keeps the rest in the array's out-of-line storage.
The out-of-line storage (sparse indices, noncanonical numeric properties,
match metadata) is no longer a header field. An array that has it keeps
its pointer in the 8 bytes before its dense allocation and sets
SCR_ARR_HAS_EXT; such an array always has a dense allocation of its own
(inline slots move out, an empty array gets the prefix alone), and every
allocation, reallocation and free of the dense storage accounts for the
prefix.
Loops over elements look the ops up once, and the state lookup below the
dense capacity reads no out-of-line storage.
The Math functions and String.fromCharCode/fromCodePoint evaluate all of
their arguments before they convert any, as in Node. When an argument's
conversion can run program code (valueOf or toString on a dynamic value)
and a later argument follows, the arguments now evaluate into temporaries
first and convert in order after (lowerToNumberArguments). Before, a
dynamic argument converted before the next one was evaluated.
A conditional argument whose branches are not both numbers boxes its
value and converts it like any dynamic value, so `isFinite(flag ? "12" :
"abc")` in a JavaScript file returns a boolean instead of reaching a
run-time fence.
A statement inside an expression can end its block (an argument call that
always throws). The expression's later statements and result now emit in
a block with no predecessors, so the instructions there read only values
defined in it.
A shape only checked conversions build keeps today's table; one an object
literal also builds takes the literal's order; a literal the IR mapper
rebuilds keeps its witness.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>