- Intl.Segmenter accepts an options object literal made only of
`granularity` entries again, including the empty `{}` main accepted;
with none the segmenter is a grapheme segmenter, and a repeated key
takes its last value, as in JavaScript.
- Namespace reads of a CommonJS module refuse a name that any module it
re-exports assigns inside a function, not only the module itself:
Node's namespace keeps the values from the end of evaluation.
- The named-import link check treats `module.exports` as an export every
CommonJS module provides, as Node 24 does, instead of throwing its
missing-export SyntaxError.
- The npm cases cjs-esm-named and cjs-lexer-aligned, which the batch no
longer refuses at build time, are listed with the run-time SC1090 their
CommonJS exports-object reads report.
- A speculated write with hoisted calls resumes over its stored call
results only when its own check failed. Reached in the resume copy
from an earlier write's failed check, its calls never ran on that
iteration, so it now runs as written: the calls happen once and the
arithmetic reads their fresh results.
- A literal divisor is a power of two only when it is exactly 2^k with k
in [0, 52]. Math.log2 rounds 2^52 + 1 to 52, so `Math.floor(v / (2^52 + 1))`
was emitted as a shift by 52.
- A helper expands only when every call in its body is of another helper
that expands, so an expansion never moves a call with effects ahead of
an argument read. Hoisting already refused such writes; the
classification now says so itself.
- A built-in typed-array constructor set as a view's `constructor` is its
own species, as the getter it inherits from %TypedArray% returns it:
`u8.constructor = Float64Array; u8.map(f)` makes a Float64Array. An own
Symbol.species on the constructor still wins.
- TypedArray.prototype.slice with a species constructor creates the
result first and then copies the source's current elements, up to its
current length: a constructor that changes the source is seen.
- Object.defineProperty on a typed-array element reads the descriptor as
ToPropertyDescriptor does, through its getters and in its order, and
throws a TypeError for a primitive descriptor or an accessor with a
value. The generic object path shares the same reader.
`const X = require("crypto").X509Certificate; new X(data)` now resolves
to the builtin class, so the standard-library constructor fence reported
SC2020 for it before the X509Certificate lowering ran. That lowering now
runs ahead of the fence. Corpus 8231 constructs a certificate through the
binding.
`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.
An instance's `constructor` read that reaches a class prototype's own property now reads it with the
instance as the receiver (Reflect.get), so an accessor's getter sees the instance as `this`, as in
Node, instead of the prototype.
A member read on a `new F()` instance looked up the name only among the
fields of F's prototype objects. A method stored under a computed key
(`F.prototype[name] = fn`) is kept in the prototype's unknown-key field,
so the read found nothing: the method's body was never visited, and the
objects it writes stayed constant in the report. The read now also takes
the prototype's unknown-key field, as reads of the object's own fields
do.
A JavaScript prototype method value runs over the call-site receiver when its private members are
fields (brand-checked reads and writes) or private methods it calls. A private accessor, or a private
method read as a value, would read a public property of the same name, so such a method keeps its
native receiver, as before. A super method call with spread arguments over the call-site receiver
now spreads them into the dynamic call instead of rejecting them.
fs.read and fs.readSync with arguments the compiler could not type
validated the length with `length |= 0` and then converted it again for
the read. A length with a valueOf method ran valueOf twice, and the read
used the second result. Node converts the length once.
scr_fs_read_validate now returns the converted length, and both reads
use it.
- 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.
A union without recursion builds its matched arm without a visit
session, so its getters and proxy traps ran without marking the
conversion. A nested union then took its arm from the earlier match,
even when the getter returned another value. A getter or trap that runs
during a trusted build outside a session now marks it as well, and the
outermost trusted build clears the mark when it ends.
An optional read tested against a one-unit literal is replaced by a test
of its code unit, built from the read's receiver and index. When that
receiver or index tested a held character local, as in
`src[c === "a" ? pos + 1 : pos] === "b"`, the inner test was kept as it
was and still compared the local as a string|undefined union while the
local's binding became a number, which failed IR validation. The same
held for a held local's own initializer. The receiver, index and temps of
both reads are now rewritten first.
lastIndexOf with a needle of the element type read every non-hole index
as the element type. An undefined or foreign element written through a
dynamic view (or a hole that toSorted turned into undefined) has no
element-typed payload, so the read aborted with "array index N is
undefined". The search now reads an index only when it holds a value,
and skips undefined and foreign elements, as Node's strict equality
does for a string or number needle.
A reference in `export { m }` resolved to the export's alias symbol, not to
the local binding, so the inference did not see the export and typed the
collection from its local uses. An importing module then used it as an
untyped Map and failed to compile (SC9001). An unrenamed export specifier
now resolves to the binding it exports, which refuses the inference as
`export const` does. Corpus 7985 adds `export { listed, renamed as alias }`.
The product of two exact integers was treated as exact everywhere when one
of them was a constant power of two, tested with Number.isInteger(Math.log2).
Math.log2 rounds to 52 for 2^52 - 1, so a product by 2^52 - 1 that passes
2^53 was compared as its integer instead of its rounded double. The test now
compares 2 ** Math.round(Math.log2(value)) with the value. Corpus 7983 adds
products by 2^52 - 1 compared with products by 4.
The string comparison emission test matched only x86-64 `call`
instructions to unprefixed symbols, so on AArch64 and macOS hosts it
counted no calls and failed its positive-count check. It now also matches
AArch64 `bl` and Mach-O's leading underscore, and it runs for the host and
for aarch64-linux-gnu and x86_64-macos assembly, so every host checks all
three spellings.
The constant-graph pass treats a function whose call edges all run once
at initialization as init-only. Some calls recorded no edge or a
once-only one:
- `new F()` of an ES5 constructor and `f.apply(thisArg, args)` bound the
arguments without a call edge, so a function also called directly once
was init-only even when a loop constructed or applied it.
- Callbacks of Array.from, toSorted, replace and replaceAll run once per
element, comparison or match, but those methods were missing from the
repeating callback set.
- JSON.parse revivers, JSON.stringify replacers and toJSON methods run
once per value; their edges at the JSON call site now count as
repeated, including toJSON calls resolved after the call's handling.
Objects these functions write or allocate were reported as constant, and
their reads as foldable. constant-graphs.test.ts covers each case.
An untyped pipeline source that is a sync iterable passed each value on
as it came, so a promise reached the next stage as the promise: a
byte-mode destination refused it and an object-mode one received it.
for await over a sync iterator, which Node's pipeline pumps it with,
awaits each value. The source's generator now awaits a value that is
an object (a promise included) before yielding it, and a rejection
closes the source, as Node's async-from-sync iterator does.
The match report counted the functions with profile counts after the
whole pipeline, so an annotated function that inlining or dead code
elimination later removed was not counted, while the annotation's
mismatch and missing-function warnings were. A pass-instrumentation
callback now counts the annotated functions right after
PGOInstrumentationUse runs, in the one-module and the partitioned
pipelines.
A new test builds helpers that reach the annotation and are then
inlined into main and deleted, and checks that the report counts them.
Staging stat'ed an artifact's path, read the path, and stat'ed it again.
When the path was a symbolic link switched to another file of the same
size during the read and back before the second stat, both identities
were those of the verified file, so the bytes of the other file were
staged without hashing. Staging now opens the artifact once and takes
both identities from the open file with fstat, so they describe the
file the bytes come from.
A test switches the artifact's link to a same-sized file during the read
and checks that the staged copy has the verified bytes.
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.
A run that a signal ends has a null exit code, so the runner recorded a
pass when Node and the executable ended by different signals, or when one
ended by a signal and the other with an exit code compared as null. The
verdict now compares both the exit code and the signal (verdict.mjs), and
records Node's signal in nodeExit as it does for the executable.
A relative --node path also made the minimal lane's fixtures symlink
relative to the copied tree, where it does not resolve. The checkout path
is now resolved first.
tests/harness/node-parallel-runner.test.ts covers both: the termination
comparison, and a relative --node path whose fixtures resolve from the
minimal tree.
The exactness test for a product's 32-bit form multiplied the operands'
magnitude bounds in a double and accepted a result of at most 2^53. A
bound of 2^53 + 1 (3 * 3002399751580331) rounds to 2^53 there, so a
product the double rounds took the integer multiply's low bits:
`(3 * 3002399751580331) | 0` printed 1 where Node prints 0. The test is
now strict: a bound below 2^53 is exact, and no bound of 2^53 or more
rounds below it.
scriptc_ts7_json_value_end searched for the next quote again after each
backslash escape in a string, so a string with many escapes was scanned
once per escape. The quote it found now stays the candidate end until an
escape skips it, and each byte of a string is scanned once by each
search.
A test scans strings of 2,000,000 newline, backslash and quote escapes,
alone and inside an object, and compares the ends with jsonValueEnd's;
before the change it does not finish within 60 s.
SCRIPTC_DEBUG_SHAPES=const reports the constant object graphs shape
inference finds (DESIGN-constant-objects.md, stage 1): objects created by
code that runs at most once (module top level, IIFEs, functions reached
only through one call site from such code) whose fields are written only
there, that never meet a reflective or computed write, and that never
escape. Namespaces (`var ns = ns || {}; ns.util = {}`), revealing
modules, `module.exports` tables, and ES5 constructors' prototype objects
qualify. Each near miss names its rule: allocated repeatedly, created by an
init factory (one object per call site), escapes, reflective or computed
write, literal form, written after init, or lazy cache. The report counts
the member reads and calls a later stage can fold.
The analysis now models ES5 constructors (a function's prototype object,
instances reading through it) and links CommonJS require() and export
bindings to the analyzed module's exports, which it previously read as
nothing. Code generation is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Loading a program with --npm-static parses package files on their own
before the checker reads them:
- The declaration closure of each named package was walked twice, once for
class overloads and once for class properties, and the one-entry parse
cache could not serve the second walk. One walk now reads both from each
parse.
- The nullable class field and Array.find return rewrites parsed every
package JavaScript file. They now parse only files whose text contains
`class` and `null` (or `find`), or a Unicode escape that could spell
them.
- The JSDoc namepath rewrite parsed every file containing `#` or `~`. It
now parses only files with a namepath segment joined by one of them
(`a#b`, `a~b`), the pattern the rewrite replaces.
The rewritten sources are unchanged.
planNpmStaticReexports visited a file again each time another of its
export names was demanded. Every visit resolved the file's import and
export declarations again and walked its whole syntax tree for import()
and require() calls. The purity check kept only pure trees and the failing
root, so once a walk reached an impure file, the next namespace re-export
walked the same files again.
Each file's edges are now collected once per plan; the calls come from
moduleSourceCandidates, which fork and worker discovery already fill. A
repeated visit re-checks only the file's namespace re-exports, the only
edges whose pruning depends on the demand. Purity is decided per strongly
connected component of the module graph and kept for every file, and a
walk that reaches an impure file marks every file on its stack impure.
The pruned re-exports and the module order are unchanged.
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.
tsgo checks JavaScript calls to an untyped Promise's resolve against its
one parameter, so resolve() with no argument is an error there while
tsc-ts accepts it. The two programs pass undefined, which both checkers
accept and which settles the promise the same way.