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>
Adds the technical-writer skill from vercel/chat to .agents/skills, with a
.claude symlink to .agents so Claude Code finds it. AGENTS.md now asks agents
to use the skill for prose in this repo.
CONTRIBUTING.md gets a matching "Writing with AI coding agents" section.
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
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.
The call-lifetime and string-and-array lifetime tests kept their optional element union a box with a `Set<Item | undefined>`, which no longer boxes a union of identity arms. They now emit with keepSetElementsBoxed, which keeps the unions the module's Sets hold boxed.
An unchecked element read of a tuple array (`rows[i].length` with
`rows: [number, string][]`) answers the element's optional value, and the
computed `.length` path discarded it, so a read past the end printed the
tuple's arity. It now holds the receiver and throws Node's TypeError,
"Cannot read properties of undefined (reading 'length')", when the element
is missing, before answering the arity.
The corpus program tuple-length-computed.ts covers a missing element read
directly and through a field holding the array.
#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 union that is a Set element or a Map key was a tagged box in the whole
program, because the C map hashes and compares union keys by tag and
payload. One `Set<Node | undefined>` made every `Node | undefined` value
of the program a box, at every read.
A union whose arms all compare by identity (records, class instances,
arrays, functions), beside at most one undefined and one null arm, now
keeps its pointer representation in collections too. The map keys such a
union by reference: the pointer carries the identity, and NULL and the
null sentinel are two more distinct keys. Unions with a string or another
value arm keep their boxes, and library builds keep the earlier rule.
The tests of the box paths keep their unions boxed through a new
`boxedUnions` emission option instead of a Set of each union.
A function with more than INLINE_DYN_RELEASES_PER_FUNCTION inline dyn
releases called scr_dyn_release at every site. A script's top-level loop
runs in its module initializer, which often has more sites than that, so
its hottest releases became calls. The emitter now marks inline dyn
releases emitted inside loops, and a body over the budget keeps up to the
budget of those inline, calling the runtime at the rest.
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.
`.length` of a tuple is its arity, and the lowering folded it to that
constant only when the receiver was rooted at a binding or `this`. Any
other receiver (a call result, a ternary, an `as` assertion, an `as
const` literal) was refused with SC1090.
The receiver is now evaluated once, for its side effects and exceptions,
and its value discarded; the expression is the arity. A binding, `this`,
or a chain of plain property reads from one still folds. A chain through
an accessor is evaluated, so an accessor that returns a tuple runs, as in
Node.
With --exceptions=rare, recursive functions and functions up to three
calls above a throw also propagate exceptions by unwinding on Linux, so
calls to them need no pending-flag test. Functions with a throw of their
own keep the flag. The default, common, keeps the current rules, and
builds without the flag emit the same code as before.
The option is part of the executable cache key; the bootstrap's cached
route leaves builds with it to the full compiler, and coverage and
library builds refuse it.
The keyed and tuple-index dispatches read the receiver and key inside a
discard statement, which re-evaluated an effectful receiver expression
(an `in` test over a call result) and ignored a key call that rebinds the
receiver binding. Both now settle the receiver into a hidden local before
the key evaluates, and the inherited-key probe returns its answer
directly instead of wrapping it in a discard statement.
Narrow the union field write's no-static-supertype path to unions that
have a dynamic arm, so a union of unrelated static storage types keeps
its refusal, matching the read path; a runtime-keyed spread into a record
is covered by the checked extraction test instead of the refusal table.
Review threads:
- 4403396631 inherited-key probe evaluated an effectful receiver twice
- 4403396650 key/index calls that reassign the receiver binding
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The emitter writes "; scriptc-entry" before the entry module's top-level
function (its %init). In modules of fewer than 32 partitions, the helper
places the roots that hold a marked function first, and their partitions
take no other root. A root that finds no partition with room goes to the
first open partition from its own.
An edit of the program's top level then recompiles only the top-level code
and the chain that follows it, not the other roots that balancing placed
beside it.
A procStream-typed receiver that is a call or an accessor read now runs
for its effects, and propagates its throw, before the fold answers 0: the
constant sits in a seqExpr behind an exprStmt receiver, the same
evaluate-for-effect shape objectIsDisjointFalse uses. A pure alias still
folds to the bare constant. The corpus program covers a call receiver, an
accessor receiver and a throwing receiver against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A program with no async functions, generators or timers never ran the
event loop, so a promise rejection nothing handled went unreported:
`Promise.reject(new Error('x'))` alone exited 0, where Node prints the
error and exits 1. The loop now also runs when the program makes a
promise (a Promise static, `new Promise` or Promise.withResolvers), and
its end reports the rejection, runs the 'exit' listeners with code 1 and
exits 1, as for async programs. Programs that make no promise keep their
code unchanged.
Array.prototype.join reads the length before it converts the separator.
The lowering converted the separator first and let the join read the
length afterwards, so a separator whose toString changed the array joined
the wrong number of elements. When ToString of the separator value can run
code of the program (a checked-dynamic value, an object or class instance
with toString), the join now reads the length first: an array that grew
joins its first elements, and the missing elements of one that shrank join
as empty strings. Primitive separators keep the direct join.
mock-timers' "named and namespace imports keep the loop's timers" and
6811-node-island-uncaught-listener check the order in which real timers
fire, with delays 1 to 5 ms apart that are scheduled one after another.
On a busy runner more time than that can pass between scheduling two of
them, and the order flips in either lane: the sanitized node:test run saw
Node's reference print 'named promise' first, and the sanitized
llvm-differential run saw 'island timer 1' before 'native timer after'.
The delays are now 100 ms apart in mock-timers (100, 200, 300) and the
island's timers (150, 250) start 100 ms after the native ones (10, 50).
Node prints the same output as before.
static-spatial bundles three.js and builds it with the sanitizer, which
passed the default 300 s test timeout on a busy runner. Its cases take
three.test.ts's 1200 s timeout.