When --split codegen keeps the module whole, the extra outputs are new
empty modules, and their source file name defaulted to the module
identifier: the path of the private copy of the LLVM input, which has a
random name. That name reached each part's STT_FILE symbol, so two
release builds of one program differed in .strtab. The empty parts now
take the module's source file name, as the first part has it.
The LLVM backend can emit scr_census_write, which a census program
defines and the runtime calls through a weak reference, so the header
declares it as it does scr_detached_exit.
Every native build started the LLVM helper once for `version` before
emitting, and hashed every file the frontend read, including the supplied
declaration files that do not change between builds.
- verifyNativeHelper takes an optional record of helpers that already
answered `version` as expected. The native compiler keeps it in its cache
(family "helper"), keyed by the helper's and the compiler's file identity,
the compiler version and the target. The probe runs again when the helper
file changes. The package name and version are still checked every time.
- FrontendInputTracker takes the snapshot of the cache entry a build
replaces. A file whose settled identity has not changed since that
snapshot keeps the digest recorded there, which is the trust
frontendInputsStillMatch already gives an unchanged identity.
Builds produce the same executables.
Libraries never build the census (heapCensusOf), but the runtime's weak
scr_census_write reference made library archives list an external
symbol outside their declared set. Declare and call it only in programs.
- The heap census also counts instances a class value constructs: the
construct thunk that `newValue` calls counts them after its allocation,
as `new` does at its own.
- A build with SCRIPTC_HEAP_CENSUS set is its own executable cache entry
(`heapCensus` in the early cache's key and route key), and one with
SCRIPTC_REPR_REPORT set does not read the early cache, since a hit
emits no module and so writes no report. The CLI's startup route defers
both to the full compiler.
- Test: a warm cache still builds the census executable, counting
`new` and class-value constructions, and still writes the report.
fork() copies only the calling thread, so a child forked while the
allocator's prefault helper held scr_sa_pf_lock waited on that lock forever
at its first fault-in request, and a child that released the range the helper
was faulting in waited forever for the helper to finish it. Native builds'
emit workers hung this way, with the parent waiting in waitpid.
The helper's first start now registers pthread_atfork handlers. The prepare
handler takes the lock across the fork and the parent handler releases it.
The child handler resets the ring, the busy range, the waiter state and the
heap polling state, reinitializes the lock and both conditions, and marks the
helper not started, so the child's first request starts a helper of its own.
check_fork_while_prefaulting in scr_alloc.test.c forks while the helper
faults in 64 MiB, over 64 rounds; each child releases that range, faults in a
range of its own and must exit within five seconds. ThreadSanitizer builds
skip it.
`[...text]` lowered to an array literal that spread the array of text's
code points: the literal's array copied the code point strings, retaining
each one, and the code point array then released them. An array literal
whose only element is a spread of a string is now the code points array
itself, as `Array.from(text)` already was. Both are fresh arrays.
charCodeAt on a non-ASCII string mapped the UTF-16 index to a byte offset
through a string index entry, which the first read claimed and the
string's release purged. For a string of one character, such as each
character of a for-of loop that codePointAt(0) classifies, the entry cost
more than the read.
A non-ASCII receiver of up to 16 bytes is now decoded from its start on
each read, as scr_str_utf16_len already counts short receivers. Longer
receivers keep the index entry and its cursor.
An index `base + c` converted `base` and took a fractional base as an
invalid index. A fraction smaller than the sum's precision is lost in the
sum, as in `1e-20 + 1`, which is the index 1. A base that is not an
integer now goes to the exact check of the sum itself.
The conversion also read the base local again when the access was
emitted. A write evaluates its right-hand side between the index and the
store, so `values[at + 1] = ((at = 5), 31)` wrote at the new base. The
sum now records the base value it was computed from, and the index
converts that value.
The corpus program covers both cases, and the emission test checks that
a write converts the base its sum read.
Optimized executables with at most 1 MiB of LLVM text were compiled as one
module on one thread. With more than 128 KiB of LLVM text they now pass
several outputs and `--split codegen` to the LLVM helper. The helper
optimizes the module whole, as for a single output, then divides its
functions in module order into runs of about the same instruction count and
generates code for each run on its own thread.
- The output count is 4 or 8 and depends only on the module's size
(nativeCodegenParts), so artifacts never vary with the host. Node and
native compiler builds pass the same outputs.
- The helper writes the optimized module to bitcode once with its use-list
order. Every output after the first parses it and turns the other
outputs' definitions into declarations; the first output does the same to
the optimized module in place. Each function's machine code matches a
single-output build except for jumps to a definition that another output
also uses.
- Only local definitions that another output uses become hidden. Dead
constant users are dropped before unused declarations are removed, so no
output names a thread-local variable it does not use.
- When one run would hold more than three quarters of the instructions, the
first output is generated from the optimized module as for a single
output and the others are empty objects.
- Dev builds, partitioned builds and program-object validation are
unchanged.
The constructor took the program object with an empty name for the main
program, which is how glibc reports it. musl reports the main program
with its path, or "/proc/self/exe" in a static executable, so on musl
targets --large-pages found no segment and left the code file-mapped.
The constructor now takes the executable segment that holds its own code:
the runtime links into the executable, so that is the main program's
code under either C library.
scr_text_pages.test.ts runs its check against a static musl build made
with zig as well as the glibc build.
A large program's hot code spans hundreds of 4 KiB pages, more than the
instruction TLB holds. tsc-ts has 12 MB of code, and 90% of its
main-thread samples at --checkers 1 fall in 274 distinct code pages.
`scriptc build --large-pages` links the new runtime unit
scr_text_pages.c, whose constructor moves the executable's code onto
2 MiB transparent huge pages before main. It copies the executable
segment, from its first to its last 2 MiB boundary, into anonymous memory
advised MADV_HUGEPAGE, makes the copy read-only and executable, and moves
it over the original range with one mremap, so addresses and instructions
stay the same. The move runs only on Linux, only when THP is "always" or
"madvise", and only in executables. If a step fails, the code stays
file-mapped. Like Node's --use-largepages, it is off by default: without
the flag the unit is not linked, and executables are unchanged.
The move costs about 3 ms per 10 MiB of code at startup, and all of the
moved code stays resident as private memory. The flag is part of the
executable cache key and the runtime-pack selection, and is refused for
non-executable output, like --detached-exit.
A typed array read or write at `base + c`, `c + base` or `base - c`,
where `base` is a number local and `c` an integer literal of magnitude
below 2^31, now converts `base` to an integer and adds `c` as an
integer, instead of converting the double sum. Accesses at several
offsets from one base, such as `x[at]` through `x[at + 7]` with a number
parameter `at`, then share one conversion and integer test. Below a
length of at most 2^52 the sum is an integer index exactly when `base`
is an integer, so the accesses read and write the same elements as
before.
A member read through a non-null asserted element read evaluates the
element's presence and then reads it, so a missing element throws Node's
TypeError. An index other than a local or a field read was held in a
hidden local first, which turned the receiver into a sequence: the
element was then read with a reference (retain and release) instead of
borrowed, and the fallback `genes[genes.length - 1]!.sc` of a search
made fasta's lookup loop 12% slower.
An index computed only from locals, field reads, number literals and the
lengths of arrays read through them (`xs.length - 1`, `i + 1`, `-i`)
has no effect and yields the same value when evaluated again with
nothing in between, so it is evaluated for the presence test and again
for the read, and the element stays a borrowed read. Other indexes keep
their hidden local.
A while loop whose remainder reads number locals that enter it without
range facts, such as copies of module globals, parameters or fields
(`const base = column;` before `(count + base - breaks) % 60`), now runs
as two copies. On entry, a test checks that each such local holds an
integer in [-2^48, 2^48] other than -0. The fast copy uses the facts the
integer range analysis proves when the function is analyzed again with
those entry ranges assumed: the loop's counters become bounded, and the
sums and remainders run as i64 arithmetic. When the test fails, the loop
runs as it did before.
- ir/loop-entry-ranges.ts picks innermost while loops with a remainder
that the assumed ranges prove and the function's own analysis does
not. The test covers only the locals the remainder depends on, directly,
through the loop's writes or through its condition.
- ir/integer-ranges.ts takes the assumed entry ranges per loop, and keeps
no fact about the assumed locals or the loop's writes after it.
- backend/llvm/loop-entry-versions.ts emits the test and both copies.
The test is marked likely. A function called only from the loop gets
`alwaysinline` on the fast copy's call, because the second copy would
otherwise cost it the inlining of a single call site.
reverse-complement (25M bases): 2.08 s -> 1.19 s; Node 3.97 s.
applyHandleProperties collects the libCalls to rename with a walk of
every function body and renames each one in place. A node the IR reaches
from several statements was collected once per statement, so its second
rename looked up the already renamed name and set fn to undefined. The
calls are now collected in a set, so each node is renamed once.
The lowering does not share such a node today, because devirtualization
rebuilds every function body before this step; the lowered IR of current
programs is unchanged.
Object.freeze of a fresh record or array literal now sets the native
frozen bit instead of compiling to the literal, so an untyped reference to
the value sees it frozen: Object.isFrozen answers true and strict-mode
writes throw Node's TypeErrors.
Untyped property writes in sloppy-mode code (IrModule.sloppyDynWrites,
decided per write by isStrictCode) call scr_dyn_key_set_sloppy and its
computed form, which drop exactly the TypeError a refused assignment raises
(a read-only or frozen property, a non-extensible object, a frozen array
element) while a setter's exception still propagates. Computed-write
dispatch helpers are shared only by writes of the same mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LLVM emits an unwind table entry for a function that may unwind or that
has the uwtable attribute. Functions it infers nounwind, such as the
number-key Map probe sc_map_entry_f64_hashed, got none, so profilers and
debuggers could not unwind past them. The function attribute groups now
carry uwtable on native targets; wasm32-wasi modules are unchanged.
An instance of a compiled class reaches untyped code as a capsule, which
a property site sent to its helper on every read: the helper normalizes
the capsule, tests each class's key in turn, and loads the field.
A read site now emits the helper's plain field branches inline. A
capsule whose key pointer is one of those classes' keys loads the field
from its instance and shares the inline hits' conversion. A branch is
emitted only when its field holds a plain dyn cell and every subclass
the helper's normalization can rebox the key to reads the same field
from the same slot. Getters, bag properties, unset fields and other
capsules keep the helper.