exactInteger can answer a limit's ToUint32 view, which for a number
literal is a literal of its unsigned bits: -1 has the view 4294967295. The
counted loop took such a literal as its i64 endpoint as written, so a
negative limit became a large positive endpoint and the loop ran when it
should not. An i32 literal endpoint is now read as signed or unsigned, as
the integer says.
A counted for loop converts its limit to an i64 endpoint. A limit that is
not a literal went through the double every time: converted to double,
clamped to 2^53, rounded with llvm.floor or llvm.ceil and converted back.
At the default target CPU the rounding is a libm call, which a loop such as
`for (let i = values.length - 1; i > at; i--)` paid on every entry even
when `at` was an int32 local. A limit that is an exact integer (an int32
local, an integer view, or a proven range) is its own floor and ceiling and
within ±2^53, so it is now the endpoint directly; other limits keep the
double path.
A function value with more parameters than the slot it flows into is
assignable in TypeScript exactly when the extra trailing parameters are
optional, and a call through the slot passes no value for them, so the
callee sees undefined as it does in JavaScript. funcCoerceAdapter refused
every such pair, so a callback parameter forwarded into a narrower slot
fell through to requireExactShape and failed with SC2003 ("union
conversion has no unambiguous layout mapping") even though the checker
accepted the program.
The adapter now supplies each omitted extra parameter with omittedArgFor
(the undefined arm of the parameter's ABI union, or the checked-dynamic
undefined). A pair whose extra parameter cannot hold undefined stays
refused, and an extra required parameter remains a checker error.
Only pairs that previously returned null reach the new code, so programs
that compiled before are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A record now has a reads section: the entries of its function tables that the unit's IR and new entries name and that the tables held before the unit, each with a digest of its content (contentDigest). An applier checks that it holds each one with the same content. This catches helpers named in creation order, such as %dyn.class.protocol.<n>, which another process can number differently.
Two lowering effects had no table a record could check. A unit that collects a function-local class registers the class's members as reachable units, and Object.freeze of a class instance adds hidden fields to the class layout. The lowerer now counts both (unitRegistrations, classLayoutChanges), and a unit that changes either count has no record.
Response.clone and Request.clone tee the body. The tee branches of a body
read from a JavaScript ReadableStream did not keep that body's chunk check,
so consuming the original or the clone took string chunks as text. The
check is now a flag on the body's stream that its tee branches inherit, and
consuming either branch rejects with Node's TypeError "Received
non-Uint8Array chunk".
analyzeFrameEscapes walked every function's body on each visit of its parameter and capture fixpoint, and once more for each function's locals: 1.8 s on tsc-ts's IR, plus 0.5 s for the locals. Each function's body is now walked once into a summary: its value groups, the groups that escape whatever the facts say, and per group the calls and closures whose facts decide it. A query explores only the facts its answer depends on (a parameter's fact depends on the uses of its own group) and solves that part of the graph to its greatest fixpoint; solved facts are kept for later queries. The answers are unchanged: on tsc-ts every parameter fact and every function's frame locals are identical, and the emitter's queries take about 0.35 s.
When Error.captureStackTrace evaluates a JavaScript constructorOpt (a getter,
a call, or this.constructor with a constructor getter), V8 has already
evaluated the target. The lowering kept the target in a local only when its
expression was not a plain read, so a constructorOpt that assigned the target
variable made the capture install the stack on the new value. The target is
now always kept in a local before the constructorOpt runs.
Under Node.js, publish() deep-freezes everything it reaches, so
Object.isFrozen and Object.isSealed return true and Object.isExtensible
returns false for a published object, array, Map or Set. Native builds
already refused writes into published values, but answered these queries
as for an unpublished value: publication marks an object immortal and sets
no frozen bit.
- record.isFrozen, array.isFrozen and the extensibility test of Maps and
Sets also read an immortal rc word as published, the test of the
published write guards, for the types the program may publish
(PublishedTypes). Other types emit the code they did.
- scr_dyn_is_frozen, scr_dyn_is_sealed and scr_dyn_is_extensible answer
for a published object, array, Map or Set, or a checked view of one,
before materializing the view.
Dev executables compiled their program shards in fixed batches of up
to eight, each batch waiting for its slowest shard before the next one
started. Keep up to eight clangs busy instead, each taking the next
shard when it finishes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A view writes its source's slots through the target type's stores, which
have no frozen-write guard, so a write through a view made before
Object.freeze(source) changed the frozen record. A view's record-source
entry also owns the source's capsule, an edge the cycle analysis does not
see: a source that stored its own view, or a value reaching it, kept both
records until process exit.
A source now takes part in a view only when it is not freezable and its
fields, elements, map entries, union arms and overflow properties cannot
hold a value of the target type. A class instance, function, untyped value
or other reference that can reach arbitrary objects counts as holding
every type, and a value of a view target type counts as holding the
records it may view, so two views cannot close a cycle either. Other
conversions keep their copy.
A Linux build that links with lld records the driver's ld.lld job with the
identities of the files and directories it names, and later links run that
job directly while those identities match. When one of them changed, for
example when a system update replaced libc or the linker, the link went back
to the driver, but the entry stayed: record() kept the existing entry, so
every later link of that shape went through the driver until the entry was
pruned.
An entry whose job names a changed path now counts as missing. A listed link
records the job again with the driver's -v -### and runs it; a traced link
through the driver records it from its -v output. Entries without a job,
for links that cannot run as one ld.lld job, are kept as before.
A nullable class field (Tag | null) stores the class pointer itself, whatever form the union takes elsewhere. When the same union was also a Set element or a Map key, it was a boxed union outside class fields, and the frame's cleanup of a frame class released the field's raw pointer with scr_union_release. classReferenceFields now gives each field its storage type (NullableRefFields.storageType), as the heap teardown does, so the scope exit calls the class's release.
A shard declares every function another shard defines. The declaration
copied the definition's personality routine, which LLVM rejects on a
declaration, so on Linux (where functions with landing pads carry one)
every shard compile failed and large dev builds silently fell back to one
single-threaded translation unit. The sanitized bootstrap seed spent about
5.5 minutes in that one clang process.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A class instance could live in its function's frame only when its class
had scalar fields only and CallLifetimes proved the binding was only read.
Per-call state objects whose fields own arrays, maps and other objects,
and whose methods and helpers store into them, stayed on the heap.
frame-escapes.ts proves which locals never leave their frame: every use
keeps the value in the frame (region-ir.ts keepsInFrame), a const binding
of it in the same function shares its fate, and a closure that captures it
stays in the frame and keeps the captured binding there. Parameter and
capture facts are solved optimistically over the call graph with a
worklist; async functions, generators and module code get none.
A frame class is a standalone class whose constructor does not let this
escape. Its instance now lives in frame storage when it is bound to such a
local, when it is passed in place to a parameter that stays in the
callee's frame, or when closures that stay in the frame capture its
binding. The frame owns the reference fields: they start empty, union and
dyn fields start at undefined, and the binding's scope (or the call's
statement frame) releases them on every exit. A cycle-capable class keeps
its collector header in front of the object, never a candidate.
A unit's record carries the unions it interned, as their definitions. Applying one interns the union under the key its arms give and checks that its name matches. Recursive unions still leave the unit without a record, and the arm-tag index is a memo.
`scripts/refresh-cli-profile.mjs --context-sensitive` runs the training twice. The second run uses a compiler that the first profile optimizes and that is instrumented again after inlining, and it merges those counts into the first profile. Builds with the result also order each function's blocks by the counts and split cold blocks off (context-sensitive profiles). `--base-profile` runs only the second phase. `scripts/build-native-cli.mts` builds such a compiler when `SCRIPTC_NATIVE_PROFILE_GENERATE` and `SCRIPTC_NATIVE_PROFILE_USE` are both set. The release workflow's cli-profile job collects the profile this way, with a 120-minute timeout.
setTimeout and setInterval passed their delay to the F64 timer slot
unconverted, so a delay typed number | undefined failed with SC2003. Such
a delay is an optional `ms?: number` passed on, or a number[] element
read, whose storage keeps the out-of-range undefined. Node converts the
delay with ToNumber, so undefined is NaN, the omitted delay. A union
delay now goes through lowerOptionalNumber, as Math arguments already
do. Other unions still reach the lib boundary's refusal, and number
delays are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tools field (Tool[]) and the all global ((number | Callbacks)[]) hold unions that canonical mapping maps to the checked dynamic, so the canonical fallback now degrades them: they are dynamic slots again, with SC6104 warnings and attributed operations, as these tests expected before the fallback classified them as exact.
Worker runtimes reserve one region for 32 arenas of 40 class slices. When
the host refused the full region (32 arenas of 1 GiB slices, 1.28 TiB of
address space), the allocator kept 32 arenas and shrank every slice. Under
valgrind, which maps at most 56 GiB, slices dropped to 16 MiB, and every
class whose live blocks outgrew 16 MiB took the system allocator.
Callgrind profiles then counted system allocations that a native run
never makes: tsc-ts checking VS Code made 10.3 M small calloc calls under
valgrind and 0.06 M natively.
The reservation now tries arena counts from 32 down to 4 at each slice
size, from 1 GiB down, before smaller slices, and 2 and 1 arena only when
no slice size fits 4. Arenas that are not reserved stay claimed, so
threads beyond them use the system allocator, and the region bounds of
foreign frees use the arenas reserved. Hosts that map the full region keep
32 arenas of 1 GiB slices.
A runtime test limits RLIMIT_AS to 60 GiB and checks that the first thread
gets 256 MiB slices for 400,000 blocks of 56 bytes, that three more
threads get arenas and a fourth uses the system allocator, and that blocks
freed across threads keep their contents.
Localized failure's canonical fallback classified a failing type as an exact mapping whenever its canonical form mapped to something other than the checked dynamic. A record or array holding a union that mapCanonical collapses to the checked dynamic (an arm such as {} or unknown) then counted as exact, so its slot got no SC6104 warning and its dynamic operations no attribution.
mapCanonical now notes such a collapse (CanonMapNotes), and canonicalType refuses the form when it happened, so the type degrades through the pushed-down form like any other.
A JavaScript ReadableStream given to Response, Request or fetch as a body
was converted to the string "[object ReadableStream]". The fetch unit now
reads such a stream through a reader of its own. A locked or disturbed
stream gives Node's TypeError, and the chunks must be Uint8Array
when the body is consumed ("Received non-Uint8Array chunk").
instanceof against a web stream class held in a variable or passed as an
argument walked no prototype for handles and answered false. It now starts
at the class prototype of the handle. The fetch unit's own streams, readers
and controllers, such as Response.body, are instances of the JavaScript
classes, so `response.body instanceof ReadableStream` is true again.
These paths, and the fetch reader functions' hand-off to web stream
readers, are in SCR_HANDLE_CLASSES variants of scr_json.c and scr_fetch.c,
which only programs with JavaScript web streams link; other programs that
use fetch build to the same size as on main.
With main's canonical-mapping gap fixes (#2340), stage N2 maps the
Tool[] field and the (Callbacks | number)[] global exactly, so they are no
longer dynamic slots. The hot-slot fixture's Hook gets an intersection arm,
which has no canonical form, so it stays a dynamic piece of Plugin[].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>