An executable with at least 4 MiB of program code is linked with 2 MiB
segment alignment and published with one write, so the page cache holds
its text in 2 MiB folios and the runtime advises huge pages for it. A run
then maps every 2 MiB text window it touches whole. Most runs of the
compiler are short: `scriptc --version` peaked at 76 MB and an unchanged
rebuild at 78 MB, against 20 MB and 26 MB with 4 KiB alignment, and cold
and edited builds of tsc-ts took the same time either way.
compile() takes an internal `hugeText: false`, which links with 4 KiB
alignment whatever the code size and is part of the executable cache key.
scripts/build-native-cli.mts passes it. Executables the compiler builds
keep huge text.
scr_fs_write_once cached whether the calling thread blocks SIGPIPE and
SIGXFSZ in an SCR_TL variable. SCR_TL is empty in builds without thread
instances, so a pool thread's answer applied to the main thread too, and
a later writeSync to a closed pipe ended the process with SIGPIPE
instead of throwing EPIPE. The cache is _Thread_local now. The
fs-write-sync fixture covers a write stream followed by writeSync on a
closed stdout.
The native compiler sends most checker requests one at a time and blocks
in recv for each reply. The server answers most of them in a few
microseconds, and a reader the reply wakes takes tens of microseconds more
per round trip, over about 130,000 round trips on a large program.
scriptc_ts7_read now polls the socket with MSG_DONTWAIT for up to 200 us
before it blocks. Windows keeps its blocking pipe read.
- A worker writes the completion wake byte only when the pipe has none.
The loop reads it, with the queue lock held, when it takes the last
waiting completion. The pipe holds a byte exactly while completions
wait, so a burst of completions costs one write and one read instead of
one write per completion and a read loop per dispatch.
- The fs pool's threads keep SIGPIPE and SIGXFSZ blocked for their
lifetime. scr_fs_write_once checks once per thread whether both are
blocked; on such a thread a write makes no signal-mask calls, and a
write that fails with EPIPE or EFBIG consumes the signal it raised.
Other threads keep the per-write guard.
fs-write-sync.test.ts covers EPIPE and EFBIG from write streams, whose
writes run on the pool's threads, and matches Node's errors.
A union retype between two shape unions (or two class unions) shares the
pointer when both encode their unit arms the same way. Narrowing a field
read past `=== undefined` or `=== null` retypes a union with a unit arm
to one without it, so the encodings differ and each listed arm was
rebuilt: a retain, a tag switch, a second retain and a release, for the
same pointer. The unit encodings only matter when the value may hold a
unit arm. When the retype lists none of the source's unit tags, every
value is a record or class pointer whose arm the target derives from the
record or class itself, so the pointer is the target value.
The rebuild's release also made the pointer a nursery cycle candidate,
and in the native compiler those candidates kept the nursery bypass's
probes of closures and boxes succeeding. Without them, a probe window can
open while young cycles of the type are held by older garbage that the
bypass promoted earlier. The window then frees nothing, the type stays
bypassed, and the garbage waits for a mature pass, which on a cold tsc-ts
build raised the compiler's peak RSS by about 100 MB. A nursery probe
window that opens now makes the next release-triggered pass that would
run at the nursery run at the mature level, which reclaims that garbage
before the window is judged.
- An async object-literal method whose contextual slot returns a non-promise
(`() => void`) keeps its own promise return, and one whose slot returns a
union takes only the union's promise arm (`Promise<T> | void`): adopting
the whole return type gave the method a non-promise type, refused with
SC2001.
- A type predicate whose record target is itself an arm of the element
union keeps that arm's elements as they are and converts the other arms
with its fields, instead of refusing the filter with SC1090.
The jam of a short constant-count loop now leaves the loop as it is when:
- the inner loop's body assigns the accumulator other than by
`acc = acc | C`. An iteration that runs past its exit could clear bits
after the accumulator was full.
- the inner loop's body assigns the inner counter. The copies share the
counter, so one copy's write changed the iterations of the others.
- the inner loop's start or bound reads the outer counter or a local of the
outer iteration. The copies share the inner loop's start and bound, which
read neither the counter's value nor the copies' renamed locals.
The unit test rejects each shape, and the corpus program adds a function
for each that matches Node's output.
The emitter named each TBAA type node `field.<n>`, numbered in the order
the module's functions first use the categories, so an edit that adds a
field or memory category renumbered every later one. Every function that
loads or stores those fields then changed text, and its program partition
was compiled again.
Each type node is now named by its category (`field:<name>`,
`array:header`, `global:<id>`). The set of nodes and their structure are
unchanged, so the generated code is the same.
refresh-cli-layout.mjs kept the raw profile of every cold training build
until the end of the run, about 23 MB per build and 8.7 GB for the 399
builds. When the disk filled, the profile runtime wrote partial files, the
compiler still exited with 0, and reading a partial file threw a RangeError
past the end of its counters.
Each cold profile is now read and deleted when its build finishes, so the
work directory stays under 200 MB. The script throws when a training run
reports `LLVM Profile Error`, naming the build and the write error, and
when a profile is shorter than its header describes.
Adds the corpus program receiver-before-effectful-key.ts, with its corpus
cost and order baseline:
- a tuple read with a runtime index that rebinds the tuple or replaces the
field holding it;
- a keyed read through a finite union of field names whose key rebinds the
record or replaces the field holding it;
- an inherited-key `in` test on a computed null-prototype receiver, which
runs the receiver once.
Each case reads the receiver as evaluated before the key, as in Node.
`x ?? { errors: new Map() }` left the empty map typed Map<any, any>,
which spread through the checker into `new Map(result.errors)` and the
binding holding it, and every use failed with SC2020/SC1090. The
fallback literal now builds as x's record, `new Map(source)` over such
a join copies into the source's layout, an unannotated binding whose
checker type differs only by that residue takes its initializer's
layout, and Map methods on it convert into the receiver's layout. No
map is converted between layouts: each value keeps the one layout it
has.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An async method in an object literal with no return annotation and a
contextual signature inferred its own (narrower) literal type, while its
`return` literal was built at the contextually typed record: the return
statement then had to convert the wider record into the narrower literal,
which fails with SC2002. The method now takes its return type from the
contextual property's call signature, as the arrow form does. Sync
methods are unchanged; they already adopt their slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A written type predicate whose target is no arm of the element union
but the same fields as one, with a nested union field narrowed
(Model<"a"> from the Model<Api> arm), was refused. The kept elements of
that arm now convert field by field, the nested union by its tag; any
other kept element still throws the predicate TypeError.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An empty binding pattern still performs its one observable step:
RequireObjectCoercible for `{}` and GetIterator for `[]`. Over a statically
non-nullish source that step is invisible, which is why the empty-pattern
lowering emitted nothing; a checked-dynamic source can be null or undefined
at runtime, where V8 throws.
Emit the runtime null/undefined test for dyn sources in the empty-pattern
branch, and let a JS-file empty object pattern take the source-spelling
message dynDestrCheck already produces for non-empty ones.
Test262 (static-adapted-v6, --workers 8), affected chapters before -> after:
language/statements/class/dstr pass 1518 -> 1526, fail 424 -> 416;
language/expressions/class/dstr pass 1518 -> 1526, fail 424 -> 416. No
pass -> fail.
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
A class whose methods are reachable through `X.prototype` widens its method
signatures to checked values so a JS caller can hand anything across. The
widening replaced every non-void return with DYN, which erased a GENERATOR
method's generator type; the generator branch below then had no generator
channels to build its metadata from and refused the whole class (SC2001 in
`%init`, re-emitted as a runtime fence in a JS file). Every
`assert.sameValue(c.m, C.prototype.m)` variant of the campaign therefore
refused at runtime, 759 of them in the generator-method chapters alone.
A generator's return kind is already a checked-crossable ABI: dynFrom boxes a
generator by reference and dyn calls resume it through the dyn generator ops,
so only the arguments need the widening. Guard the return widening to
non-generator methods.
Test262, 1,110 affected variants, before/after on the candidate
(class/gen-method 52->76, class/async-gen-method 76->128,
class/scope-gen-meth 0->8); no pass->fail and no typed-path delta.
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
Reading the name of a function stored in a const followed const aliases to
the definition, but stopped at destructuring: `const { cwd } = process`
gave the binding's name instead of Node's "wrappedCwd". A member
destructured from the process global or a builtin module's namespace now
gives the module table's name for it.
An anonymous function wrapped in parentheses (`const f = (function () {})`)
answered an empty name. As in Node, it takes the name of the binding or
property it initializes through the parentheses.
A builtin read as a property (`util.debuglog.name`) answered the name its
shared adapter was first created under, so after `util.debug` it answered
"debug". The name now comes from the property and the module table, and
util.debug's value is named "debuglog", as in Node.
A borrowed element read of a reference array falls back to
scr_arr_borrow_ref when the element is outside the dense fast path. The
call carried no memory attributes, so LLVM assumed it could write any
memory: a loop holding such a read reloaded the array's header on every
iteration, after inlining too. The fallback only reads the array (a hole
or an out-of-range index traps), so it now carries the attributes of the
other array read fallbacks, and the header loads stay hoisted.
- A BigIntStats Date member is built from its truncated millisecond member,
as Node's getters do, where the Stats getters round the fractional one: a
timestamp at least 0.5 ms past a millisecond no longer reads one
millisecond later than Node.
- statSync, lstatSync and fstatSync evaluate a bigint option whose value can
have an effect (`bigint: (count++, true)`), in member order after the path
and before the stat; an exception it throws is the call's.
- An object spread of a node:path namespace in JavaScript copies the module's
members, read through the module value so the copy holds the same
functions: an ES module namespace with its keys sorted and `default`, and
posix, win32 and the default import in Node's member order.
- scr_lib.h declares the BigIntStats readers, so every runtime name the
backend can emit has a prototype in scr_runtime.h.
A scanner reading a mostly ASCII text front to back (slices and
positioned startsWith at increasing positions) walked from the hot
cursor on every read within 64 units of it, since the near-cursor walk
came before the ASCII-run growth. A forward read past the run now grows
the run first. When a non-ASCII character ends the run, a nearby read
walks from the run's end or the cursor and starts a new run where it
lands.
The helper's program target options left UseInitArray off, so a program
constructor went to .ctors. The C startup files and lld run only
.init_array entries, so such a constructor never ran. Runtime units
already set the option (runtime-unit.cpp); program code now does too.
No program emits a constructor today, so executables are unchanged.
native-constructors.test.ts emits a constructor through a single object,
split code generation and program partitions, checks that it lands in
.init_array on ELF, and runs it.
The tag test and the walk share one evaluation: a call in the loop head
appears in the IR exactly once, and the loop walks the value the test saw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- lowerForOfEmptyArrayArm binds the `Set | Map | string | never[]` source
to a hidden local before the tag test, so a call or getter in the loop
head runs once and the loop walks the value the test saw.
- markTarget marks a structural view as a collection view when its source
is already one, and reruns the fixpoint when a view marked earlier
becomes a collection view, so a Map or Set handed through two views keeps
answering through the collection.
- Promise.all in a JavaScript source whose call types as Promise<any[]>
(an unannotated parameter) runs the engine's Promise.all over the entries
as checked-dynamic values when they are typed promises. A merged
`Promise<number | string>[]` argument reaches that call since union
promise arms merge.
Corpus 8419 and 6956 and the js-own-tostring npm fixture cover each case.