Commit Graph
2361 Commits
Author SHA1 Message Date
Chris Tate a2e1251759 Prepare v0.4.6 release 2026-10-29 06:02:00 -05:00
Chris Tate b260a13dda Make fewer syscalls per fs thread-pool completion and pool-thread write (#2519) 2026-10-29 00:20:58 -05:00
Chris Tate a1458145fc Keep the pointer when a narrowing drops a pointer union's unit arm (#2517) 2026-10-29 00:20:01 -05:00
Chris Tate 50aa42883f Cache the pool-thread signal check per OS thread
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.
2026-10-29 00:18:11 -05:00
Chris Tate 5f27c4d233 Exit cleanly with Node loaded and keep nested entries on Node's stack (NI-7c, NI-3b) (#1905) 2026-10-28 23:43:21 -05:00
Chris Tate f909bdf6c8 Make fewer syscalls per fs thread-pool completion and pool-thread write
- 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.
2026-10-28 23:34:34 -05:00
Chris Tate a35bf813db Keep the pointer when a narrowing drops a pointer union's unit arm
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.
2026-10-28 23:23:07 -05:00
Chris Tate 686c34bd25 Store tty.isatty and process.cwd as function values (#2494) 2026-10-28 22:46:19 -05:00
Chris Tate 3e7528ca86 pi wave 4: path values, union delete, NonNullable unions, BigInt stats, optional-param function values, live accessor literals, clearer stdlib-alias errors (#2497) 2026-10-28 22:33:16 -05:00
Chris Tate 0b7b678fac Grow the ASCII run before walking for forward index reads (#2502) 2026-10-28 22:27:56 -05:00
Chris Tate 91b5d267b9 Keep frozen checked records frozen, and give sloppy-mode this the global object (#2453) 2026-10-28 22:02:18 -05:00
Chris Tate 2ae08e4002 Route more packages and values through Node islands (NI-4c–4g) (#1904) 2026-10-28 21:53:40 -05:00
Chris Tate 52e3dadc20 Match Node for BigIntStats dates, bigint option effects and path namespace spreads
- 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.
2026-10-28 21:16:57 -05:00
Chris Tate abb87f4dd7 Grow the ASCII run before walking for forward index reads
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.
2026-10-28 21:13:01 -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
Chris Tate 3b520e4fb2 Merge main 2026-10-28 20:50:23 -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
Malte UblandClaude Opus 5.5 cf7814d19c Lower fs statSync/lstatSync/fstatSync with { bigint: true }
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>
2026-10-28 17:58:01 -07: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
Chris Tate 84644dfe9d Send the writeHead head with the response's first write (#2488) 2026-10-28 19:18:26 -05:00
Chris Tate db30118a0e Store tty.isatty and process.cwd as function values
Fixes #2408
Co-authored-by: MathurAditya724 <57684218+MathurAditya724@users.noreply.github.com>
Fixes #2402
2026-10-28 19:18:01 -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 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 9f3fb0fb17 Keep glibc's memset, memcpy, memcmp and bcmp in zig-linked executables (#2490) 2026-10-28 17:47:23 -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 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 3a15562085 Follow a redirect whose chunked body has not ended (#2484) 2026-10-28 16:06:56 -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 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
Malte UblandClaude Opus 5.5 c3e4f2ced8 Merge remote-tracking branch 'origin/ni/i7-routing' into ni/i8-exit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:57:23 -07:00
Malte UblandClaude Opus 5.5 d4445ec628 Merge remote-tracking branch 'origin/ni/i6-subclasses' into ni/i7-routing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:57:10 -07:00
Malte UblandClaude Opus 5.5 81580f44ad Merge remote-tracking branch 'origin/main' into ni/i6-subclasses
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:56:30 -07:00
Malte UblandClaude Opus 5.5 753532a3da Merge origin/main into fix/pi-wp2-stdlib-node
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:33:39 -07:00
Chris Tate 44a4cd9db1 Merge main 2026-10-28 13:19:00 -05:00
Chris Tate 2f3b926de8 Share modules between a program and its Node islands through loader hooks (NI-6) (#1902) 2026-10-28 12:41:52 -05:00
Malte UblandClaude Opus 5.5 5d8d6b35a1 Address the child-lifecycle, stream-alias and fs write-options reviews
- 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>
2026-10-28 09:15:37 -07:00
Malte UblandClaude Opus 5.5 10f974aab9 Merge ni/i6-subclasses into ni/i7-routing
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>
2026-10-28 08:59:57 -07:00
Malte Ubl 8a918e7aff Merge branch 'ni/i7-routing' into ni/i8-exit 2026-10-28 08:47:47 -07:00
Malte UblandClaude Opus 5.5 0fb9e25464 Merge ni/i6-subclasses into ni/i7-routing
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>
2026-10-28 08:47:41 -07:00
Malte UblandClaude Opus 5.5 3b86b83c45 Merge origin/main into ni/i6-subclasses
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>
2026-10-28 08:46:53 -07:00
Malte Ubl e5f57e2093 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-28 08:43:13 -07:00
Malte Ubl 29a31446d3 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-28 08:43:06 -07:00
Chris Tate a484390e07 Merge main 2026-10-28 09:50:05 -05:00
Chris Tate ec79f2aca0 Destructure narrowed and joined union sources, await promise-armed unions, support writeFileSync(fd) (#2466) 2026-10-28 08:57:38 -05:00
Malte UblandClaude Opus 5.5 6ec5b3ff58 Merge ni/i7-routing into ni/i8-exit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:42:43 -07:00