Scope-B milestone NI-3. When a program loads Node in-process, its pending
ticks, promise jobs, timers and immediates move onto Node's queues in
order (timers keep their deadlines relative to the moment Node began
loading; the work of a phase the switch interrupted finishes first) and
every later one is scheduled there, so the jobs of both sides interleave
as in Node. The runtime's I/O hooks are watched from Node's loop, and its
liveness is re-read before each poll phase, after each check phase and
after every microtask checkpoint. Node's loop runs on Node's stack, and
V8's limit follows whichever stack an entry comes from.
Island signal and exit listeners join the runtime's registries at their
position, so the runtime keeps owning every signal disposition; Node's own
'exit' emission is left to the runtime's exit sequence, and the exit code
island code leaves in process.exitCode becomes the program's. Bridge ABI 3.
Ordering programs (tests/corpus-node-islands-order, harness
node-island-order.test.ts), corpus programs 6804-6811 and the
node-island-process harness pin the behavior against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A --libnode=beside build whose executable comes from the executable
cache places the pack next to it again (compile, index.ts): the pack
is an output of the build, and a rebuild after the outputs were
deleted restored the executable without it.
- The cache key names the libnode pack by the option, the pack
directory and a digest of its manifest.json (libnodeCacheKey), so a
pack upgraded in place gives cached executables a new key instead of
their old recorded Node version.
- nodeIsland.callSync marks the program's event loop live, as
nodeIsland.call does: the called function may leave Node with ref'd
timers or I/O, which the loop drives before the program exits.
- scr_runtime.h includes scr_node_island.h, so the LLVM backend's
declarations of the Node island entry points have prototypes there.
- Corpus costs for 6800-6803, from CI's differential and
llvm-differential timings of the programs.
Scope-B milestone NI-1, API side: the explicit call API that replaces the
build container's hand-written node subprocess (api patch 09).
- The new package @scriptc/node exports callSync(specifier, name, ...args)
and call(...). The module loads with require() from the calling module;
arguments and results cross as JSON copies; a thrown Error crosses as its
nearest intrinsic class with its message and string code, other thrown
values as JSON copies; callSync refuses a function that returns a
promise. Its Node implementation runs the calls in-process with the same
copies, so a program prints the same under Node and compiled.
- The compiler lowers calls to the named or namespace imports
(node-api-import.ts, lower-node-island.ts) to the nodeIsland.callSync
and nodeIsland.call libCalls: the calling module's real path, the
specifier, the export name and the boxed argument array, with the result
checked against the declared type like `JSON.parse(s) as T`. Arguments
and results whose types are not JSON data are refused at build time, and
the functions and the namespace refuse as values.
- Corpus programs 6800-6803 (sync and async results, errors, process.exit
from Node with the program's exit listeners, Node's pending timers
keeping the program alive) run under Node and as binaries that load
libnode; `// @requires-libnode` skips them where SCRIPTC_LIBNODE names no
pack (until the pack CI matrix publishes packs). The node-island harness
covers loading Node only on the first call, the beside packaging, a
missing pack, a pack of another Node version or bridge ABI, and the
executable's symbols.
- Release tooling publishes @scriptc/node with the other packages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calls into Node now run on one dedicated 64 MB stack with a guard page,
reached by a context switch, and the bridge sets V8's stack limit on it
(bridge ABI 2). Before, a call from an async fiber (512 KB stacks without a
guard page) failed to bootstrap Node, which the dev-mode LLVM differential
showed. The runtime's stack guard follows the switch, and the switch
carries ASan's fiber annotations.
The Node island tests load Node from a libnode pack. Until the NI-13 packs
are published, .github/actions/libnode-dev-pack assembles one for the
Linux x64 test lanes (plain and sanitized): a pinned, checksum-verified
prebuilt libnode of the repository's Node release (24.22.0) and Node's
official headers (packages/libnode-common/dev-pack.json), with the bridge
built against them by build-pack.mjs. The pack is cached by its inputs and
the bridge source, so only a cache miss downloads and builds; the normal
build path never does.
tests/harness/libnode.ts makes a missing pack a failure in those lanes:
island tests skip only off Linux x64 CI without a configured pack. The
bridge smoke test (packages/libnode-common/test/bridge-smoke.c) loads the
pack the way scr_node_island.c does and makes synchronous, throwing and
asynchronous calls through the bridge with the runtime's bootstrap.
IR, LLVM and object outputs of programs that call into Node no longer need
a pack at build time; only executables record one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An event that typed code only listened to with a listener taking no
parameters had no bridge entry: the scan marked a listener as typed only
when one of its parameters was not a checked-dynamic value. Emitting the
event through an `any` reference then threw instead of calling the
listener. A TypeScript listener with no parameters now marks the event as
typed. A JavaScript function without parameters still registers as before,
since it can read `arguments`.
Scope-B milestone NI-1, runtime side. Programs whose IR has a
`nodeIsland.*` operation link scr_node_island.c and the SCR_NODE_ISLANDS
build of scr_async.c; every other program links neither, byte for byte.
- scr_node_island.c dlopens the bridge of a libnode pack on the first call
(RTLD_NOW | RTLD_GLOBAL), checks its bridge ABI and that it links exactly
the Node version recorded at build time, starts one Node environment and
runs the embedded bootstrap (scr_node_island_host.cjs). A run that never
calls into Node never maps libnode. The pack directory is SCRIPTC_LIBNODE
at run time, else the recorded path; a `$ORIGIN/` path resolves against
the executable's directory (the beside packaging, and the hook for
shipping Node with the program).
- scr_node_bridge.h is the C ABI between the runtime and the bridge; the
runtime includes no Node or V8 header. The bridge
(packages/libnode-common/bridge/scr_node_bridge.cc) initializes Node once
with no default signal handling, no debug-signal handler or inspector,
no stdio reinitialization and one platform worker, creates the
environment from the built-in snapshot, routes Node's process exit to
the runtime's exit sequence, promotes libnode to the global scope for
native addons, and runs calls in a CallbackScope with a TryCatch.
build-pack.mjs assembles a pack (libnode, the bridge, manifest.json) from
a Node --shared build and the matching headers.
- The SCR_NODE_ISLANDS build of scr_async.c adds a loop slot through which
the runtime's loop drives Node's until NI-3 hosts the runtime's queues on
Node's libuv: it keeps the program alive while Node has ref'd work,
sleeps on Node's backend fd up to its next timer, and runs one
non-blocking turn per loop turn.
- The `nodeIsland` runtime feature (rule, link features, unit table, pack
matrix row and scr_async.c variant), the nodeIsland.call/callSync
libCalls, the install call in main, and the build-time record of the
pack (node-islands/libnode.ts: --libnode=<pack>|beside or
SCRIPTC_LIBNODE, SC8005 when none is usable, Node >=24.13.1).
- The zero-cost gate and the bench nm test require that no good-code
executable defines a Node island symbol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first Object.freeze, Object.seal or Object.preventExtensions of a typed
function creates its property table from the function's dynamic box. The
box had no name, so later `f.name` reads answered "". The box now carries
the function's JS name, as a closure flowing into any checked-dynamic slot
does.
The runtime's own callers (published values, os and node constants, the
private-range block list, Wasm exports) freeze data they built, and now
call scr_dyn_freeze_data, the freeze for values that are not functions.
Object.isFrozen, Object.isSealed and Object.isExtensible read a
function's existing table and answer false, false and true when it has
none. Fully typed programs therefore link no dynamic-tier object code
for these calls again.
tsc wraps every CommonJS import in its helpers: `const events_1 =
__importDefault(require("events"))`, `const path =
__importStar(require("path"))`. cjs-tsc-interop.ts reads such bindings of
program modules as plain require bindings, so the helper declarations go
dead, but a builtin module is not a program module: its binding kept the
helpers live, and their probe of the module's top-level `this`
(`(this && this.__importDefault) ||`) failed at load ("property access
through top-level 'this'"). That is the shape of hundreds of tsc-compiled
files in a typical dependency tree.
A builtin carries no `__esModule`, so `events_1.default` is the module and
the namespace copy reads the module's members. When every reference has
that form, a package-file stage now respells the binding as the plain
require() of the builtin and `x.default` as `x`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Revert 3a27a92cb ("Forward at most four emit arguments through
EventEmitter method values") and 0cf62ebec ("Answer EventEmitter methods
read as values off compiled emitters"). Under the scope-B plan,
EventEmitter methods read as values, `EventEmitter.prototype.on` in
JavaScript and constructor-function emitters belong to Node islands, not
the native compiler. The typed/untyped listener sharing and the
catchable throw in place of the trap stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A union, unknown or any value is converted to a dynamic value before it
is thrown, and promise rejections and other runtime throws carry dynamic
values too. A catch binding then holds a dynamic payload, which the
native class test does not recognize: `e instanceof C` was false for
every class of a hierarchy, and a read narrowed by it trapped. A test
through a parameter of type unknown was already correct.
When a class or one of its subclasses crosses into unknown, the class
dynamic-dispatch pass now routes the catch-binding tests of that class
(`e instanceof C`, the reads it narrows and `e as C`) through helpers
that keep the native test and answer a dynamic payload with the dynamic
class test, as standalone classes already do. The new "dynamic" caught
test selects that payload, so native payloads are not converted.
Programs whose classes never cross into unknown compile as before.
fs.promises.rm(path, options) accepts the { recursive, force, maxRetries,
retryDelay } literal options that rmSync accepts. It runs the same removal
and rejects with its errors.
Removing a directory without recursive now throws Node's ERR_FS_EISDIR
SystemError from rmSync and fs.promises.rm. A retryDelay outside int32 >= 0
or a maxRetries outside uint32 throws ERR_OUT_OF_RANGE. rmSync checks these
options before the path, and fs.promises.rm checks the path first, as Node
does.
Fixes#1615
Co-authored-by: jlucaso1 <55464917+jlucaso1@users.noreply.github.com>
A generic arrow or method held by an object literal is also lowered with
its type parameters erased, for the literal's own value. In that lowering
the object it returns builds as a checked-dynamic value, so a callback
member with an optional or defaulted parameter, such as
`decode(value, at = [])`, met a slot type that maps to a dynamic value.
The completed-signature check refused it with SC1090, although the same
code in a standalone generic function compiled.
A slot whose type maps to a dynamic value now takes the function the same
way an 'any' slot does: the value boxes into a checked-dynamic function,
which completes omitted arguments with undefined.
Fixes#1373
Co-authored-by: blurname <50226652+blurname@users.noreply.github.com>
TypeScript accepts parentheses and non-null assertions around the
reference an assignment, a `++`/`--` update or a for-in/of head writes
(`(x) = 1`, `h.sums[k]! += 10`), and both are erased at run time. The
compiler now reads its source files with the AstFile writeTargets option:
the writer's `left`, `operand` or `initializer` is the reference itself
and the reference's `parent` is the writer, so lowering and every analysis
of a binding's or member's writes see one shape. The reference keeps its
own source span. Files read without the option, such as type nodes from
the checker and the AST conformance tests, keep TypeScript's exact tree.
Fixes#1625
Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>
A WebAssembly module the build finds but does not translate (one that uses
a proposal outside the translated feature set, an invalid one, or one the
translator rejects) now rejects with a CompileError that says the module
was found and names the reason, for example "it uses simd, outside the
translated WebAssembly feature set". Bytes the build did not see keep the
existing message.
The build emits the hash, length and reason of each such module in a
second table, scr_wasm_untranslated, next to scr_wasm_registry; the runtime
reads it when a lookup misses. WebAssembly.validate still answers false for
these modules, and the error stays a catchable CompileError.
Fixes#1617
Co-authored-by: jlucaso1 <55464917+jlucaso1@users.noreply.github.com>
TypedArray.of(...items) on the numeric typed-array constructors creates a
new array of its arguments. Each argument converts with ToNumber and then
to the element type, in argument order after all arguments are
evaluated, as in Node. Arguments that are all numbers copy directly;
other values and spreads collect into an array that goes through
TypedArray.from's conversion.
Spreading a dynamic iterable into a call's items (Array.of, push,
TypedArray.of) now drains it through the iterator protocol with the
spread call's TypeErrors, and a typed iterable spread into dynamic items
boxes and drains the same way. Before, both reached a run-time SC2020.
Fixes#1440
Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com>
A plain function's own properties (name, length, prototype, and the ones a
program assigns, as in `hash.outputLen = 32`) live in its property table.
Object.freeze, Object.seal and Object.preventExtensions of a function now
act on that table and answer the function, and Object.isFrozen,
Object.isSealed and Object.isExtensible answer from it, as in Node:
- Object.freeze of a function was refused as a possibly-aliased value,
and Object.isFrozen of one threw SC2020;
- Object.isExtensible answered false and Object.isSealed true for every
function.
Adding a property to a frozen or non-extensible function throws Node's
TypeError; assigning a read-only one throws the runtime's existing
read-only TypeError. Class constructors keep their fences. This lets
@noble/hashes' createHasher return its frozen hasher, so the sha256
program in the issue prints the digest Node prints.
Fixes#1437
Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com>
A comparator that chains key differences with `||`, such as
`(b.score ?? 0) - (a.score ?? 0) || a.id - b.id`, ran through the
comparator sort: TimSort called the closure once per comparison. Each
term orders one number field, ascending or descending, and a term that is
0 or NaN falls through to the next, so the chain orders elements by their
keys with the first deciding. With no key NaN or -0 that order is
consistent, and a stable sort by the keys gives the comparator sort's
permutation.
The recognizer now accepts such chains of up to eight terms over number
fields of records and class instances, and a key may read an optional
number field with a number default (`x.k ?? 0`), also as a single key. The
runtime reads and checks every key of every element before it moves a
slot, then runs stable keyed passes from the last key to the first. A NaN
or -0 key, or a chain with any other term, keeps the comparator sort.
`a instanceof Uint8Array || (... a.BYTES_PER_ELEMENT === 1)`, the
@noble/hashes isBytes guard, narrows `a` to never on the right side when
the guard is specialized for a Uint8Array argument, so the read missed
the typed-array member lowering; it now answers the element size from
the receiver's storage. The checked-dynamic read of BYTES_PER_ELEMENT on
a typed array or Buffer (an untyped argument) answered undefined; it now
answers the element size, as each kind's prototype does in Node.
Refs #1437
Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com>
A benchmark workload or the self-hosted compiler that fails to build
with both checkouts now fails the gate, and so does a run in which no
program built with both. Before this, a run whose builds all failed the
same way, such as with a helper of another version, reported PASS. A
build error now prints in full under its program; its first line is
often a warning.
An element write whose index the checker types as a number but whose
storage may hold undefined (a for...of variable over an array that may
have holes) converted the index with ToNumber only on plain-array writes.
Typed-array writes, compound and update writes, element assignments used
as values and element targets in destructuring assignment now convert it
the same way, as the element reads already do, instead of refusing it as
a non-number key.
Fixes#1622
Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>
forEach, map, filter, some, every, find, findIndex, findLast,
findLastIndex, reduce and reduceRight compile on every typed-array kind
through one shared helper per method shape. The receiver, the callback and
reduce's initial value evaluate in order before the helper runs; the
helper reads the length once and each element when it visits it, so the
callback's writes show in later elements. Callbacks receive the element,
its index and the typed array (reduce's receive the accumulator first).
map and filter return a new typed array of the receiver's kind, map
storing each result with that kind's element conversion, and a Buffer's
map and filter return a Buffer. reduce without an initial value throws
Node's TypeError on an empty typed array. A thisArg keeps the fence, as
for arrays.
Fixes#1620
Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>
Each call of a function makes a new function object for a generic arrow
or function expression bound inside it, so `make() !== make()` when make
returns the binding, and such values are distinct Map and Set keys. The
compiled instances are shared module functions, which cannot give that
identity, so a value use of such a binding keeps an SC1090 fence that
names the reason. Calls of the binding still compile.
An unannotated `const xs = []` (or `let`) inside a function, which
TypeScript types by the elements written before each read, takes the array
type its settled reads have: records pushed in a loop give a record array,
numbers a number[], strings a string[]. Reads inside closures, which the
checker leaves unsettled, do not count. When settled reads disagree or the
binding is reassigned, the binding is a dynamic array, as the same binding
already is at module scope. Before, such a binding was always number[] and
pushes of other values were refused.
Fixes#1623
Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>