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.
- 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.
- 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.
Node answers a BigIntStats when the stat call is passed `bigint: true`: the
same snapshot, read through a bigint member table. Every number member of
Stats reads as a bigint — the integers, the `*Ms` millisecond members (the
truncated nanosecond quotient, as the binding computes them) and the
BigIntStats-only `*Ns` nanosecond members (sec * 1e9 + nsec, exactly) —
while only the four time members stay Dates. scriptc refused the option
outright.
The representation is static, chosen by the option literal: the lowerer
validates `bigint` against the call's result type (so
`{ bigint: true, throwIfNoEntry: false }` still narrows
`BigIntStats | undefined`), refuses a runtime-valued option and an option
that disagrees with the destination, and keeps every other option key
fenced. fstatSync joins statSync/lstatSync on the same path, with the
descriptor evaluated before the options.
The IR gains a `bigintStats` handle kind beside `stats`: the same RC
family, the same non-traced leaf and non-dynamic handle treatment, so no
shape, tracing or dyn-check behavior moves for any program that never asks
for one. Its member table is a `bigstats.*` lib-call family whose Date
members reuse the existing stat reads.
The runtime support is its own unit, scr_stats_bigint.c: the snapshot
layout moved into scr_stats_private.h so the bigint getters can read the
same fields without scr_lib.c carrying their code. The unit is gated on the
existing `bigint` feature in both the runtime-pack matrix and the
compiler's from-source unit table (the sanitized lane links from that one),
and `-ffunction-sections` keeps a bigint program that never reads a member
from linking any of the getters. No typed program's IR or LLVM changes.
tests/corpus/7018-bigint-stats.ts pins the member table, the Date/bigint
split, ms/ns agreement, arithmetic on a member, lstat over a symlink,
fstatSync over a descriptor, the throwIfNoEntry union, and the plain Stats
answers of the un-optioned and `bigint: false` forms against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The socket registry, the http client registry, an agent's entry list and
fetch's list of transfers in flight were singly linked: each append walked
to the tail and each removal walked to the element. A client request on a
pooled connection registers and unregisters a socket and a client and adds
and removes an agent entry, so every request walked lists as long as the
open connections. These lists now link both ways with a last pointer, in
the same order as before. A client finds its agent entry through a
back-pointer, and an agent counts its queued entries, so a finished
request scans for a queued one only when there is one.
scr_str_case_conv scans ASCII text for the first letter to convert eight
bytes at a time, then converts the rest eight bytes at a time; the tail
and big-endian targets take the byte loop. With a refreshed profile, the
byte-at-a-time scan compiled worse for short strings with nothing to
convert, which made validate-errors slower.
The runtime profile is collected again on this change with
scripts/refresh-runtime-profile.mjs (326 raw profiles). Changed
functions hold 0.0% of its counts, against 3.7% for the previous one.
tests/corpus/string-case-ascii-words.ts covers ASCII strings of every
length from 0 to 40, with letters at block boundaries and next to the
bytes just outside A-Z and a-z.
An uncork() flushed a head writeHead stored even when nothing was
written while corked. Node keeps the stored head until write(), end() or
flushHeaders(), so the flush now sends it only when a write() or
flushHeaders() ran while corked. The http-held-head fixture covers the
three cases.
#2468 shipped child.off()/removeListener() and child.signalCode in the gated
scr_child_lifecycle.c unit, but it also restructured the always-linked
scr_child.c terminal dispatch loops to detach their listener arrays before
emitting. scr_child.c is compiled once per program and linked into all of
them, so its object moved and every child-process executable grew: the
zero-cost gate reported 55 runtime-only corpus programs.
Restore scr_child.c byte for byte and move the emit-snapshot discipline into
the gated unit instead. The base loops already bracket each terminal emit
with a state flag that only the emitter writes -- `settled` for 'exit' and
'error' (set after the stream drain, immediately before the terminal branch)
and `close_done` for 'close' (set at the top of the close emit) -- and run no
user code in between. A removal inside that window is therefore a no-op: the
entry stays, the in-flight emit still delivers it, which is exactly Node's
answer (emit copies the listener array), and the emitter's own drop releases
it. Outside the window no emit of that event is in flight, so removal in
place is Node-exact, and the base already releases late registrations once
the window has closed. 'spawn' detaches its array before invoking its
listeners, so removal during it finds an empty registry.
scr_child_private.h repeats the structure layout for the gated unit only;
scr_child.c keeps its own definitions untouched.
Validation: zero-cost gate against 68980c241 reports no runtime-only program
and no scr_child symbol in any IR, LLVM or executable difference; scr_child.o
is byte-identical to the base in all six shipped flavors, and the
scr_child_off_*/scr_child_signal_code entries live in scr_child_lifecycle.o.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The BigInt reciprocal divided a two-limb value by one limb, which compiles
to a __udivti3 call. zig provides that builtin from its compiler-rt, a
single object that also defines memset, memcpy, memmove, memcmp, bcmp,
strlen and the _chk variants. Linking it for __udivti3 made those
definitions take precedence over libc's in every glibc executable built
with SCRIPTC_CC=zigcc, and zig's bcmp and memcmp compare one byte per
iteration and its memset stores one byte at a time.
The reciprocal now divides in 32-bit halves with one-limb operations, so
the runtime references no compiler-rt builtin and glibc executables import
the four functions from libc.
The sweep and the dispatch loop's pending check walked every registered
socket on each loop pass, so a server with many keep-alive connections
paid for all of them on every request. A socket now joins a work queue
when it may have deferred work (bytes buffered for a consumer, write
callbacks waiting for the drain, 'finish', an error or 'close') and
leaves it when a sweep visit leaves it none. The queue is ordered by
registration, so the sweep visits sockets in registry order as before.
With SCR_RC_AUDIT, the dispatch loop checks that no registered socket
outside the queue has deferred work.
res.writeHead() builds the head and keeps it until the first write(),
end() or flushHeaders(), which sends it with that call's data in one
socket write, as Node's _storeHeader does. A response with an Expect
header sends the head at once, as in Node. The http-held-head server
fixture compares the wire timing with Node's.
A client response cut off mid-body emitted 'error' ("aborted") whether or
not it had an 'error' listener, so a response without one ended the
process with "Unhandled 'error' event". Node's IncomingMessage reports its
destroy error only to 'error' listeners; 'aborted' and 'close' still fire.
fetch leaves a redirect response by destroying its request, so following
a redirect whose chunked body had not ended crashed the process before
fetch settled. It now follows the redirect.
Fixes#2395
Co-authored-by: linguofeng <460328+linguofeng@users.noreply.github.com>
readline's emit and EventEmitter's emit copy the listener list before
running it, so that listeners added or removed during the emit don't
change who runs. The copy was a malloc and a free per emitted event: per
line for a readline 'line' listener. A list of at most four listeners is
now copied into a stack buffer; longer lists still allocate.
A new corpus program pins the snapshot semantics for lists below and above
four listeners: a listener added during an emit runs from the next emit, a
removed one still runs in that emit, and a once listener runs once.
- child_process: the exit/close/error dispatch loops read the live
listener registry in their loop condition, so a listener that called
off/removeListener on a sibling cut the in-flight emit short. Node
copies the listener array before it emits; detach the registry
(scr_child_detach_exit/_close/_err), deliver that snapshot, release
each closure, and free the array in both platform arms.
- process: a first-class alias of stdout/stderr (`const out =
process.stdout`) keeps the stream's type but read `writableLength`
through the keyed dynamic get, which answered undefined. Fold that
read to the same synchronous-write 0 the direct lowering answers,
leaving the runtime untouched.
- fs: a `const` write-options record was inlined at the write site even
when it was mutable, so `OPTS.mode = 0o644` before the call diverged
from Node, which reads the record when the call runs. Accept only a
record whose properties are all readonly (`as const`, `Readonly<>`);
a mutable one keeps the SC2020 fence.
- Refresh the node-types-fenced snapshot: `path.win32` sub-namespaces
lower now, so the PlatformPath.sep fence is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve tests/harness/npm.test.ts by keeping main's npm-suite split and
porting this branch's island KNOWN entry and stderr frame normalisation
into tests/harness/npm-suite.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve tests/harness/npm.test.ts by keeping main's npm-suite split and
porting this branch's island KNOWN entry and stderr frame normalisation
into tests/harness/npm-suite.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve tests/harness/npm.test.ts by taking main's npm-suite split and
porting the island path normalisation and the two island KNOWN entries
into tests/harness/npm-suite.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>