Commit Graph
2627 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 eaefbdf44f Host the runtime's event loop on Node's libuv once Node loads (NI-3)
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>
2026-10-21 13:31:51 -07:00
Chris Tate 8cc2594d6e Place a beside libnode pack on cache hits, key the cache by the pack's manifest, keep the loop for synchronous Node calls, and record the Node call programs' costs
- 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.
2026-10-21 12:45:05 -05:00
Chris Tate 6d19198eed Merge main 2026-10-21 12:43:37 -05:00
Chris Tate a437102af6 Keep object literals' key order through IR rewrites and on shapes typed JSON.parse also builds (#1607) 2026-10-21 12:28:31 -05:00
Chris Tate 33110f1938 Track unions in nonJsonPart and expect per-platform lifecycle output 2026-10-21 12:08:57 -05:00
Malte UblandClaude Opus 5.5 51e9812493 Call into Node.js from compiled programs with @scriptc/node
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>
2026-10-21 09:59:55 -07:00
Malte UblandClaude Opus 5.5 1081315a49 Run Node on its own stack; provide a libnode pack to the Linux CI test lanes
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>
2026-10-21 09:58:32 -07:00
Chris Tate b45b8e0898 Call an object's own or inherited toString in template literals and join (#1655) 2026-10-21 11:50:36 -05:00
Chris Tate 04435a70ac Address the second round of review comments on prototype toString conversions 2026-10-21 11:44:13 -05:00
Chris Tate 86aee1e81a Support TypedArray.of (#1658) 2026-10-21 11:42:08 -05:00
Chris Tate 5484c1254b Answer catch-binding instanceof for values thrown as unknown (#1674) 2026-10-21 11:27:19 -05:00
Chris Tate cdcf69c728 Give ProgramLifetime class instances a 32-bit header and packed fields (#1651) 2026-10-21 11:21:22 -05:00
Chris Tate 9a504b6f0d Share one listener list between typed and untyped uses of an EventEmitter (#1563) 2026-10-21 11:18:16 -05:00
Chris Tate b104643a95 Accept defaulted callbacks in literals under an erased generic member (#1666) 2026-10-21 11:15:39 -05:00
Chris Tate e34e5edfd5 Run Buffer methods on Buffers held as checked-dynamic values (#1659) 2026-10-21 11:15:06 -05:00
Chris Tate c8193b9486 Unwrap tsc's interop helpers around builtin modules in packages (#1683) 2026-10-21 11:14:39 -05:00
Chris Tate f2f0617c79 Support fs.promises.rm options (#1673) 2026-10-21 11:08:38 -05:00
Chris Tate cd38fd2248 Bridge events whose typed listeners take no parameters
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`.
2026-10-21 11:03:19 -05:00
Chris Tate aea358eed5 Freeze, seal and query functions through their property tables (#1657) 2026-10-21 11:01:40 -05:00
Chris Tate 4312b78ab5 Name the reason when a discovered WebAssembly module is not translated (#1660) 2026-10-21 10:59:44 -05:00
Malte UblandClaude Opus 5.5 9a0a3f1562 Load Node.js in-process on first use: libnode bridge and runtime loader
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>
2026-10-21 08:54:47 -07:00
Chris Tate 858328703b Name typed functions' property tables and keep runtime freezes data-only
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.
2026-10-21 10:34:21 -05:00
Malte UblandClaude Opus 5.5 f793009ee5 Unwrap tsc's interop helpers around builtin modules in packages
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>
2026-10-21 08:11:47 -07:00
Chris Tate 94f040b3df Address review comments on Buffer methods for checked-dynamic values 2026-10-21 09:57:56 -05:00
Malte Ubl d3d39af3fe Merge remote-tracking branch 'origin/main' into opt/bc-satisfies-key-order
# Conflicts:
#	packages/compiler/src/ir/traverse.ts
2026-10-21 07:57:00 -07:00
Malte Ubl f798f6736c Merge remote-tracking branch 'origin/main' into opt/emitter-mixed-events 2026-10-21 07:53:10 -07:00
Malte UblandClaude Opus 5.5 3f3e90b846 Drop the EventEmitter method-values follow-up
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>
2026-10-21 07:53:10 -07:00
Chris Tate 77aab9c0fd Address review comments on prototype toString conversions 2026-10-21 09:41:55 -05:00
Chris Tate 5a56cc2564 Answer catch-binding instanceof for values thrown as unknown
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.
2026-10-21 09:40:20 -05:00
Chris Tate d78bd871f6 Clear recycled blocks for compact instances and keep TypeScript 7 parity 2026-10-21 09:16:06 -05:00
Chris Tateandjlucaso1 973884eabe Support fs.promises.rm options
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>
2026-10-21 09:13:12 -05:00
Chris Tateandblurname a7faf91515 Accept defaulted callbacks in literals under an erased generic member
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>
2026-10-21 08:59:12 -05:00
Chris Tateandejc3 e41858c7df Compile assignment and update targets wrapped in parentheses or non-null assertions
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>
2026-10-21 08:58:51 -05:00
Chris Tateandjlucaso1 f168402e1f Name the reason in the CompileError for untranslated WebAssembly modules
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>
2026-10-21 08:52:18 -05:00
Chris Tate f2fd1c85a2 Run Buffer methods on Buffers held as checked-dynamic values 2026-10-21 08:50:54 -05:00
Chris Tateandsaboursepideh e50454caf3 Support TypedArray.of
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>
2026-10-21 08:48:03 -05:00
Chris Tateandsaboursepideh 726c2ce3d7 Freeze, seal and query functions through their property tables
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>
2026-10-21 08:47:37 -05:00
Chris Tate 13790b88c7 Call an object's own or inherited toString in template literals and join 2026-10-21 08:44:31 -05:00
Chris Tate f58c321c07 Sort by several keys without calling a chained comparator
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.
2026-10-21 08:31:35 -05:00
Chris Tate c1bb553964 Give ProgramLifetime class instances a 32-bit header and packed fields 2026-10-21 08:30:09 -05:00
Chris Tate 1ee5c84cad Read BYTES_PER_ELEMENT on narrowed-away and untyped typed arrays (#1639) 2026-10-21 04:24:36 -05:00
Chris Tate 8b4e74d050 Type empty arrays filled by later writes inside functions by their reads (#1632) 2026-10-21 04:23:15 -05:00
Chris Tateandsaboursepideh 916303beff Read BYTES_PER_ELEMENT on narrowed-away and untyped typed arrays
`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>
2026-10-21 04:21:00 -05:00
Chris Tate cb75c51eda Fail the zero-cost gate when it compares nothing
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.
2026-10-21 03:45:36 -05:00
Chris Tateandejc3 b237cf1ff0 Convert possibly undefined indexes of element writes with ToNumber
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>
2026-10-21 03:04:59 -05:00
Chris Tate 1f554fbc75 Compile the typed-array callback methods (#1633) 2026-10-21 03:01:01 -05:00
Chris Tateandejc3 2078392a52 Compile the typed-array callback methods
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>
2026-10-21 02:55:08 -05:00
Chris Tate 095acffb46 Compile generic arrow bindings inside functions that use no enclosing frame (#1627) 2026-10-21 02:54:32 -05:00
Chris Tate 3e08632467 Refuse values of generic bindings declared inside functions
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.
2026-10-21 02:51:36 -05:00
Chris Tateandejc3 009a01d074 Type empty arrays filled by later writes inside functions by their reads
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>
2026-10-21 02:40:20 -05:00