Reflect.has had no lowering. It is [[HasProperty]] — own and inherited
names (Array.prototype's, Object.prototype's, a class's compiled methods),
proxy `has` traps, symbol and property-key conversion — which is exactly
the `in` operator's checked-dynamic probe, so it lowers to dyn.hasKeyComputed
over the target's dyn view (and the target is fenced when it has no such
view). A primitive target is Node's TypeError, which the probe answers
false for instead: fenced by name, never silently divergent.
Object.getOwnPropertyNames/Symbols/Descriptors only accepted values that
already were checked-dynamic; a record, array or class instance now boxes
into its own dyn view (own fields and elements, in property order) like
the function and error targets already did.
Corpus 8426 pins both against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
URLSearchParams crossing an implicit ToString (`${sp}`, "" + sp, String(sp))
had no lowering: ensureString now answers sp.toString, the serialized pair
list. Record inits also accept shorthand members (`new URLSearchParams({ model })`)
like the explicit `{ model: model }` spelling.
`re.exec(s)?.index` keeps the optional chain: the read is the match row's
index/input member (the chain lowering binds the guarded receiver first),
so the questionDotToken guard no longer diverts it.
Array.from(s) over a string whose binding the checker narrowed through a
guard read as the narrowed union and fenced; the site type now decides, and
the read is coerced to that string (code points, fresh array).
Corpus 8423-8425 pin all three against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The six new differential programs' plain and sanitized measurements, in
tests/harness/corpus-costs/<program>.json so the shard balancer weighs
them without touching the shared table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`spawn(cmd, args, { detached: process.platform !== "win32" })`, the
portable daemon idiom, no longer refuses: a non-literal detached value
lowers into scr_spawn_opts's bool slot, the same POSIX_SPAWN_SETSID flag
the literal forms reach, so the child leads its own process group.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`class Sink extends EventEmitter<Map>` now compiles: the type argument of
an EventEmitter base is the listener MAP, a pure typing aid (Node erases
it too), so the base is the same emitter class whatever the map says. A
stream base's argument IS the element layout and still refuses, as does
every other generic base.
The bundled fallback `events` module declares the class with an
unconstrained map so interfaces typecheck against it, and its members stay
the untyped `...args: any[]` surface either way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
process.stdin/stdout/stderr.fd answers Node's compile-time descriptor
(0/1/2, on a TTY, a pipe, and a redirected file alike), and
process.stdin.isRaw answers the TTY's raw-mode flag through
scr_process_stdin_is_raw (1/0), or Node's undefined on a pipe-backed
stdin through the module's interned `boolean | undefined` union.
The declared type of isRaw is `boolean` (as @types/node has it), so the
read is added to isOptionalProcessStreamProperty: without that entry
maybeNarrow bridges the union to the declared arm and the runtime guard
traps on the undefined a pipe actually answers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
process.prependListener now registers at the FRONT of the signal, 'exit',
and 'uncaughtException' listener lists instead of refusing: the runtime
gained scr_signal_prepend and scr_process_prepend_exit (both memmove the
existing entries down one slot), and the uncaught-exception registry
shares one prepend implementation with the append path (scr_rej_push_at).
Families with an append-only registry ('message', 'disconnect',
'unhandledRejection', 'rejectionHandled', 'warning') and
prependOnceListener still refuse rather than reordering silently.
Server.off/removeListener unregister the most recently added matching
listener from each of the server's lists (error, close/listening,
connection/secureConnection, timeout, drop) through scr_net_server_off,
releasing the callback and its once-marker; the emit forms keep their
existing lowering.
A child handle that is a `ChildProcess | undefined` field or parameter now
takes `child?.kill(...)` / `child?.on(...)`: both were gated on a
questionDotToken that the optional-chain lowering has already consumed, so
the chain-blocked check (lowerer.chainBlocked) replaces it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pi's compatibility shims patch host objects — `process.stdout.write = fn`
to capture output, `.bind()` on a native function, `Object.defineProperty`
on a module export. A compiled binary has no host object to patch: the
natives are direct calls, so these sites keep an explicit refusal instead
of compiling to something that silently diverges from Node.
tests/diagnostics/native-shim-monkeypatching.ts pins the three shapes
(SC1090 assignment to a non-variable, SC2007 on a valued overload set,
SC2020 for defineProperty on an import), so the boundary is visible and
any future process-object model has to update it deliberately.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Module resolution is a build-time artifact, but only a string literal
counted as a static specifier: `const FS = "node:" + "fs"; import(FS)`
and `import("node:" + "path")` fell into the computed-specifier refusals.
pi's `packages/ai` env loader writes exactly that shape.
`staticString` now folds a literal, a cast or parenthesized form of one,
a `+` concatenation of those, and a `const` binding whose initializer
folds to one (depth-bounded); anything only known at run time still
returns null, so genuinely computed specifiers keep their refusal.
`import()`, `import.meta.resolve` and `createRequire`'s `resolve` share
it. A folded specifier carries no module symbol on its argument node, so
the own-module test and the import target fall back to the checker's
program import resolution.
Corpus 8502 pins both folded forms and the builtins they name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`this.cache?.warm()` refused whenever the receiver lowered to a dynamic
value: the checked-dynamic receiver path treated any `?.` step as
unhandled chain work and bailed, even inside a chain body whose guard had
already proven the receiver present. Those steps are now gated on
`chainBlocked`, the canonical "is this `?.` still unhandled" test, so the
guarded body is the plain read-and-call it already is, with `dynCall`
throwing Node's catchable "x.y is not a function" for a present but
non-callable member. The string-only and primitive-method branches drop
their raw `questionDotToken` checks the same way.
A capture the checker itself types with an absent arm (`T | undefined`,
an optional property or parameter) also refused as a "complex" use in a
logical or conditional receiver, even though the union value flows into
the surrounding operator, which handles the absent arm itself. The use
now lowers to a plain read when both the checked type and the recorded
storage carry an undefined arm; a hidden absence keeps its refusal.
Corpus 8500 and 8501 pin optional method-call receivers and
`(entries ?? []).map(...)`-shaped capture receivers against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
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.
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>
canDynCheckTo answered false at every recursion back-edge in a record or
array, so a type whose checked graph re-enters itself had no checked
extraction at all. That is ordinary in a callback-options API: a context's
method takes an options object whose callback receives the same context, so
passing an unknown value into the record raised SC1100 even though every
leaf is checkable — pi's LoadExtensionsResult graph (extension commands
whose handlers take the extension context) is the census shape.
Answer the back-edge coinductively, as the composite dynFrom walk already
does for recursive shapes: mark the in-progress key (the wrapper's own
success key) for the duration of the frame and verify every reachable field
at its first visit. A field that fails anywhere still fails the frame, and
the existing failure path keeps dropping dependent conversion results, so a
cycle cannot hide an unsupported sibling. The `unsafe` unit fixture now
carries a genuinely unsupported sibling; its former Map sibling is inside
the check domain, so the back-edge refusal was what made it look unsafe.
Only the static conversion domain widens: a program that compiled before
compiles to the same code, and the runtime extraction still walks the
actual value, throwing a catchable TypeError on a mismatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
canBoxDynCompositeAt, the composite extension of the dynFrom domain, had no
promise case: it fell through to the runtime-handle test and answered false,
so a typed record with a Promise-typed field could not convert into an
unknown slot (SC1101). That is the shape of a model store's read state
(`reload?: { readers: number; promise: Promise<StoredModels> }`), and the
same fence blocked a promise in an array element or a union arm.
The backend already knew how to box one: toDynHelper emits
scr_dyn_new_promise for a dyn payload and scr_dyn_new_promise_adapting for
any other inner type, exactly as it does for a bare promise. Mirror that
conversion domain in the predicate — dyn and void inners, or an inner that
converts to dyn — so the promise boxes by REFERENCE wherever the walker
meets it and identity survives the round trip, as in Node.
The corpus program pins the round trip both ways: the payload, the promise
identity, and the absent-field case, under Node and natively.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`for (const name of registry.get(key) ?? [])` (mcp's server-tool registry)
types the head `Set<string> | never[]` and had no lowering: the iterable
union rule knows Iterable/generator unions and the indexed numeric arms, so
the loop fell through to the generic SC2001 fence on the union type.
`never[]` is the empty array literal's own type: its only value is the empty
array, which yields no iterations. `T[] | never[]` reduces in the checker
(never is assignable to T), so the reachable shape is exactly one inhabited
non-array iterable beside `never[]`. That loop is the existing container walk
on the arm the runtime tag selects:
if (unionIsTag(head, setArm)) { <lowerForOfSet on unionNarrow(head)> }
for Set, Map and string — no new representation, and the empty-array arm is
an empty `else`. A union of two inhabited iterables
(`Set<string> | string[]`) still fences: both arms hold values and their
walks yield different element kinds through different protocols.
Corpus 8416-iterable-never-array-arm pins Set/Map/string registries with
present and missing keys, a live set mutated between loops, and an explicit
`Set<string> | never[]` annotation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`entries.map((entry) => async () => …)` (package-manager's concurrency task
lists, `(() => Promise<…>)[]`) was refused: `.map` rejected a `func` return
with the same badType as a `void` return.
A closure is a supported array element. The closure array literal `[() => 1]`
already lowers to a `func`-element array, where the value is one ScrClosure
for its whole life, stored by reference with the element kind's `_v` adapter
and `scr_closure_trace_v`. The map helper builds the same array, so the
`func` half of the fence goes at both `.map` sites (plain receiver and
union-of-arrays receiver).
The `void` fence stays: `void[]` has no element, because a void return value
is erased before it could be written.
Corpus 8415-map-closure-elements pins the `.map`-produced arrays of closures:
`Array<() => Promise<string>>` and `Array<() => string>`, closure identity
through `indexOf`/`includes`, an inline `.map`, and
`Promise.all(tasks.map((task) => task()))`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`void | Promise<A> | Promise<void> | Decision` (the agent-loop `finishTurn`
hook, codemode's remote client) had no representation: a union arm's index is
its runtime tag and a promise arm's tag IS the `instanceof Promise` test, so a
second promise arm had no tag. The union was refused (SC2009) and every use
site inherited the blocker.
JavaScript cannot tell the arms apart: `instanceof Promise` answers true for
both and `await` settles to either payload. The union therefore maps to one
promise arm `Promise<A | undefined>`: payloads join like union arms (`void`
becomes the undefined unit, an already-union payload flattens, identical
payloads dedupe, a checked-dynamic payload absorbs the arms the dynamic
representation holds faithfully). A payload with no union arm home (a RegExp
payload, a promise-of-a-promise) keeps the whole union fenced, and the
refusal now says why.
A `Promise<A>` value flowing into the merged arm converts with the existing
checked promise-payload widening (promisePayloadWidens / promiseWiden): the
promise keeps its identity and its settlement turn, and the settled payload
is checked against the payload slot.
Cost: the merge only runs for a union that already has two promise arms,
which is refused today. Every other union interns the same arms in the same
order, so a fully typed program without such a union compiles to identical
IR.
Corpus 8414-promise-union-arms pins identity through the union slot,
`instanceof Promise` narrowing, `await`, `.then`, the void arm, the rejection
path and a non-promise data arm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>