Commit Graph
766 Commits
Author SHA1 Message Date
Chris Tate 85439ae670 Merge main 2026-10-16 19:54:32 -05:00
Chris Tate 451924b3d0 Lower statSync's throwIfNoEntry option without building the error (#1136) 2026-10-16 19:38:44 -05:00
Chris Tate 5a091f050f Run the zlib one-shot forms on the stream engine (#1139) 2026-10-16 19:11:18 -05:00
Chris Tate 26cdf9cc1d Merge main 2026-10-16 19:09:58 -05:00
Malte Ubl 8f5ec2d94f Report record width copies at assertions, assignments and array elements
SC6001 only looked at call arguments and typed initializers, and only at
strict field subsets: a target that adds optional properties, the empty
record type, `r as T`, a later `v = r` into a typed binding, and records
stored as elements of a typed array literal copied silently.

The scan now reports a copy whenever the property sets differ, covers
those sites, and reports a class instance that reaches a plain record
type which does not keep its identity (the remaining boundary: interfaces
of only generic methods). Every corpus program still reports no
divergence.
2026-10-16 14:02:02 -07:00
Chris Tate de08ab1920 v0.3.5 2026-10-16 10:33:14 -05:00
Chris Tate fe86c85902 Support socket.address(), resetAndDestroy() and dropMaxConnection in net
socket.address(), socket.resetAndDestroy() and server.dropMaxConnection
were refused at compile time, and address() and resetAndDestroy() also
through untyped socket handles. They now match Node:

- address() answers { address, family, port } while the socket has a
  local address, and {} while it connects or once it is destroyed. The
  declared AddressInfo | {} result is a checked-dynamic value, so a type
  assertion to AddressInfo narrows it.
- resetAndDestroy() closes the connection with an RST (SO_LINGER with a
  zero timeout), so the peer reads ECONNRESET, and 'close' reports
  hadError false. A connecting socket resets once it connects: the reset
  is a once('connect') listener, so it runs at that place among the
  'connect' listeners. A destroyed socket is left alone, a TLS socket
  throws ERR_INVALID_HANDLE_TYPE, and the call answers the socket.
- dropMaxConnection is a stored value (undefined until set), since Node
  reads it only in cluster mode.

Corpus 5657 (typed) and 5658 (untyped handles and the TLS throw) and the
node-types fixture net-reset-address.ts cover each member.
2026-10-16 10:01:22 -05:00
Chris Tate 01f25f6b3e Lower util.promisify over the fs callback functions (#1138) 2026-10-16 09:37:58 -05:00
Chris Tate d533fd7ce9 Merge main 2026-10-16 09:33:08 -05:00
Chris Tate f4445f1540 Keep undeclared keys of checked JSON records; lower compound assignment with untyped operands (#1123) 2026-10-16 09:20:49 -05:00
Chris Tate ed5ab6796b Run the zlib one-shot forms on the stream engine
deflateSync, inflateSync, gzipSync, gunzipSync, deflateRawSync,
inflateRawSync and unzipSync, and their callback forms, accept Node's
options and run Node's processChunkSync over the zlib stream engine.

- Options (windowBits, level, memLevel, strategy, dictionary, chunkSize,
  finishFlush, maxOutputLength, rejectGarbageAfterEnd) are validated at
  run time with Node's ladder, shared with the stream classes; info is
  refused at run time.
- Errors carry Node's errno and code; maxOutputLength reports
  ERR_BUFFER_TOO_LARGE and rejectGarbageAfterEnd
  ERR_TRAILING_JUNK_AFTER_STREAM_END. The default-options decoders also
  read every gzip member.
- Untyped JavaScript inputs may be strings, TypedArrays, DataViews or
  ArrayBuffers; other values throw Node's ERR_INVALID_ARG_TYPE after the
  options are checked.
- Results are Buffers, as in Node: they convert to their text.

Corpus programs 5378, 5379 and 5415 cover the sync, callback and untyped
input forms.
2026-10-16 09:18:32 -05:00
Chris Tate 06f69e0d25 Dispatch closed virtual calls directly and reuse Map lookups for read-modify-write (#1134) 2026-10-16 09:17:28 -05:00
Chris Tate daa5cc256f Lower util.promisify over the fs callback functions
promisify(fs.X) over a named or namespace import of node:fs lowers as
the fs/promises member of the same name for access, appendFile, chmod,
chown, copyFile, lchown, link, lstat, lutimes, mkdir, mkdtemp, readFile,
readdir, readlink, realpath, rename, rm, rmdir, stat, statfs, symlink,
truncate, unlink, utimes and writeFile. For these, Node's promisified
callback form settles with the callback's single result and rejects
with the same errors as the fs/promises call. Calls through the bound
function accept the forms the fs/promises lowering accepts and refuse
the others by name.

fs.open (an fd number where fs/promises.open gives a FileHandle),
fs.exists (a custom promisify) and fs.read/fs.write (multi-value
callbacks) remain refused.
2026-10-16 09:14:35 -05:00
Chris Tate 241006cff3 Add fs read and write streams (#1089) 2026-10-16 09:02:48 -05:00
Chris Tate 61de523d3e Lower statSync's throwIfNoEntry option without building the error
fs.statSync(path, options) and fs.lstatSync(path, options) now compile
with an inline options object, where throwIfNoEntry takes any boolean and
bigint must be false, or with a stored options object whose only member
is throwIfNoEntry, read at each call. With throwIfNoEntry: false the
result is Stats | undefined, and a missing entry (ENOENT, or ENOTDIR for a
path through a file, a dangling link's stat included) answers undefined
without building the error object, as Node's binding does. Other
failures still throw. Before, any second argument was refused.

Stats | undefined is a nullable union, so the result is the Stats pointer
itself (NULL for undefined) rather than a union box, and Stats snapshots
come from the small-object allocator. A stat-and-test probe such as
`stat !== undefined && stat.isFile()` therefore makes no system
allocator calls. stat?.isFile() and the other type tests also lower
through optional chains.

existsSync answers false for a path with a null byte, as Node does,
instead of testing the path up to that byte.

Corpus 5747 and 5748 cover the typed and JavaScript forms.
2026-10-16 09:01:01 -05:00
Malte Ubl 1127b4c7b6 Pin guarded dispatch in the inheritance codegen tests
Both hierarchies construct the base and the subclass, so the base-typed
call sites are closed sets now: they still load the vtable slot, then
call each implementation directly behind a slot test.
2026-10-16 06:54:45 -07:00
Chris Tate 21c2340ef6 Support the dgram socket options and disconnect() (#1122) 2026-10-16 08:39:23 -05:00
Chris Tate 451e925b12 Merge main 2026-10-16 08:32:21 -05:00
Malte Ubl a850eca98d Pin kept extras in the width-tolerance dyncheck test
The checked conversion now keeps undeclared keys for serialization, so the scriptc-only test that pinned dropping them compares against Node instead.
2026-10-16 06:31:52 -07:00
Malte Ubl dff726715b Keep undeclared keys of checked JSON records
`const parsed: { score: number }[] = JSON.parse(doc)` printed a different
checksum from Node: the checked conversion (and the schema-directed
JSON.parse fast path) stored only the declared fields of each object in
its native record, so JSON.stringify(parsed) silently dropped `id`,
`name` and `active`, and every other reflective surface agreed.

A closed record converted from an ordinary object now keeps the source's
own properties beside it when the object has undeclared keys or holds the
declared ones out of declaration order. The runtime keeps a per-thread
table from the native record to a view of the source's own keys
(placeholders for declared keys, which the record still owns), released
through the native dispose hook when the record is freed:

- the checked record builder notes its source (scr_record_source_note);
- the schema parser parses undeclared members into the view instead of
  declining, so typed parses keep their one-pass fast path;
- typed JSON.stringify writes such records in source key order, declared
  fields through a generated per-shape field writer and the others through
  the dyn writer;
- record capsules adopt the view and refresh it in place, so untyped reads
  and writes see and persist undeclared properties;
- util.inspect and Object.keys/values/entries answer through the live view
  (values/entries check the extra values into their static result type and
  throw the checked-conversion TypeError when one does not fit).

Copies made by object spread, Object.assign, rest destructuring or
conversion to another record type still hold only the declared fields;
the limitations page documents this.
2026-10-16 06:31:51 -07:00
Malte Ubl 6cd96eec49 Lower compound assignment with untyped operands like the expanded form
`checksum += parsed[i].score` (a number local plus an `any` value) failed
with SC1043 "comparing non-number, non-string values" in TypeScript, while
the expanded `checksum = checksum + parsed[i].score` compiled: the
compound lowering only routed checked-dynamic operands through the
runtime operators in JavaScript files.

Locals, record fields and array elements now lower `op=` with an untyped
right operand (or an untyped local target) exactly like `x = x op rhs`:
dyn.add / dyn.arithmetic / dyn.bitwise, then the checked conversion into
the target's declared type. Numeric operators keep Node's ToNumber
semantics; a `+=` that produces a string for a `number` target throws the
same catchable TypeError as the expanded assignment and leaves the target
unchanged (pinned in tests/harness/dyncheck.test.ts).

Remaining SC1043 refusals for arithmetic, unary, update and compound
operators now name the operator and operand types instead of the
comparison wording.

The retired-engine-763 diagnostics fixture compiles now and matches Node,
so it moves to the corpus as 5343.
2026-10-16 06:31:51 -07:00
Chris Tate a5da46267b Merge main 2026-10-16 08:31:20 -05:00
Chris Tate 21c0e17178 Merge main 2026-10-16 08:17:52 -05:00
Chris Tate 4a56c17be7 Release a FileHandle stream's owner when its options are refused
scr_fss_new takes ownership of the FileHandle owner record (and its
reference to the handle), but its first two refusal paths returned before
the shared cleanup, so a FileHandle stream refused by its options, such
as fh.createReadStream({ encoding: "nope" }), leaked both. Those paths
now release the owner as the later ones do.

Corpus 5554 refuses a FileHandle stream and then closes the handle, and
tests/harness/fs-stream-leaks.test.ts runs the fs stream programs under
LeakSanitizer in the sanitized lane: 5554 reports the leak without this
change.
2026-10-16 08:16:46 -05:00
Chris Tate df335ad719 Prove int32 loop arithmetic through calls and module-level code (#1125) 2026-10-16 08:13:18 -05:00
Chris Tate 6da5ad6eb9 Lower events.once over EventEmitter instances (#1061) 2026-10-16 08:01:35 -05:00
Chris Tate 4adc3e6f2a Support the dgram socket options and disconnect()
dgram.Socket's setBroadcast, setTTL, setMulticastTTL,
setMulticastLoopback, setMulticastInterface, addMembership,
dropMembership, get/setRecvBufferSize, get/setSendBufferSize,
getSendQueueSize, getSendQueueCount and disconnect were refused at
compile time. They now lower to scr_dgram.c and match Node:

- The TTL setters and setMulticastLoopback return their argument. The
  value checks run first: a TTL outside 1-255 (0-255 for multicast,
  after ToInt32) throws "setTTL EINVAL" with code, errno and syscall.
- A socket that has not been bound has no descriptor yet, so the
  options throw EBADF, and the buffer size methods throw Node's
  ERR_SOCKET_BUFFER_SIZE SystemError.
- After close(), the members that read Node's null handle throw its
  TypeError ("Cannot read properties of null (reading 'setTTL')"), and
  setMulticastInterface and the membership calls throw "Not running".
- Buffer sizes must be uint32 values (ERR_SOCKET_BAD_BUFFER_SIZE), 0
  reads the current size, and sizes past the int32 range fail as EINVAL.
- IPv6 interfaces and groups go to the IPv6 socket options, so a udp4
  socket answers ENOPROTOOPT as in Node.
- disconnect() throws ERR_SOCKET_DGRAM_NOT_CONNECTED until 'connect' was
  emitted, and dissolves the association with an AF_UNSPEC connect.

addMembership and dropMembership before bind() throw SC2020, where
Node binds the socket to an ephemeral port first; the limitations page
documents it, along with the missing info object on the buffer size
SystemError. Corpus 5655 and 5656 and the node-types fixture
dgram-options.ts cover each method.
2026-10-16 07:34:12 -05:00
Chris Tate 60623086a3 Lower util.promisify(child_process.exec)
A const binding of promisify(exec) now lowers like promisify(execFile):
each call runs the command line with /bin/sh -c, as execSync does, and
settles with { stdout, stderr }. A non-zero exit, a missing command (the
shell's 127) or a timeout rejects with Node's "Command failed: <command>\n
<stderr>" message. The options are the execFile slice: encoding utf8,
cwd, env (inline spread included), timeout, and maxBuffer accepted.

Three gaps closed for both targets along the way:

- the { stdout, stderr } result maps as a record when it is held in a
  variable, passed or returned, instead of only when destructured at the
  await (it is an anonymous literal in @types/node and in the fallback);
- a namespace member is a recognized target (promisify(cp.exec));
- util.promisify(...) through a namespace or default import is
  recognized like the named import.
2026-10-16 07:32:06 -05:00
Chris Tate 90a6e66422 Merge main 2026-10-16 07:31:21 -05:00
Chris Tate df74099468 Free released regions without a pass over the whole heap
Every release of a LifetimeRegion, and every retry at region creation, ran
scr_collect_cycles(): full cycle-collector passes over the thread's heap, to
a fixpoint, before checking any region, in case dead cycles of ordinary
objects held counted references into it. Freeing a region ran them again
whenever the owned ordinary objects left counts behind after the region
released its fields. Next to a large heap of candidates (a type checker
beside per-file emit regions) those passes cost far more than the regions.

A retry now checks the due regions first. A region whose references are all
explained frees without a pass; only when one would be kept does the heap
get the full pass, after which every due region is judged as before. The
first check leaves a region it cannot free as it was, so the backoff, the
kept report and SCRIPTC_REGION_DEBUG see only the check after the pass. It
keeps what its walk found: a region whose counted references the pass did not
change is judged from that, without walking it again. A pass that freed a dead
cycle referring only to an owned object leaves the count unchanged; such a
region waits for its next attempt.

After a region releases its instances' fields, what can still hold counts
is the owned ordinary objects' cycles. Each has a member that lost a
reference in those releases and is buffered as a candidate, and the release
walk entered every cycle member. The cycle collector gains a pass limited to
a set of candidates (scr_cyc_collect_set): it takes the set's members out of
the buffers up to a given generation, reading only the buffers' pointers, so
the candidates it leaves are not touched, and it walks at full depth without
moving the schedule's baselines. A dead object such a pass reaches through a
candidate it did not take leaves its buffer before it is freed. Freeing a
region runs these passes over the walk's objects from the youngest buffer
out, and the whole heap only if counts remain.

SCRIPTC_REGION_STATS also prints, per thread at exit, how many full passes
over the heap regions needed.
2026-10-16 07:01:01 -05:00
Chris Tate 2e8b836a79 Merge main 2026-10-16 04:33:13 -05:00
Chris Tate eb48e1d2e4 Pool small Buffers as Node does (#1101) 2026-10-16 03:50:17 -05:00
Chris Tate 6bdb194a04 Keep integer-slot checking working in the native compiler for library calls with bytes or array arguments (#1105) 2026-10-16 03:45:27 -05:00
Chris Tate e5dc49c0cb Refresh the Effect fixture costs from CI measurements (#1097) 2026-10-16 03:38:04 -05:00
Chris Tate 4858a24420 Merge main 2026-10-16 03:30:56 -05:00
Chris Tate 383f43bdeb Keep integer-slot checking working in the native compiler for library calls with bytes or array arguments
The global-effects summary typed each library-call argument as a
structural `{ kind: string }`. The native compiler extracted that one
record arm from the IrType union and threw on bytes, array and other
arms with more fields, so `scriptc build --lib` failed with SC3004 for
integer-slot profiles that call, for example, `Buffer.alloc` or
`String.fromCharCode`. The helper now takes `IrType`, and the bootstrap
test compares native and Node sidecars for such a profile.
2026-10-16 02:51:23 -05:00
Chris Tate bf98a562d8 Run each darwin corpus slice as two concurrent halves
The macOS lane of pull requests and pushes to main runs the darwin suite in
three slices. Each slice runs differential.test.ts over a third of the
corpus while the darwin contracts run beside it. vitest runs one file's
cases in sequence, so a slice's corpus time is the sum of its case times:
in three green main runs, 587-891 s (plain) and 800-1,731 s (sanitized),
on 3-core runners whose contracts task ends after 46-180 s.

ciTestPlan now runs slice i of n as case shards 2i-1 and 2i of 2n in two
vitest processes: the first beside the side queue, the second in the side
queue after the darwin contracts. The halves of all slices are the case
shards of 2n slices, so every program still runs once.

The comment above darwinFiles now says which macOS suite each CI event
runs: pull requests and pushes to main run the darwin suite, and the
nightly schedule and manual runs with macos_full run the full suite.
2026-10-16 01:55:40 -05:00
Chris Tate 0d59abe143 Pool small Buffers as Node does
Buffer.from of strings, arrays and Uint8Arrays, Buffer.allocUnsafe and
Buffer.concat now return views into a per-thread pool slab of
Buffer.poolSize bytes (64 KiB by default) when the size is below
Buffer.poolSize >>> 1, following Node's allocate, fromStringFast and
fromArrayLike: 8-byte aligned offsets, a new slab when a request does not
fit, and the four-bytes-per-unit reservation for strings. Buffer.poolSize
can be read and set, and Buffer.allocUnsafeSlow returns an unpooled Buffer.
A slab lives while any of its views does. Its ArrayBuffer cannot be
transferred, and structuredClone of a typed array view copies the whole
ArrayBuffer and keeps the view's offset. Buffer.alloc, allocUnsafe and
allocUnsafeSlow validate sizes with Node's ERR_OUT_OF_RANGE error.

The first pooled allocation finds the startup slab, which has the
default 64 KiB size. A request it cannot hold gets a slab of the current
Buffer.poolSize, as in Node, and the room is checked after every new
slab, so a Buffer.poolSize raised before the first pooled allocation
cannot place a reservation past the end of a 64 KiB slab. A reservation
larger than a fresh slab traps instead of writing past it.

Corpus 5531 (typed) and 5532 (untyped) raise Buffer.poolSize before the
first pooled allocation and allocate just under the new threshold
through each pooled path, each case in a fresh child process, and also
shrink Buffer.poolSize and change it between allocations. The
node-types fenced fixture no longer lists Buffer.poolSize, which now
compiles.
2026-10-16 01:39:10 -05:00
Chris Tate a09af4f721 Report what keeps a region with SCRIPTC_REGION_DEBUG (#1095) 2026-10-16 01:12:28 -05:00
Chris Tate 6f63520c98 Emit Atomics operations inline as atomic instructions (#1094) 2026-10-16 00:54:32 -05:00
Chris Tate c8999078ca Refresh the Effect fixture costs from CI measurements
tests/harness/effect-costs.json and effect4-costs.json were recorded on
2026-10-01 and give the fixtures' relative cost for balancedShardSelect
across the 16 Effect shards of each flavor. Against the fixture times of
three green main runs (38851650148, 38851726594, 38852266290), the old
tables put 1,171 s of plain and 1,295 s of sanitized fixture time on the
longest shard, against a mean of 1,027 and 1,111 s.

scripts/ci-effect-costs.mjs refreshes both tables from Effect job logs:
each fixture's plain and sanitized medians, summed in seconds, as the
tables recorded before. Fixtures without a measurement keep their value,
and fixtures that no longer exist are removed. With the refreshed tables,
the longest shard of the same measurements carries 1,082 s (plain) and
1,144 s (sanitized).
2026-10-16 00:50:37 -05:00
Chris Tate ba72bb6420 Merge main 2026-10-16 00:50:23 -05:00
Chris Tate 30bb7d4703 Add mock.fn and t.mock.fn with Node's calls log and implementation controls (#1092) 2026-10-16 00:49:22 -05:00
Chris Tate a8d154f370 Report what keeps a region with SCRIPTC_REGION_DEBUG
A released region that something outside still refers to is kept, and
SCRIPTC_REGION_STATS only said how many references the walk could not
explain. In a build with --optimization=dev (or --sanitize), setting
SCRIPTC_REGION_DEBUG=1 (or a larger number, the line limit) makes each
attempt that keeps a region also print what keeps it:

- per chunk, the references to its objects that the walk did not find
  (from variables, other regions or objects it does not walk), with the
  classes the chunk holds, since counts are kept per chunk;
- each ordinary object the region owns that is referenced from outside,
  named by the region class and field it was reached through, with its
  reference count and the references the walk found.

The emitted region class descriptors gain the names of the fields their
visits report, in visit order. Debug walks give every owned object an
entry, so that each one records where the walk first reached it. The
bookkeeping is compiled only into unoptimized and RC-audit runtimes:
compiled in, it made the release walk 6.5% slower even with the variable
unset. Optimized runtimes walk as before and, when the variable is set,
say once that the reports need a dev build. Retains, releases and
allocation are unchanged.

Adds corpus program 5703.
2026-10-16 00:35:06 -05:00
Chris Tate 93bc902cd8 Emit Atomics operations inline as atomic instructions
Atomics.load, store, exchange, compareExchange, add, sub, and, or and xor
on an integer typed array compile to one sequentially consistent LLVM
atomic instruction on the element. An index that is not an integer in
range takes the runtime operation, which applies ToIndex and raises its
errors.
2026-10-16 00:31:52 -05:00
Chris Tate d64d19bf1e Split the native frontend test across three files (#1088) 2026-10-16 00:25:32 -05:00
Chris Tate 42954a3266 Add mock.fn and t.mock.fn with Node's calls log and implementation controls
node:test's mock.fn(original[, implementation]) and t.mock.fn compile.
A Mock<F> is the function-with-properties record { %call, mock }: the
callable is a wrapper closure that shares the mock context with the mock
field. The context records each call's arguments, result, error and this,
and keeps the current implementation, the original and the one-time
implementations by call index. fn.mock supports, as in Node 24.22:

- calls, read as a copy of the log;
- callCount();
- mockImplementation(fn);
- mockImplementationOnce(fn[, onCall]), for the next call by default, a
  later registration for the same index replacing the earlier one;
- resetCalls();
- restore(), which brings back the original.

A throwing implementation records the error and rethrows it. Functions
with fixed parameters or a single rest array parameter are supported; a
union return type needs an undefined arm. mock.fn() without an original,
mock.method, the tracker's reset and restoreAll, and t.mock.method are
refused with SC2020.

The node:test fixture mock-fn compares the behavior with Node, and
tests/diagnostics/mock-fences.ts covers the refusals.
2026-10-16 00:22:22 -05:00
Chris Tate de0917284d Merge main 2026-10-16 00:05:07 -05:00
Chris Tate 93dcd9865e Merge main 2026-10-16 00:01:13 -05:00
Chris Tate 9acf71cfe3 Split the native frontend test across three files
self-hosting-native-frontend.test.ts was one test of 577 s (plain) and 607 s
(sanitized) in the cost table, and 748 s on san 1/20 in run 38835202089; once
the other long files were spread, it alone bounded the Linux test lane. It
builds the compiler frontend into a native executable, then checks 17 corpus
programs and four other cases with it. Locally the build took about 189 s and
the checks 297 s (14-20 s per program).

The build and the checks move to tests/harness/native-frontend.ts without
changes to what they check; useNativeFrontend builds the executable in a
beforeAll of the calling suite. self-hosting-native-frontend.test.ts,
-programs.test.ts and -checks.test.ts each build once and run programs 1-6,
7-12, and 13-17 plus the four other checks, each check as its own test. The
diagnostics, library contract and FFI checks use project directories of their
own instead of rewriting one shared main.ts.

scripts/ci-test-costs.json replaces the old file's entry with estimates for
the three files (the build share of its measured cost plus each file's share of
the checks); the next refresh measures them, since vitest's file time includes
beforeAll. The largest side-queue share per shard at 20 shards goes from 580 to
367 s (plain) and from 608 to 432 s (sanitized), for two more builds per flavor.
2026-10-15 23:46:14 -05:00