- 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.
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.
- 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.
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 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.
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_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.
The copy of a dense-array, byte-window, counted, callback or typed-array
versioned loop that runs when its guard fails no longer gives its loops
speculative integer or int32 kind copies, which emitted each loop again for
a path that rarely runs. Proven integer loops, which emit one copy, are kept
in it.
Linux executables linked through the clang driver with its default linker,
GNU ld. lld links the same objects in about half the time.
- Native and Node-hosted Linux exe links pass -fuse-ld=lld when ld.lld is
beside the driver's executable (where clang looks first) or on PATH, and
the default driver is in use (no SCRIPTC_LINKER, no sanitizer link).
- A link with lld that fails runs again with the driver's default linker,
whose error is the one reported when that fails too.