Commit Graph
186 Commits
Author SHA1 Message Date
Chris Tate 1f7f029a05 Carry AsyncLocalStorage stores into timer, immediate, tick and microtask callbacks
AsyncLocalStorage changes, matching Node 24.22:

- setTimeout, setInterval, setImmediate, process.nextTick and
  queueMicrotask callbacks run with the stores that were active when they
  were scheduled. Before, getStore() returned undefined in them.
- An interval keeps that context on every tick, and a refreshed timer
  keeps it too.
- enterWith inside one of these callbacks lasts until the callback
  returns. An enterWith in top-level code ends when the main module's body
  returns, and one inside a callback scheduled with no store active ends
  when that callback returns, so the store does not reach callbacks that
  were scheduled without it.

Programs that enter stores (store.run, exit, enterWith, or a
diagnostics_channel store binding) schedule through new *_als runtime
entry points, which wrap a callback scheduled with a store active in a
closure that installs the stores around the call. The emitter picks them
from the new asyncContext runtime feature; other programs keep the plain
entry points, so their scheduling is unchanged.

The limitations page notes that completion callbacks of callback-style
I/O APIs still run with no store.

5815-als-callback-context compares the stores each kind of callback sees
with Node, plain and with SCRIPTC_SAN=1.
2026-10-18 22:26:09 -05:00
Chris Tate 684f73d186 Add ThreadSlots, per-thread values on shared objects, to @scriptc/experimental (#1271) 2026-10-18 20:39:11 -05:00
Chris Tate cf11fbaaeb WebAssembly W0–W2: free handle tags, a cross-thread clone hook, the WebAssembly types and refusals, and a decoder that reports every module a program can load (#1339) 2026-10-18 18:12:42 -05:00
Chris Tate b32f4e31f3 Merge main 2026-10-18 17:47:55 -05:00
Chris Tate 2f40bcb8d4 Lower iterator helpers on generators and Map and Set iterators
map, filter, take, drop, flatMap, toArray, forEach, reduce, some, every
and find compiled only as one chain from arr.values() or arr.entries()
that ended in a consuming method. Every other form was refused, and in
JavaScript the refusal was thrown at run time.

IteratorObject now maps to the generator type. A consuming method over
a chain of stages fuses into one loop over the source: a generator
driven like for-of, a Map or Set walked live, or an array by index. A
stage on its own builds a helper object: a generator over its source
that supports next(), for...of, spreads, Array.from and yield*. A
for...of whose head is a chain of stages without flatMap runs the
stages inside the loop.

The source is closed where Node closes it: when take reaches its count,
when some, every or find has its answer, when a callback or the loop
body throws, when the loop exits early, and before the RangeError of an
invalid take or drop budget. It is not closed when it is exhausted or
its own next() throws. return() on a helper object before its first
next() closes the source through a new runtime hook,
scr_gen_set_unstarted_return.

When closing an iterator throws while another error propagates, the
first error reaches the caller, as in Node. This applies to for...of
over a generator too, where the error from the close used to replace
the body's error. The try statement takes a dropFinallyErrors flag,
whose cleanup handler calls the new scr_exc_keep_first.

throw() lowers only on values typed as generators. Iterator helpers and
the built-in iterators have no throw method, and a value typed as an
iterator may hold one.

Callbacks in JavaScript bind their unannotated parameters to the
element type, and flatMap callbacks may return generators. A generator
whose record yields share checked storage with its return channel has
each value converted to the declared element type as it is pulled.
2026-10-18 17:46:31 -05:00
Malte UblandClaude Opus 5.5 daf4ff6a2b Decode and validate the WebAssembly modules a program can load, and report them in coverage
`scriptc coverage` now lists every WebAssembly module whose bytes the
build can see, with its size, whether it validates, its function, import
and export counts, and the WebAssembly proposals it uses. Nothing is
translated yet; builds are unchanged.

- packages/compiler/src/wasm/binary.ts decodes and validates a module in
  plain TypeScript (the native compiler compiles it too) at the feature
  level scriptc means to translate: the MVP plus sign-extension,
  non-trapping float-to-int, bulk memory, multi-value, mutable globals,
  reference types and extended constant expressions. Where WebAssembly
  3.0 and V8 relaxed a 2.0 rule (global initializers reading defined
  globals, memory indices), it follows V8, the behavior oracle. Modules
  that use SIMD, threads, exception handling, tail calls, function
  references or GC, memory64 or multiple memories still decode, and are
  reported as needing that proposal instead of as invalid.
- packages/compiler/src/wasm/discover.ts finds modules from four sources:
  paths the compiler resolves from the source text (require.resolve,
  createRequire(...).resolve, import.meta.resolve,
  new URL(..., import.meta.url), __dirname joins), inline base64 and byte-array
  literals, the .wasm files of reached packages whose code mentions
  WebAssembly, and a `scriptc.wasm` list in package.json or the new
  coverage-only `--wasm <file>` flag. Modules are reported once, by
  SHA-256, with every source that found them; module files are input
  probes of the coverage verdict cache. The JSON envelope
  (--print=diagnostics) carries the same records under `wasmModules`.

Evidence: packages/compiler/test/wasm-spec.test.ts runs the decoder over
the 3,017 distinct binary modules of the WebAssembly 2.0 core test suite
(without SIMD) and the extended-const proposal's tests, generated by
gen-wasm-spec-cases.mjs with wabt's wast2json, and requires V8's verdict
for each; 8,000 seeded mutations of those modules must also agree with
V8, as must undici's inline llhttp modules (scalar valid, SIMD reported)
and es-module-lexer's. src/wasm/binary.test.ts checks each feature and
each error class against V8. On the survey's real modules the decoder
agrees with V8 for quickjs-wasi (637 KB, 36 ms) and photon (1.9 MB,
121 ms). tests/coverage-fixtures/wasm pins the four discovery sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:19:39 -07:00
Chris Tate 356d968528 Merge main 2026-10-18 17:07:58 -05:00
Malte UblandClaude Opus 5.5 147013d7b9 Merge origin/main into opt/npm-small-fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:58:58 -07:00
Malte UblandClaude Opus 5.5 8597a364f6 Merge origin/main into opt/npm-small-fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:45:47 -07:00
Malte UblandClaude Opus 5.5 577b0135a9 Add execution tiers to the IR and a report-only tier analysis
Stage S2 of the two-tier design. IrFunction gains a tier (absent means
static); lowering assigns it from function bodies, placing functions that
use the dyn vocabulary in the dynamic tier, and the validator enforces
invariant I1: a static function performs no dynamic operation, only
boundary operations (dynFrom, dynCheck, kind tests) and moves. Serialized
IR keeps the field out in this stage and loaders derive it again, so every
IR, LLVM and executable artifact is unchanged.

Every committed refusal and runtime fence becomes a recorded demotion
request attributed to the binding, field, function instance, class or
module the dynamic tier would take over, while the compiler still refuses
and fences as before. `scriptc coverage` reports dynamic-tier statements
(statementsDynamic), `--explain-dynamic[=<name>]` prints the demotion
chains, and `--tier=auto|static-only` is wired through build, run and
coverage for stage S6; both modes compile identically today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:12:27 -07:00
Chris Tate 2de6f617e6 Merge main 2026-10-18 11:08:54 -05:00
Chris Tate 3de60e4332 Merge main 2026-10-18 09:55:59 -05:00
Chris Tate 0d4e6a0cc1 Compile classic CommonJS packages: late requires, JSON requires, nested builtin destructuring, settled require cycles (#1287) 2026-10-18 09:16:05 -05:00
Chris Tate 4ed75111a7 Carry lifetime regions across await and reuse freed region chunks (#1194) 2026-10-18 08:47:04 -05:00
Malte Ubl 8255315883 Merge origin/main into opt/cjs-interop 2026-10-18 06:46:23 -07:00
Chris Tate 1663d6d35e Merge main 2026-10-18 08:45:06 -05:00
Malte UblandClaude Opus 5.5 ae91df908e Admit require cycles whose modules settle their exports first
A require cycle was always refused (SC1016). Node answers a require of a
module that is still evaluating with its exports as they stand, while the
compiled bindings alias the final declarations; the two agree when every
module of the cycle assigns all of its exports before its first statement
that can run another module's code, every other use of its export object
is a plain member read, and no module's top-level code can reach its
binding of another member. Each module then initializes exactly as it
would outside the cycle. This admits semver's comparator/range cycle
("hoisted class for cyclic dependency"); other cycles keep SC1016 with a
reason naming the offending statement.

The require reachability scan no longer treats a declaration's own name
as a read (esbuild's `var X = class {...}` before its requires), treats a
plain write of an export member as running only a setter published for it
(on-finished's `module.exports.isFinished = isFinished`), and keeps the
conservative scan for writes of __proto__, __esModule and members of a
replaced export object.

Evidence: corpus 6145 (the cycle), 6142 gains the esbuild layout; the
npm-static harness compiles the published fs.realpath@1.0.0,
on-finished@2.4.1 (with ee-first@1.1.1) and statuses@2.0.1 statically;
program.test.ts pins the TDZ analysis through var bindings plus the cycle
admission and refusals. Preflight/order baselines recorded for the new
fixtures; a full re-record changed no other fixture. Dependencies and
limitations docs describe the supported require forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 06:24:04 -07:00
Malte UblandClaude Opus 5.5 1c19d6613d Compile the deprecated punycode and constants builtin modules
- node:punycode: encode, decode, toASCII, toUnicode, ucs2.decode,
  ucs2.encode and version, ported from the punycode.js 2.1.0 Node bundles
  (scr_url.c), with its RangeErrors. DEP0040 follows Node: a static ES
  import reports before any module evaluates and before the program can
  register a 'warning' listener; require() reports on the next tick, once
  for both spellings, not when a dependency under node_modules loads the
  module first, and not under process.noDeprecation.
- node:constants: the frozen object Node builds from os.constants'
  dlopen, errno, priority and signals groups, fs.constants (node_constants.cc
  order and guards) and crypto.constants (scr_os.c); its bindings read it as
  a checked-dynamic value.

These unblock tr46 (whatwg-url, node-fetch@2) and graceful-fs (fs-extra).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 06:23:56 -07:00
Malte UblandClaude Opus 5.5 10c5e269d7 Merge origin/main into opt/06-regions
Resolve the scr_resume_fiber conflict with stackless async coroutines
(#1204): the region the fiber carries is made current before either
resume path and restored after both. scr_async_spawn_stackless now
captures the spawning region like the fiber spawns, so stackless bodies
(every async body in 5013 and 5023 after the merge) keep allocating in
their region after an await and keep it alive while they exist.

5024-lifetime-region-await-forms pins regions carried by stackless and
fiber (for await) bodies that spawn and await each other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 06:23:44 -07:00
Chris Tate 4fe392cd25 Return a worker's memory once it and the other workers are done
On Linux, a worker thread that exits without having made any object it
allocated reachable from another thread gives back all of its memory: the
pages it carved, including ProgramLifetime instances and other objects it
never freed, and the system blocks it still holds. The memory goes back
once no other worker thread is running, so that returning it does not slow
the workers that are still running.

A worker carves past each slice's frontier while the arena's free lists and
returned regions are set aside, takes from those free lists only once a
slice has no bump room left, and records the system blocks it allocates.
Messages and intern tables that share published graphs by pointer, and the
process-wide key arrays of typed records, mark the thread as sharing; so
does a remote free of one of its blocks. A worker that did not share parks
with its arena claimed and its discard queued: returning pages makes the
kernel interrupt every running thread of the process to flush its TLB, and
glibc's trim locks the arenas other threads allocate from. The last worker
to park starts one helper that returns each queued worker's pages, frees
its system blocks, and restores its arena to its state at the claim, with
the free blocks the worker took free again. A thread that finds every arena
claimed runs the queued discards itself. A worker that shared keeps its
memory as before.

SCR_SA_DISCARD=0 keeps every worker's memory; SCR_SA_DISCARD=poison makes
the discarded pages inaccessible instead, for tests.
2026-10-18 05:15:17 -05:00
Chris Tate a3feec9b35 Add ThreadSlots, per-thread values keyed by objects, to @scriptc/experimental
ThreadSlots<K, V> holds one value per key and thread, like a WeakMap that
each thread has its own copy of; under Node.js it is a WeakMap per name
and thread. Setting a value never writes the key, so values can be set on
published keys.

Classes used as keys carry a 32-bit slot word after their header words,
assigned on the first set for a key (with a compare-and-swap for a
published key) and never reused. Each family has one block per thread
that maps slot indices to values through pages of 1024 entries. get and
has are inline reads of the slot word, the page and the entry; set and
delete call scr_slots.c, which holds one reference to each value. A key
class's free paths release the key's values, and thread exit releases the
rest.
2026-10-18 04:11:02 -05:00
Chris Tate 3fd1d2bde6 Revert "Return a worker's memory when it exits without sharing any of it (#1258)"
This reverts commit 2385fc4fff.
2026-10-18 03:37:07 -05:00
Chris Tate add07dba3d Return a worker's memory when it exits without sharing any of it
On Linux, a worker thread that exits without having made any object it
allocated reachable from another thread gives back all of its memory: the
pages it carved, including ProgramLifetime instances and other objects it
never freed, and the system blocks it still holds.

A worker carves past each slice's frontier while the arena's free lists and
returned regions are set aside, takes from those free lists only once a
slice has no bump room left, and records the system blocks it allocates.
Messages and intern tables that share published graphs by pointer, and the
process-wide key arrays of typed records, mark the thread as sharing; so
does a remote free of one of its blocks. When a worker that did not share
parks, the trim helper returns its pages, frees its system blocks, and
restores the arena to its state at the claim, with the free blocks the
worker took free again. A worker that shared keeps its memory as before.

SCR_SA_DISCARD=0 keeps every worker's memory; SCR_SA_DISCARD=poison makes
the discarded pages inaccessible instead, for tests.
2026-10-18 00:46:59 -05:00
Chris Tate 370bf5962d Add the cstring class to library profiles
Library exports and callbacks passed text only as a pointer and length.
profile_format 2 adds the cstring class, a NUL-terminated `const char *`
(an i32 pointer in Wasm reactors), for export parameters, export results
and callback parameters. Parameters are copied in and decoded as UTF-8,
with U+FFFD for malformed bytes; a NULL pointer is the SC4012
host-contract trap. Results live in the result arena like string
results. A result or callback argument containing U+0000 traps instead
of being truncated.

Closes #615
Co-authored-by: ailuoku6 <23209876+ailuoku6@users.noreply.github.com>
2026-10-17 23:40:06 -05:00
Malte UblandClaude Opus 5.5 e79380ce17 Name the runtime package in npm refusals and add transitive --npm-static
- A package typed only by an @types twin was judged under the twin's name
  and refused with "its import resolves to '@types/x/index.d.ts', not to
  runtime JavaScript". Selection now resolves the runtime package, and the
  refusal names it with the --npm-static option that compiles it anyway.
- Refusals the declaration and shipped-JS heuristics make carry the same
  remedy as a diagnostic hint; a dependency of a named package suggests
  the transitive form.
- `--npm-static=pkg...` (and `"pkg..."` in a library profile) selects the
  package and every package its files import, nested require() calls
  included, without the eligibility heuristics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:39:21 -07:00
Malte UblandClaude Opus 5.5 d1639298ff Erase only import type edges, following Node's type stripping
Node runs TypeScript with verbatimModuleSyntax semantics: `import type` and
`export type … from` vanish, while `import { type A }` and
`export { type A } from` keep an empty clause and still load their module.

- Package selection skipped nothing: `import type { X } from "type-fest"`
  selected type-fest and failed with SC2013, and type imports pulled whole
  packages into the compile. Erased edges no longer select packages.
- Files reached only through erased edges are no longer program modules,
  so their imports (a `node:vm` import, say) no longer fence the build.
- Inline type-only clauses are real module edges again (their modules'
  side effects ran under Node but not in the binary); a cycle closed by
  one binds nothing and is admitted like a side-effect import.
- Interfaces, type aliases and type literals from an installed package's
  declarations map structurally, so values the program builds with a
  type-only package's types compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:39:16 -07:00
Chris Tate dfd19cbea6 Give socket 'data' listeners strings after setEncoding
After socket.setEncoding("utf8"), Node passes strings to 'data'
listeners, while @types/node types the chunk as a Buffer. An inline
listener whose chunk is typed Buffer (annotated or inferred) received a
Buffer. When a program can call setEncoding on a socket, such listeners
now take the chunk as a checked dynamic value, so console.log, typeof,
string methods and concatenation behave as in Node, and Buffer-only
methods throw Node's TypeError. Programs without setEncoding on a socket
compile to the same IR as before.
2026-10-17 20:11:58 -05:00
Chris Tate 42906482b6 Compile JavaScript classes like TypeScript classes when their layout or JSDoc types allow it
Fixes #934
Co-authored-by: Alan Leal <33952+alantriesagain@users.noreply.github.com>
2026-10-17 18:44:30 -05:00
Chris Tate 8fbc1d9b77 Document the wasm32-wasi runtime pack and fix the SC3003 hint
The WebAssembly and platform docs now say that wasm32-wasi builds need
zig and the @scriptc/runtime-wasm32-wasi package where the target is
introduced. A missing cross-target runtime pack still reports SC3003
with the package name and version; its hint now says that scriptc
includes the runtime pack for its own platform only, instead of
suggesting a reinstall with optional dependencies.

Fixes #717
Co-authored-by: guest271314 <4174848+guest271314@users.noreply.github.com>
2026-10-17 16:39:20 -05:00
Chris Tate 4018a1d957 Compile async functions that suspend only at their awaits to stackless coroutines (#1204) 2026-10-17 15:38:36 -05:00
Malte UblandClaude Opus 5.5 24c54fc5d3 Add Int32[] element contracts
`Int32` from @scriptc/types now also annotates the element type of a
declared number array (`Int32[]`, `readonly Int32[]`, `Array<Int32>`,
`ReadonlyArray<Int32>`) on parameters, variables, class instance fields
and function results. The contract promises that every element stored
into the array, through any reference, is an int32 by construction.

contracts/int32-elements.ts checks it over a whole-program flow graph of
the places array references live (locals, parameters, globals, results,
class field families, record fields, container element slots), with one
node per allocation and per opaque origin. Allocations that can reach an
annotated slot are constrained and every element store into an array that
may be constrained must be range-proven an int32 (SC7002); element copies
constrain their sources by fixpoint; arrays from or escaping to code the
IR does not show refuse the contract (SC7003). Slots only constrained
arrays reach are recorded as IrContracts.int32Elements.

Element reads that find a value through those slots carry the int32
range, `getNumber` reads are integer-or-NaN operands of wrapping sums, and
ToInt32 of any integer-or-NaN operand is branch-free. Holes, missing
indices and length changes keep their JavaScript behavior.

Integer kernels over a 2^20-element array: `(s + a[i]) | 0` 1086 -> 175 ms,
`(Math.imul(h, 31) + a[i]) | 0` 602 -> 150 ms, prefix sums 575 -> 155 ms;
programs without the annotation compile to identical binaries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:08:06 -07:00
Malte UblandClaude Opus 5.5 74a9cfb8cc Compile async functions that suspend only at their awaits to stackless coroutines
Native async bodies whose only suspension points are their own awaits now
compile to LLVM switched coroutines, the lowering WASI async bodies and
native synchronous generators already use. A suspended call keeps a heap
frame of the values live across its awaits instead of a fiber with its own
stack. Bodies that reach other suspension points (async generator
consumption, dyn awaits, node:test subtests), module evaluation bodies,
dyn results, worker and library programs, and Windows keep their fibers.

The emitter selects bodies by emitting them as usual first: that text may
suspend only through await runtime calls and await-call entries, and may
call no function that parks. The coroutine form is then emitted and
checked again. Each await prepares its continuation in the runtime (a
pending promise's waiter list, or one READY-queue turn, continuing in place
exactly where a fiber's hop would) and extracts the settled result after
resuming. The eager prefix runs on the spawner's stack and moves to a fresh
stack near the stack guard, so deep async recursion keeps working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:29:05 -07:00
Malte UblandClaude Opus 5.5 3fcef83d38 Carry lifetime regions across await and reuse freed region chunks
An async function called inside LifetimeRegion.run now keeps its region
after every await: its fiber records the region that was current at spawn,
and the event loop makes that region current while it resumes the fiber.
A region is not freed (or attempted) while a fiber that carries it exists,
so a region released while its async work is pending keeps taking that
work's objects and is freed once the work ends. Before, objects created
after an await went to the default region, which is never freed, and kept
the request's region allocated for good, so per-request regions in async
servers grew without bound.

Freed region chunks now go to a per-thread cache (at most 8 MiB resident,
the first 256 KiB of each kept cleared, fresh pages past it, no huge-page
chunks) instead of being unmapped, so a region per request or per batch no
longer maps and faults in new memory every time.

Adds the requests.ts region benchmark (concurrent async handlers), corpus
5023 and its region-stats checks, and a chunk-reuse runtime unit check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:26:12 -07:00
Chris Tate 05cb9bd609 Add the concurrency test option
node:test changes, matching Node 24.22:

- { concurrency } on describe, suite, test, it and t.test sets how many
  subtests run at once: a number, true for os.availableParallelism() - 1,
  or false for 1. Subtests inherit the parent's value.
- Above 1, each subtest runs on its own fiber. Subtests start in
  declaration order as slots free up, and t.test and test() calls inside a
  concurrent test start the subtest at once; t.test's promise settles when
  it finishes. The parent finishes after all of them.
- Results, diagnostics and failures are reported in declaration order.
- A concurrent subtest waits after its beforeEach hooks, and a concurrent
  suite before its children, for as many turns as Node's runner does, so
  sibling bodies interleave as they do there.
- A concurrency that is not an integer from 1 to 4294967295 throws Node's
  ERR_OUT_OF_RANGE RangeError when the test or suite is created.
- Nested test() calls and hooks inside a concurrent subtest attach to the
  subtest that runs on the calling fiber.

The concurrency fixture compares the reporter output, the order of the
bodies' side effects, and the range errors with Node, plain and with
SCRIPTC_SAN=1.
2026-10-17 03:28:15 -05:00
Chris Tate 2896e94b20 Merge main 2026-10-17 02:59:21 -05:00
Chris Tate db94b2d257 Add fs.cpSync, fs.cp and fs/promises cp
fs.cpSync, fs.cp and fs/promises cp compile with every option:
dereference, errorOnExist, filter, force, mode, preserveTimestamps,
recursive and verbatimSymlinks. They follow Node's two implementations.
cpSync follows cp-sync.js over the fs binding: copy errors are plain
Errors with a code, std::error_code failures throw an ErrnoException,
and links inside a copied directory point at their canonical targets.
fs.cp and fs/promises cp follow cp.js, whose copy errors are
SystemErrors with code, info, errno, syscall and path.

Options and paths validate in Node's order with Node's errors. filter
runs for the top-level paths and each entry in Node's order; an async
filter must return a promise that is already settled, and a pending one
throws SC2020. The callback and promise forms complete the copy before
the callback runs or the promise settles. Windows builds throw SC2020.

The copy lives in the new scr_fs_cp.c. Directory walks keep their frames
on the heap, so deep trees also copy on a fiber's stack. File contents
copy with sendfile on Linux, and COPYFILE_FICLONE uses FICLONE
where it exists. ScrError gains the SystemError context and the
ErrnoException key order, with matching fields in the compiler's Error
layout. The stored fs.cp callback uses the same implementation.

Corpus 5567-5571 cover the sync, callback and promise forms, errors,
symlinks and 1500-level trees.
2026-10-16 21:06:22 -05:00
Chris Tate e2c34f381b Add the plan and timeout test options 2026-10-16 20:55:26 -05:00
Chris Tate d9229b044f Inline small runtime functions into release executables (#1130) 2026-10-16 19:16:56 -05:00
Malte Ubl f4ce6910c7 Inline small runtime functions into release executables
Release executables now import small runtime functions from runtime
bitcode, as speed executables do, so LLVM can inline them into program
code. Release units keep the runtime compiler's own objects, unchanged,
and additionally ship the bitcode of the same compilation reduced by the
helper's prune-import. Their unit-local symbols stay local, so only
bodies that need none of them can be imported, which keeps the import
small: about 1.3 MB per program.

Only unsanitized programs small enough for one codegen partition import.
Every partition reads the bitcode again; on the Effect fixtures (64
partitions) import cost +2..18% compile time and +4.3..4.9% size, so
partitioned release builds stay exactly as before. Speed is unchanged.

Measured against main in Linux Vercel Sandboxes, same-VM interleaved
medians (bench-runtime --layouts=4 --runs=16), on two VMs:

- bench-runtime (31 workloads): -1.3% (8 faster, 0 slower) and -1.0%
  (1 slower, typed-array-scan-workers +3.2%); size +0.0%
- js-compiled suite (22 programs): -2.2% and -1.8%; size -0.1%
- cold builds of all 53 programs, interleaved: +3.1%; helper time per
  build +40..235 ms
- Effect fixtures: byte-identical to main
2026-10-16 14:03:52 -07: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 241006cff3 Add fs read and write streams (#1089) 2026-10-16 09:02:48 -05:00
Chris Tate 21c2340ef6 Support the dgram socket options and disconnect() (#1122) 2026-10-16 08:39:23 -05:00
Chris Tate a5da46267b Merge main 2026-10-16 08:31:20 -05:00
Chris Tate a09990192e Add tlsSocket.getProtocol, getCipher, alpnProtocol and tls.DEFAULT_MIN_VERSION/MAX_VERSION (#1054) 2026-10-16 08:23:42 -05:00
Chris Tate 0cad2fe551 Name the TLS ciphers limitations partial after its heading
The partial for the "TLS ciphers" section was tls-cipher.mdx, while the
limitations tooling names a section's partial after its heading's slug
(tls-ciphers). It is renamed, so a join and split of the page leaves the
docs unchanged.
2026-10-16 07:48:05 -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 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 d28ec3e31e Merge main 2026-10-16 04:34:48 -05:00
Chris Tate 18e69b81c5 Merge main 2026-10-16 04:30:20 -05:00
Chris Tate d3dda8271d Leave the main thread's heap to the operating system at exit
When the main thread's script ends, the executable released every module
global and ran a full cycle collection before exiting, which takes time
proportional to everything the program still holds (about 15 ms for
1.5 million live objects) and frees memory the operating system reclaims
anyway. The main thread now skips those releases, the exit-time
collection and the collector's buffer cleanup. Exit listeners and output
flushing run as before. Worker threads still free their heaps when they
exit, since later threads reuse their arenas, and builds that check for
leaks (the RC audit, AddressSanitizer) free everything as before, as does
any process run with SCRIPTC_EXIT_FREE=1 for external leak checkers.

SCRIPTC_WORKER_STATS=1 reports, as each worker thread exits, how long it
took to start, to run its script and to exit.
2026-10-16 01:58:45 -05:00