Commit Graph
3145 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 8c8ee28413 Compile bodies with thousands of capture boxes unoptimized
Every box pointer of a captured binding is a value live from its
binding to its last use. A factory whose methods share thousands of
bindings hands LLVM thousands of such ranges, each crossing every call
in between, in the factory and in each method that receives them, and
the inliner, instruction selection and register allocation grow
quadratically: Bun's large-environment AOT program (8,701 captured
bindings) spent about three minutes in native code generation.

A function holding at least 512 capture boxes (the self-hosted compiler's
largest holds under 100) is now emitted `noinline optnone`, and its box
slots are loaded and stored volatile so no IR pass promotes them, which
the native helper's pipeline would otherwise still do for optnone
bodies. Each use reloads its box pointer from the frame. Boxes,
environments and semantics are unchanged; only those bodies' machine
code is unoptimized.

large-environment.ts now compiles in 16 s instead of 227 s (peak RSS
543 MB instead of 777 MB) and runs in the same 0.18 s. Corpus 5973 pins
such a factory's behavior (shared mutation, per-iteration closures, a
nested closure, a forward-captured read in its TDZ), and a harness test
bounds compile-time growth between 500 and 4,000 bindings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:43:03 -07:00
Malte UblandClaude Opus 5.5 eb0d09fb81 Keep capture analyses linear in the size of a closure environment
Three analyses did work proportional to the whole function once per
captured binding, so a factory with thousands of bindings shared by its
methods spent most of its compile time in them (48 s of LLVM emission
at 8,000 bindings):

- LazyCaptures walked the body again for every capture it checked and
  rebuilt a locals map for every capture of every creation site. Each
  function's rebound bindings are now collected in one walk and the
  unchanged-parameter sets are memoized.
- int32-slots walked the declaring function once per box to find its
  initializer and scanned capture lists per member. Both are now
  per-function sets built on first use.
- CaptureFlow scanned all capture edges, capture lists and entry copies
  per binding and searched statement lists per hoisted or TDZ binding.
  Edges are grouped by source, captures, parameters and entry copies are
  indexed, and hoisted and TDZ initializers are found in one pass.

The results are unchanged: the candidate compiler emits byte-identical
LLVM for the self-hosted compiler's source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:40:45 -07: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
Malte UblandClaude Opus 5.5 611e814200 Doom unreferenced cycle components without a scan walk
A collector pass used to decide once for all its candidates: if any gray
object was still externally referenced after markGray, scan walked every
candidate's subgraph again, including the dead structures. A pass almost
always has one live structure in it (the one under construction when the
trigger fires), so most passes paid a second walk over their garbage.

markGray now walks each candidate that no earlier walk reached as its own
component and keeps the externally referenced count per component, using
the header's buffer word as the component number (gray objects are never
buffered during a pass). A component that ends its walk with no externally
referenced member is garbage: every reference to its members came from
inside it, and no later walk can add one. It is doomed at once, and scan
runs only from the candidates of referenced components. Limited passes,
whose untaken candidates keep their buffer positions, keep the single
whole-pass decision. The freed set of every pass is unchanged.

The fast path also stops re-reading the dead set: doomed entries skip
settle's filtering loop, the level statistics are counted in the teardown
loop that reads the type anyway, and the gray-list push is inline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:30:56 -07:00
Chris Tate f1f7302179 Monomorphize destructured options parameters of package functions (#1243) 2026-10-17 21:13:38 -05:00
Chris Tate aad9b4f140 Merge main 2026-10-17 21:01:15 -05:00
Chris Tate 8d8b96fefa Merge main 2026-10-17 20:59:05 -05:00
Chris Tate a5c7c09ad3 Add mock.timers to node:test 2026-10-17 20:55:17 -05:00
Chris Tate f291f1c275 Monomorphize destructured options parameters of package functions
A package function that destructures its options object, such as
makeClass({ Base, name }), now specializes per call site like positional
parameters: each destructured name binds the static type of the matching
property, so a class passed as { Base } keeps its static type and
class extends Base builds a constructible subclass.

Fixes #938
Co-authored-by: phall <25888173+phall1@users.noreply.github.com>
2026-10-17 20:50:59 -05: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 24df6bb0cd Mark socket and stream chunks as Buffers (#1238) 2026-10-17 20:06:16 -05:00
Chris Tate f87b6d7528 Mark socket and stream chunks as Buffers
Chunks from net sockets (including socket.read()), http messages and
upgrade heads, child_process output, dgram messages, http2 streams and
TLS sessions were byte arrays without the Buffer brand. String
conversion of them joined the byte values ("112,105,110,103") instead
of decoding UTF-8, so `body += chunk` and template literals differed
from Node. They are now Buffers, as in Node.

Closes #903
Co-authored-by: João Carlos Magalhães de Castro <88864312+JohnC0de@users.noreply.github.com>
2026-10-17 19:42:11 -05:00
Chris Tate 6d79399862 Tunnel fetch through env proxies with CONNECT
Fixes #1185
Co-authored-by: Andrey Kiselev <35863331+TheAsda@users.noreply.github.com>
2026-10-17 19:38:19 -05:00
Chris Tate 2b6f15c7fa Answer undefined for missing Record and index-signature keys
A keyed or dot read of an index signature that misses now answers
undefined, as in Node, instead of aborting with "record has no key".
The read carries an explicit optional value through the same analysis
that already widens bindings, parameters, returns and fields for missing
array elements; TypeScript's static types are unchanged. Numeric reads
whose consumers treat undefined like NaN lower to plain doubles, and
nested reads and writes through a missing entry throw Node's TypeError.

Fixes #964
Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com>
2026-10-17 19:35:34 -05:00
Chris Tate b1e22da06d Allow overrides that narrow a union return type (#1235) 2026-10-17 19:34:12 -05:00
Chris Tate 52ddc1e4c6 Call generic members through receivers that are not statically known (#1231) 2026-10-17 19:31:28 -05:00
Chris Tate 2e0f44c8b7 Call valueOf and Symbol.toPrimitive on values held as any (#1234) 2026-10-17 19:24:42 -05:00
Chris Tate 2e7e825e38 Widen ASCII to UTF-16 sixteen bytes per step in Buffer.write (#1233) 2026-10-17 19:21:32 -05:00
Chris Tate 4060ea51d4 Expect generic protocol dispatch to compile
A class behind an interface whose method is generic now dispatches to
the method's erased instance, so the boundary test compiles the program
instead of expecting the old refusal.

Fixes #905
Co-authored-by: Konstantin Mikheev <8560582+konmik@users.noreply.github.com>
2026-10-17 19:11:33 -05:00
Chris Tate 2e7a37c0be Allow overrides that narrow a union return type
An override whose return type is part of the base method's union return
(`Meta` for `Meta | undefined`, `number` for `number | undefined`, or a
subclass of one of the union's class arms) was refused with SC1090,
although TypeScript accepts it. The override now returns through the
base's union ABI, so calls through the base type receive the value
unchanged, and calls resolved to the override narrow the result back to
its declared type.
2026-10-17 19:06:47 -05:00
Chris Tate 8bb3fa1077 Call valueOf and Symbol.toPrimitive on values held as any
`value.valueOf()` on a dynamic receiver was refused at compile time, a
class's own toString was not reached by `value.toString()`, and classes
could not declare a `[Symbol.toPrimitive]` method.

- `value.valueOf()` lowers to dyn.valueOf, which answers each kind's own
  valueOf: a primitive itself, a Date its time value, an object with a
  valueOf member that member's result, and any other object itself. A
  null-prototype dictionary throws "<spelling> is not a function", and
  null and undefined throw Node's property-read TypeError.
- When a boxed class declares valueOf or toString, `value.valueOf()` and
  `value.toString(argument)` call the class's method for its instances
  and keep the runtime call for every other value.
- Classes can declare `[Symbol.toPrimitive](hint)`. The dynamic
  conversions call it with "default" for `+` and `==`, "number" for the
  numeric operators and conversions, and "string" for template literals,
  String() and property keys, and throw Node's TypeError when it returns
  an object.
2026-10-17 19:01:59 -05:00
Chris Tate 9fd5c8a73f Widen ASCII to UTF-16 sixteen bytes per step in Buffer.write
Buffer.write(text, "utf16le") zero-extended an ASCII run eight bytes per
step with shifts and masks in 64-bit words. Where the target has vector
unpacks (SSE2 on x86-64, NEON on AArch64) it now widens sixteen bytes
per step: one load, a test of the high bits, and two unpacked stores.
The word loop stays for the remaining bytes and for other targets. A
scanner that fills a Uint16Array of code units this way, as tsc-ts does
once per source file, spends about half as long in the copy.
2026-10-17 18:59:01 -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 95b0e2132d Call generic members through receivers that are not statically known
Generic arrow properties and generic methods now have an erased runtime
value, their instance with every type parameter bound to unknown. Calls
through a round trip via any, a function result, an array element or a
class-backed interface call that value and convert the result to the
call's type, as JavaScript does. Direct calls on a known definition keep
their monomorphized path.

Fixes #905
Co-authored-by: Konstantin Mikheev <8560582+konmik@users.noreply.github.com>
2026-10-17 18:36:45 -05:00
Chris Tate 5e020e9ea6 Regenerate the surface manifest for the new util.types brand checks 2026-10-17 18:25:15 -05:00
Chris Tate 7c90c8e3a1 Pool keep-alive sockets in http Agents and use globalAgent for default requests 2026-10-17 18:23:08 -05:00
Chris Tate 7980eb4345 Match Node 24 in events and timers/promises (#1227) 2026-10-17 18:11:06 -05:00
Chris Tate 1ad47f1f77 Match Node 24 in util.format, util.types and util.inspect
- util.format %d and %i format bigint, symbol, null and undefined arguments
- util.format %j prints [Circular] for circular arguments
- util.types answers the remaining brand checks
- util.inspect prints WeakMap, WeakSet, Date and numeric typed array values as Node does
- util.inspect lowers the breakLength, compact and sorted options
2026-10-17 17:58:40 -05:00
Chris Tate 2ba6bfa9cd Merge main 2026-10-17 17:57:22 -05:00
Chris Tate 0580460c46 Match Node 24 in URLSearchParams, querystring, string_decoder and os
- URLSearchParams iterators compile outside a for-of head
- URLSearchParams can be built from [string, string] tuple pairs
- querystring.stringify writes bigint values
- StringDecoder accepts an encoding known only at run time
- os.setPriority is lowered
2026-10-17 17:54:52 -05:00
Chris Tate 6ae3eb5cc8 Match Node 24 in events and timers/promises
- The EventEmitter leak warning reaches process 'warning' listeners on the next tick
- The events listener-count and max-listener helpers work over EventEmitter instances
- timers/promises setTimeout, setImmediate and scheduler.wait accept an AbortSignal
- timers/promises scheduler.wait and scheduler.yield are lowered
2026-10-17 17:48:42 -05:00
Chris Tate a5675b2279 Call factory-defined static methods with their receiver class
A static method that a function-local JavaScript class defines at
runtime and a program class inherits (Animal.extend("Dog")) is now
called directly with this bound to the receiver class when it captures
nothing from the factory's scope, so the subclass it builds has a static
base and constructs like Node.

Fixes #938
Co-authored-by: phall <25888173+phall1@users.noreply.github.com>
2026-10-17 17:31:30 -05:00
Chris Tate 1783196f60 Resolve host names in net.connect (#1224) 2026-10-17 17:17:13 -05:00
Chris Tate a3b2c24b23 Document the wasm32-wasi runtime pack and fix the SC3003 hint (#1223) 2026-10-17 17:13:16 -05:00
Chris Tate a4a2472bd6 Lower Error.isError (#1221) 2026-10-17 17:01:01 -05:00
Chris Tate c9de7a351d Read .groups on matchAll rows passed to array callbacks (#1222) 2026-10-17 16:55:56 -05:00
Chris Tate 8bc797779a Group Sets, Maps and generators with Object.groupBy and Map.groupBy (#1220) 2026-10-17 16:49:35 -05:00
Malte UblandClaude Opus 5.5 714ef5aa09 Prove array lengths stable in index loops and version dense array loops
Index loops over arrays left the counter a double in several common
shapes, and every element access at the counter tested the array's
hole-free prefix on each pass:

- A syntactic proof (ir/array-length-stability.ts) shows that a loop never
  changes an array's length: the body runs no user code, never rebinds the
  array or lets it escape, and only resizes local arrays that no other
  binding can hold. Element writes at a counter kept below the length are
  accepted.
- Counted loops take a stable `a.length` as their limit, and an element
  access at the counter makes an unversioned counted loop worthwhile, so
  strided (`i += 2`) and descending (`i = a.length - 1; i >= 0`) loops get
  an integer counter.
- Array lengths bound while-loop and for-loop counters by their range,
  which needs no stability proof.
- A loop whose array length is stable and whose indexed arrays keep their
  storage runs as two copies. One test before the loop (the length is at
  most 2^31 - 1 and at most each indexed array's `filled` bound) selects a
  fast copy whose element accesses at the counter skip the prefix test and
  whose counter is an int32. LLVM can vectorize that copy.
- Loop counters whose ranges fit in 32 bits convert to double from their
  low half, so an inlined callee's index conversion folds away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:48:38 -07:00
Chris Tate e02834f078 Use asserted key types in the method replacement census (#1218) 2026-10-17 16:43:49 -05:00
Chris Tate 55005ef73d Resolve host names in net.connect
net.connect and net.createConnection dialed only IP literals and
"localhost"; any other host name failed with getaddrinfo ENOTFOUND
without a lookup, so names served by /etc/hosts or a container's DNS
(such as Docker service names) did not connect. The dial now resolves
the name with getaddrinfo using Node's hints, orders the addresses as
Node's autoSelectFamily does, and tries them in order.

Fixes #1156
Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com>
2026-10-17 16:42:55 -05:00
Chris Tate 89a6991278 Expose Promise.prototype methods on promises in untyped bindings (#1219) 2026-10-17 16:41:39 -05:00
Chris Tate 7689d4b148 Merge main 2026-10-17 16:39:51 -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 69fe16ac0f Keep the identity of checked-dynamic objects placed in JavaScript literals (#1207) 2026-10-17 16:37:43 -05:00
Chris Tate b7eb8255f9 Read .groups on matchAll rows passed to array callbacks
m.groups on a match row compiles when the row's regex is statically
known. The row can now also be the element parameter of an inline
callback over a matchAll drain (map, forEach, filter, find, findLast,
some, every, flatMap) or Array.from's mapping function over s.matchAll(re).
In JavaScript sources such a parameter holds the row as a checked-dynamic
value without its groups, and the read is refused instead of reading
undefined.
2026-10-17 16:35:27 -05:00
Chris Tate f1a4b364fb Report program type errors instead of npm package refusals (#1216) 2026-10-17 16:32:09 -05:00
Chris Tate adc2190bd6 Widen Result<T, never> and Result<never, E> unions (#1213) 2026-10-17 16:28:15 -05:00
Chris Tate 983058a3a7 Lower Error.isError
The ESNext error lib is now part of the type world, and Error.isError(value)
compiles. A catch binding tests its payload against the Error class, an
unknown value tests the checked-dynamic error encoding, and a union tests its
runtime tag against the arms whose class extends Error. Other values answer
statically: true for instances of Error and its subclasses, false for
everything else, including objects that only have an error's shape. The
argument is always evaluated. Unions with unknown arms are refused.
2026-10-17 16:26:42 -05:00
Chris Tate ed380babe9 Treat type-asserted values as present in the optional-value analysis (#1214) 2026-10-17 16:24:44 -05:00