Commit Graph
6275 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 bd06b809f1 Answer drain probes and drain listeners on the process streams (W1)
Under the synchronous output model nothing is ever queued: Node emits
'drain' only after a write() returned false, and these writes never
return false. The process streams therefore describe a stream whose
writes complete synchronously, and the members a flush idiom probes are
constants:

- writableHighWaterMark is 65536 (Node's default for a pipe, a file and
  a TTY);
- writableNeedDrain is false;
- writableLength stays 0.

`on`/`once`/`addListener`/`off`/`removeListener`/`prependListener`/
`prependOnceListener` of "drain" evaluate their receiver and listener and
register nothing: the event cannot fire, and the listener table is not
observable (listenerCount/listeners/eventNames on the process streams
stay refused).

`events.once(process.stdout | process.stderr, "drain")` lowers to
`events.pendingForever`, a fresh promise that never settles, which is
Node's behavior for a stream no write asked to drain; it holds no loop
handle, so a program awaiting it still exits.

The fallback declarations gain writableHighWaterMark, writableNeedDrain
and the event methods, so typed programs can ask.

Corpus: `tests/corpus/process-stream-drain.ts`; the diagnostics fixture
`process-stream-listener-count.ts` pins that reading the listener table
stays refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:37:57 -07:00
Malte UblandClaude Opus 5.5 cf369f70af Fix write callbacks that take the error argument (W3)
`writable.write(chunk, cb)` with `cb` passed as a function VALUE lost the
completion's error: `lowerWriteCb` checked only the callback's syntax and
lowered every other callback through `lowerExprExpecting(cbArg,
funcOf([], VOID))`, which adapts a one-parameter function down to zero
parameters. The native binary therefore printed `undefined` where Node
prints `null`, or the write's own error (`ERR_STREAM_WRITE_AFTER_END` /
`ERR_STREAM_DESTROYED` for late and destroyed writes).

Lower such callbacks through a new `writable.writeEcb` libCall
`(recv, mode, chunk, encoding, cb)`: a zero-parameter callback keeps the
existing synchronous path, and a callback that declares the error (a dyn
value, or `null | Error`) is wrapped by `scr_stream_ecb_wrap` with the
compiler-built adapter that delivers the error the write completion
carries. The stream unit publishes that error only in its
`SCR_STREAM_WRITE_ERRORS` build, selected by the new `streamWriteErrors`
runtime feature (rule: any `writable.writeEcb`). Every other build
compiles `SCR_WCB_CALL` to the same plain call, so their objects are
unchanged.

The destruction path also matched Node: `afterWrite` calls `cb(null)`
even for a stream destroyed after the write was queued (Node 24
`internal/streams/writable`), so the destroyed `SCR_ST_AFTER_WRITE` tick
passes NULL rather than the stream's stored error.

Corpus: `tests/corpus/writable-error-callbacks.ts` covers success, an
erroring write, a write after end, and a write on a destroyed stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:37:57 -07:00
Malte UblandClaude Opus 5.5 2b03f66ff3 Host patching H1: typed process output write slot
Reading `process.stdout.write` as a value, `write.bind(stream)`, and
`process.stdout.write = fn` now reach the runtime stream object's write
slot in TypeScript, and a program that replaces an output write method
calls its typed writes through that slot (SC8020 refuses the combination
with Node islands until H2 shares the stream object with them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:37:56 -07:00
Chris Tate 1555a6161f pi conversions and layouts: class/collection/error views, merged promise arms, recursive checked records (#2486) 2026-10-28 21:52:11 -05:00
Chris Tate 45f4f0d47e Refresh the native compiler's function layout and optimization profile (#2498) 2026-10-28 21:12:43 -05:00
Chris Tate cbd6b6ee2d Register and unregister sockets, clients and agent entries in constant time (#2496) 2026-10-28 20:56:27 -05:00
Malte Ubl db617bd594 Merge remote-tracking branch 'origin/main' into fix/pi-conversions-layouts 2026-10-28 18:30:14 -07:00
Malte UblandClaude Opus 5.5 ea4f54691a Test that a for..of beside never[] evaluates its source once
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>
2026-10-28 18:30:09 -07:00
Chris Tate e0c692593c Evaluate a for..of source beside never[] once, keep collection views through a second view, and combine typed promises in untyped JavaScript
- 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.
2026-10-28 20:22:14 -05:00
Chris Tate c64a9c4cdb Refresh the native compiler's function layout and optimization profile 2026-10-28 20:18:35 -05:00
Chris Tate d1abf69149 Refresh the runtime profile and convert ASCII case eight bytes at a time (#2492) 2026-10-28 20:15:03 -05:00
Chris Tate 84be8220a0 Register and unregister sockets, clients and agent entries in constant time
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.
2026-10-28 19:55:35 -05:00
Ben SabicandBen Sabic 1e48ebe187 chore(docs): add Technical Writer agent skill (#2447)
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>
2026-10-29 11:35:20 +11:00
Chris Tate 84644dfe9d Send the writeHead head with the response's first write (#2488) 2026-10-28 19:18:26 -05:00
Chris Tate 7f0c987140 Key Sets and Maps of identity-arm unions by their pointer representation (#2487) 2026-10-28 19:08:20 -05:00
Chris Tate 61c24c08e9 Refresh the runtime profile and convert ASCII case eight bytes at a time
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.
2026-10-28 18:43:37 -05:00
Chris Tate 18ab17848e Keep the held head across a cork() and uncork() with no write
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.
2026-10-28 18:23:49 -05:00
Chris Tate 33799052e5 Keep the source stack-box tests' Set element unions boxed through the emission option
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.
2026-10-28 18:22:49 -05:00
Chris Tate 94e921728c Island handle operations, native subclasses of island classes, arrays and collections by reference (NI-5b/5c/5d) (#1903) 2026-10-28 18:09:56 -05:00
Chris Tate f233fb5c1b Keep child-process executables byte-identical when the new listener APIs are unused (#2491) 2026-10-28 18:01:26 -05:00
Chris Tate 7409c85216 Compile .length of computed tuple expressions (#2482) 2026-10-28 17:51:45 -05:00
Chris Tate 9f3fb0fb17 Keep glibc's memset, memcpy, memcmp and bcmp in zig-linked executables (#2490) 2026-10-28 17:47:23 -05:00
Chris Tate 3919eec758 Visit only sockets with deferred work in the net sweep (#2489) 2026-10-28 17:32:13 -05:00
Chris Tate 3a1ba89194 Throw for .length of a missing tuple element
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.
2026-10-28 17:04:51 -05:00
Malte UblandClaude Opus 5.5 1962a28fd7 Keep child-process executables byte-identical without the new listener APIs
#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>
2026-10-28 15:02:54 -07:00
Chris Tate 94215d6f0e Keep glibc's memset, memcpy, memcmp and bcmp in zig-linked executables
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.
2026-10-28 17:01:19 -05:00
Chris Tate 5c49604e41 Visit only the sockets with deferred work in the net sweep
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.
2026-10-28 16:49:22 -05:00
Chris Tate 7384ff616e Keep in-loop dyn releases inline in bodies over the release budget (#2485) 2026-10-28 16:49:02 -05:00
Chris Tate 2814b57e20 Send the head writeHead stores with the response's first write or end()
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.
2026-10-28 16:35:39 -05:00
Chris Tate 6cc5345a41 Key Sets and Maps of identity-arm unions by their pointer representation
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.
2026-10-28 16:22:38 -05:00
Chris Tate 3a15562085 Follow a redirect whose chunked body has not ended (#2484) 2026-10-28 16:06:56 -05:00
Chris Tate 4d1e3fbbcb Keep small listener snapshots of readline and EventEmitter emits on the stack (#2483) 2026-10-28 15:51:22 -05:00
Chris Tate 7f950bbbf9 Keep in-loop dyn releases inline in bodies over the release budget
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.
2026-10-28 15:48:47 -05:00
Chris Tate fb5df61367 Reuse lib diagnostics across options that cannot change them (#2472) 2026-10-28 15:41:23 -05:00
Chris Tate 361e660a72 Give the entry module's top-level code a partition of its own (#2480) 2026-10-28 15:31:45 -05:00
Chris Tateandlinguofeng 46058c67a2 Report an aborted client response's error to its 'error' listeners only
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>
2026-10-28 15:19:07 -05:00
Chris Tate 061020b2f8 Add --exceptions=rare to unwind through recursive functions (#2481) 2026-10-28 15:17:22 -05:00
Malte UblandClaude Opus 5.5 a148177432 Check recursive records coinductively, not by refusing the back-edge
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>
2026-10-28 13:17:19 -07:00
Malte UblandClaude Opus 5.5 d4041d6b25 Box promises nested in records, arrays and unions as dyn values
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>
2026-10-28 13:17:19 -07:00
Malte UblandClaude Opus 5.5 54cd19e52a Walk a for..of over an iterable beside never[]
`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>
2026-10-28 13:17:18 -07:00
Malte UblandClaude Opus 5.5 c3954d4dd4 Give a .map callback that answers a closure an ordinary function element
`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>
2026-10-28 13:17:13 -07:00
Malte UblandClaude Opus 5.5 ea23feeb21 Merge a union's promise arms into one promise of their payload union
`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>
2026-10-28 13:17:05 -07:00
Malte UblandClaude Opus 5.5 ab5b603e1f Keep the error itself when an interface extends Error
`interface HttpError extends Error { status: number }` receives `new Error(...)`
asserted to it. The error's fields are writable and later writes must show
through the view, so the view is the error object, not a record built from it.

A lib Error, TypeError, ... instance now carries identity into the structural
view, the same way a program class instance does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:16:21 -07:00
Malte UblandClaude Opus 5.5 b716775730 Keep a Map or Set handed to a record view as the collection
A Map or Set is a live collection: a record copy answers the members it copied
once and misses every later write, so `function freeId(seen: { has(k: string):
boolean })` called with a Map must answer through the Map.

The collection now carries identity into the record view, and the view is
registered as a collection view: it keeps checked identity without protocol
dispatch, because there is no stored member to read. Its method calls dispatch
on the collection at runtime (`scr_native_map_invoke`), and `size` reads the
collection's own member.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:14:47 -07:00
Malte UblandClaude Opus 5.5 8e495686e1 Resolve a named Pick or Omit alias to the class it views
`type Readable = Pick<Session, "read">` is the same live view of a class
instance as the inline `Pick<Session, "read">` spelling. Only the inline form
was recognized, so a view named through an alias became a record copy: it
dropped the class's methods and lost the instance's identity.

The resolved type of a program alias is the alias itself, so the reference is
read from the alias's declaration instead: `Pick`/`Omit` keep their first type
argument, and another alias is followed through its own declaration, bounded
in depth. `originsOf` resolves the view the same way, so the class family
behind a named view is found too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:14:12 -07:00
Chris Tate ee30477142 Keep small listener snapshots of readline and EventEmitter emits on the stack
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.
2026-10-28 15:07:25 -05:00
Chris Tate d83a5dc929 Fix census WP-2 stdlib/@types sites: void-slot lambda ABI, ChildProcess.off/signalCode, writeFileSync const options, path sub-namespaces, stdout writableLength (#2468) 2026-10-28 15:04:48 -05:00
Chris Tate e80b5f1447 Compile .length of computed tuple expressions
`.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.
2026-10-28 14:57:21 -05:00
Chris Tate 1edb46545a Lower more pi constructs: tuple and keyed reads, mixed union fields, spreads, assertion probes (#2476) 2026-10-28 14:51:13 -05:00
Chris Tate 4066cf1d1a Lower Object.getOwnPropertyNames over records and Array.prototype.join with a dynamic separator (#2475) 2026-10-28 14:44:07 -05:00