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.
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>
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.
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.
- Named loads also read what was written under that name or an unknown key where an own field
shadows a builtin: an instance's constructor, a function's call, apply, bind, prototype, name
and length, a class's prototype and name, an array's methods. Named loads of arrays read what
was written under unknown keys, which computed stores on arrays now record.
- Computed stores into builtins, promises and collections escape, as named stores there do.
- JSON.stringify calls toJSON on arrays, functions and classes, and on values written under
unknown keys. A JSON.parse reviver's results are part of the result and of later calls' values.
- Reflect.construct(Proxy, ...) and globalThis.Proxy record a proxy operation, as new Proxy does.
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.
SCRIPTC_DEBUG_SHAPES=1 runs a whole-program allocation-site points-to
analysis over opted-in npm packages' JavaScript and the user files that
call into them, and prints which object and array literals a native
record would represent exactly, the inferred class field types, the
census of writes and reflection on untracked receivers, and the hot
functions' parameter and return types. Report only: nothing reaches the
lowering, and the analysis runs only when the variable is set.
Design: results/design-shape-inference/DESIGN.md (scriptc-perf).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
m.set(k, f(m.get(k))) and a get followed by a set of the same key, with a
number or boolean value, now reuse the entry the read resolved, as
string-keyed maps already do: the write overwrites the value in place
when the map is the same and the key has the read's bits, and takes the
ordinary set otherwise. Before, the write probed the table again: the two
lookups were not merged, because each sits behind its own direct-index
test.
A warm native build validates its frontend cache entry by reading and
SHA-256 hashing every source and declaration file the frontend read.
A tracked read now also records the file's identity (device, inode,
size, modification and change time) when the identity is the same before
and after the read and the file had been unchanged for two seconds. A
later check accepts a file whose identity is unchanged without reading
it, and hashes it otherwise. A write always gives a file a newer change
time, which tools cannot set back, so an unchanged identity means the
content hashed with it. The equivalent-edit check uses the same identity
for the unchanged sources.
A mapped type with no members was refused (SC2001) to keep `Partial<T>`
inside a generic body, where keyof T is still unknown, from interning a
wrong empty record. That also refused mapped types the checker resolved
from concrete inputs whose key filter keeps no key: `Record<never, T>`,
`Partial<{}>`, and a catalog type such as
`{ [K in KeyOf<Data, "image"> as Id<K>]: Model }` for data without image
models. When the mapped type comes through an alias whose arguments mention
no type parameter (or a module-level alias without arguments), it is the
empty object type and lowers like `{}`.
pi-ai declares its per-provider image and classifier model catalogs this
way; most providers have none, which refused 105 declaration groups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`value instanceof Boolean` (or Number, String, BigInt, Symbol) died at run
time in JavaScript with Node's TypeError for a function without a prototype
object, because those constructors are compiled as plain conversion
closures; in TypeScript the right-hand side was refused (SC1090). A
compiled program never creates a boxed primitive (`new Boolean(...)` has
no lowering) and a primitive is never an instance, so the test is false
for every value. It now folds to false after evaluating the operand.
typebox's value guards (IsBoolean, IsNumber, IsString) test this way, so
pi's tool-schema construction and validation died at its first guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A class field typed AbortController or AbortSignal (plain, optional,
nullable, or in an array) maps to checked-dynamic storage, and the
class-field rule refused it (SC1090) although the same handles already
live in locals, Map values, and closures. They now take the checked-dynamic
slot, like the fetch Headers, Request, and Response handles.
controller?.abort() on a nullable controller was refused as a method call
(also for locals): the static abort lowering ran only without an optional
chain. It now runs again once the chain has narrowed the receiver.
Found by pi's TUI Editor (autocompleteAbort: AbortController | undefined).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A native build reuses its stale frontend cache entry when every changed
source is token-equivalent to the text the entry was built from, but dev
builds never did: their debug information records source lines and
columns, which a token-equivalent edit can move.
Builds with debug information (dev builds without --strip) now reuse the
entry when every changed source is token-equivalent and every token keeps
its line and column (tokenPositionEquivalentSource): edits to a comment's
text within its line, comments and whitespace after the last token, and
trailing whitespace. Edits that move a token still rebuild.
The token scanner also reads a `/` after a non-null assertion as division
(`data[i]! / 7`). It used to read it as the start of a regular expression
and give up on the file, so comment edits in files with that pattern
always rebuilt. A `!` after a line break, or after an operator or a
keyword such as `return`, still starts a negated regular expression, and
a `!` after `)`, `}`, `++` or `--` gives up.
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>
The committed profile was collected before about 200 commits that changed
12,800 lines of the compiler. Functions whose control flow changed since are
optimized as without a profile, so its gain had mostly gone: a cold tsc-ts
speed build was 2.7% faster with it than with no profile, against 5% to 6%
with a fresh one.
Regenerated with scripts/refresh-cli-profile.mjs on this commit.
The native compiler forked four worker processes to emit the function
bodies of a large module. It now forks one per available core, up to
eight. SCRIPTC_EMIT_WORKERS still sets the count. The output is the same
text at any count.
On Linux the executable cache stored an executable only from a key's second
link on, so the first unchanged rebuild after a build linked again, and only
the build after that restored the executable.
- A key's first link on Linux is traced, in the same verbose link. Instead
of storing the executable, the cache records the output's file identity
together with the link's inputs once the output is in place.
- keep() answers an unchanged rebuild whose output still has that identity
and whose inputs are unchanged. The build then neither links nor copies:
the output stays as it is.
- A rebuild whose output changed or is missing links again; from that second
link on, the executable is stored as before and restored where needed.
- The cache records the output after every Linux build that places one, so
the next unchanged rebuild keeps it.
darwin keeps its existing behavior.
A reader that starts after the second chunk arrives reads both chunks in
one 'readable' event. On loaded test workers Node started that late while
the compiled program did not, so the two outputs differed. The first chunk
now waits 500 ms.
Several analyses the emitter runs per function or per module walked whole
bodies to collect facts from a few node kinds. They now read those kinds
from the module's node index (ir/node-index.ts), which records every node
once in walk order, and the output is unchanged.
- numberGlobalWriters, readsLoopMetrics, closure creation sites
(LazyCaptures) and the ambient receiver readers visit only the kinds
they inspect.
- findLoopArrayBorrows visits only loop statements, and findIntegerViews
returns at once for a function without a loop, where it wants no view.
- foldConstantGlobals records which candidates each function reads during
its write walk instead of walking every function again.