Skip to content

AOT: lock a DOM constructor and Bun where they are made - #47545

Closed
robobun wants to merge 81 commits into
mainfrom
robobun/5e0bed89/lock-dom-constructors-where-made
Closed

robobun wants to merge 81 commits into
mainfrom
robobun/5e0bed89/lock-dom-constructors-where-made

Conversation

@robobun

@robobun robobun commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • An executable from bun build --compile --aot makes a write to URL.canParse that the build does not follow, when the program came to URL through instance.constructor. It then ends with AOT: built-in cell 952 (key 4165, URL.canParse) is not in this realm: 9, exit code 134.
  • The engine locks a constructor when the global object first adds its name. getDOMConstructor() makes it earlier, for instance.constructor and module exports. require("bun") hands out Bun the same way.

Fix

Background

  • Compiled code calls URL.canParse with no test if no compiled statement writes it. So the engine locks URL: an unfollowed write throws a TypeError.
  • Considered: an engine hook keyed by the ClassInfo of the instances. It names 19 of 51 DOM constructors. The class name reaches all 48 with a global name.

Downsides

  • An unfollowed write to AbortSignal.abort or URL.createObjectURL through instance.constructor now throws. Before, 40 of 129 tested rows made the write.
  • In an executable, a DOM class made under its name costs 344 more instructions once.
  • Still exit 134: Temporal and WebAssembly constructors, and delete on a DOM prototype from an instance (Notes).
Notes

Where this goes

The reported program

const u = Bun.pathToFileURL("/a");
globalThis.u = u;
const text = ["u.constructor.canParse", "= () => 'mine'"].join(" ");
(0, eval)(text); // new Function(text)() does the same
console.log(URL.canParse("x:y"));
answer
bun a.js mine
executable, canary 1.4.4-canary.1+da867c60b and this branch's base AOT: built-in cell 952 (key 4165, URL.canParse) is not in this realm: 9, exit code 134
executable, this change the store throws TypeError: Cannot assign to URL.canParse because built-in objects are read-only in a compiled executable
globalThis.keep = URL in front of the eval, before and after the same TypeError, then true
the store written in the file itself, before and after mine

No evaluated text is needed. A receiver that the build cannot follow is enough: hide(u).constructor.canParse = f with hide = v => new CustomEvent("x", { detail: v }).detail.

What the test holds

  • One executable answers 129 rows: 10 members (URL.canParse, .parse, .createObjectURL, AbortSignal.abort, .any, .timeout, AbortSignal.prototype.throwIfAborted, ReadableStream.from, SubtleCrypto.supports, Bun.pathToFileURL), each way to the holder (the global name, instance.constructor, Object.getPrototypeOf(instance).constructor, the export of abort-controller, require("bun") in the text of the write) and each write (a store by eval, a store by new Function, Object.defineProperty, delete).
  • With src/ of the base (release build of 5a98d88): 54 rows refuse, 35 end with is not in this realm (exit code 134) and 40 make the write. With this change all 129 refuse.
  • The 40 rows are a wrong answer and not a kept freedom. Compiled code calls the replacement as if it were the host's function: with signal.constructor.any = () => 42 from evaluated text, AbortSignal.any([s]).aborted throws TypeError: 42 is not an object, where plain Bun prints undefined.
  • More tests: a receiver that the build cannot follow, a Worker, the names URL and AbortSignal replaced by a store that the build follows (the lock is on the constructor, not on what the name holds), a followed store that stays, bun test --aot as one program and over two layers, and the class name of the constructor of each of the 48 rows of ZigGlobalObject.lut.txt.
  • A constructor is locked with what hangs on it, so instance.constructor.prototype is locked too (4 rows).

Still open: the same abort, other makers

program today where the fix is
Temporal and WebAssembly constructors const i = Temporal.Now.instant(); globalThis.i = i; (0, eval)(["i.constructor.compare", "= () => 'mine'"].join(" ")); Temporal.Instant.compare(i, i) .. (key .., Temporal.Instant.compare) is not in this realm: 9, exit code 134 oven-sh/WebKit: the engine makes these classes, and didCreateClass cannot name them
a DOM prototype taken from an instance delete Object.getPrototypeOf(hide(signal))[hide("throwIfAborted")]; AbortSignal.prototype.throwIfAborted.call(signal) .. AbortSignal.prototype.throwIfAborted) is not in this realm: 1, exit code 134. A store is made hostDidCreateStructure drops the report for a class whose row names no Structure
a member that a compiled statement also writes compiled URL.canParse = f, then an unfollowed delete URL.canParse not locked by design. The delete faults on main too not this change

The test lists the first two as rows that are not met (gap). Both are handed to follow-up work.

Other behavior changes

  • spyOn(hide(u).constructor, "canParse") throws Cannot spy on URL.canParse ... Before: spied, then exit code 134.
  • bun test --aot: as one program, the store through held.constructor ended the whole run with SIGABRT (URL.canParse) or was made (AbortSignal.abort). Over two layers it was made. Both forms now answer the TypeError.

Measurements

Release builds without LTO (the container cannot link the LTO build), linux-x64, base 5a98d88 against the same commit with this change. Instruction counts are user-mode counts of the first thread (test/aot/startup-scaling/measure-startup.ts, perf counters). bloaty, valgrind, perf and strace are not installed.

before after
instance.constructor read in plain Bun, JIT off (slope of 1e7 to 2e7 reads, 3 runs) 528.000 instructions 525.000
the same with the JIT (slope of 1e8 to 2e8, least of 3) 67.3 63.7
jsDOMURLConstructor on a hit (disassembly) 25 instructions, 192 bytes 22 instructions, 113 bytes
.text of bun (size -A) 107,291,184 bytes 107,285,808 (-5,376)
plain Bun, one DOM constructor made (disassembly, URL) +28 instructions, once for a class and a realm. 2 of them are the gate
plain Bun, hello to its end (median of 7) 2,768,202 2,767,444 (noise is 10K)
plain Bun heap after 16 classes are made (heapStats().objectTypeCounts) 92 types, 2,277 cells the same
generated code (test/aot/tools/same-output.ts, 289 programs) IDENTICAL
executable that reads 16 class names (median of 7) 196,567 202,076: +344 a class, for the second report of the same constructor
executable that reads .constructor of 16 instances and so makes 7 constructors without their name 52,739 78,478: +25,739 once, the lock that was missing

The slow path of getDOMConstructor() was inlined at every call site. It is now one function for each class (51), which is where the 5,376 bytes go.

Self-review

6 concerns raised, 5 addressed: a test of the class name that the report uses, the two forms of bun test --aot, rows for what stays open, a followed store to the global names, and more static members with a prototype row. Not done here: Structure rows for the DOM prototypes, which are the follow-up above.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/aot/a-constructor-is-locked-whichever-way-a-program-comes-to-it.test.ts

Jarred-Sumner and others added 30 commits October 27, 2026 17:58
…ut of lists and Workers' programs; WebKit preview 7f21d5ed [skip size check]

- WebKit: autobuild-preview-pr-927-7f21d5ed.
- Modules: bind_modules() keeps one set of stored __esModule marks for both kinds of module and reads a changed mark from the object. limit_exports() limits the export tables of faces as it does those of files. A name taken from a JSON import asks for the files between, so files that a package lists under sideEffects run. What `delete` removes from a namespace is bound on no object.
- CommonJS: under "type": "module" a default import holds what the link captured, and takes its short way only where nothing stores to module.exports behind the end of the body. A file whose namespace may be looked at and whose text does not say all its names has its object.
- Names of modules out of lists and tables: a walk of a list (forEach, map, filter, find ..) leaves the list as written only where its function is written in the call, has no rest parameter, does not find itself through `arguments`, a direct eval or its own name, and holds its third parameter as every other holder of the list does. `instanceof` hands the list on. A table that writes its own toString or valueOf is not followed.
- Programs under a second tsconfig take what the application's load found: import.meta.main, which file stands for which, erased sites, folded constants. An imported require that is given a path from its own folder resolves from that folder. Which file of the application a file is goes by its path and its loader.
- Classes: an unused class goes whole only where its heritage is proven; otherwise it leaves its heritage and one look at it, a computed key leaves ToPropertyKey, and a static member named "prototype" keeps the class. The transpiler records which classes assign their fields under useDefineForClassFields: false and the second parse reads that, with the build's own table of defines. Methods stored on the prototype of a constructor function are members of its layout.
- Syntax: the parser says where no `;` can follow a bare `declare`, so `declare` is a name on the left of for-of, for-in and for await and in front of every kind of line break; a line break behind `global` is any of the language's.
- Types: only callable built-ins join a function type; the type of a well-known symbol is symbol.
- Workers: whether the files and texts that Workers start from are one program with the rest is decided behind the front end, ahead of the first run of the engine. The constructor tells the engine when other threads are ending.
- macOS: operator new of every piece of a trimmed executable goes to mimalloc, and the ranges of its pieces are known to stack traces.
- bun check: a ring of functions without annotations is resolved in linear time.
- The build: a third list of the engine's sources, compiled for minimum size.
- Tests: stored programs, generated tables and fuzz families for each of the above and for the engine's changes; a NaN of every pattern of bits through N-API, bun:ffi and the serializer into fields of every kind; definitions by descriptor; host objects that come back by some road; declarations that nothing uses; contextual keywords as names. Files that read how often compiled code calls the run time run on Windows. The large build of workers-from-texts-that-the-build-knows takes its turn among the others again. Program folders have short names; two tests join the fixed step; tests that pass on both Windows lanes leave the awaited list and two units join it; the engine's native tests run on Windows.
- Docs: rows of docs/compiler/compatibility.mdx follow the above.
…off an ES module, and tests for the engine's changes; WebKit preview 617ed49e [skip size check]

- WebKit: autobuild-preview-pr-927-617ed49e.
- Modules: what kind of module a file is goes by whether a top-level await is written in it, not by whether one can be reached. A CommonJS file whose getter reads, off an ES module, a name that the ES module hands on from another CommonJS file keeps its object unless the copy and the name cannot be told apart.
- The loader's fetch hook asks VM::synchronousModuleQueue().
- Strings: the natives of node:path take the width of a substring's characters from its base's storage as it is now; a string that is used as a property key takes the storage of an equal key, of either width.
- Output: a build prints neither the hint about untyped code nor the counts of property accesses; the counts end the text report of --explain.
- macOS: --aot-symbols takes a file whose table of indirect symbols has an odd number of entries.
- bun check: the calls that were resolved meanwhile are one list.
- Windows: in place, the path that an import with { type: "file" } gives has the separators of the host.
- Tests: stored programs for each of the above and for the engine's changes: module variables of files that are still running, patterns without elements, Reflect.construct, super behind a delete, closures among many entry points, counted imports under a synchronous load, a callback that stores the answer of a call that never returns into a captured variable. Own members of compiled objects that native code looks for: RegExp test and search, the first line of a stack, Bun.inspect, concat, printed class names, labels of heap snapshots. Sums over typed arrays. A family for traps that are stored on the handler after the proxy is made. Four units run the engine's flow tests under 32 other schedules of the rules. The reference run of the packages takes the reference's environment. Five more units run on every commit on Windows. The three programs that terminate a Worker inside a loop count the walks that came to their end, in place of a clock. The tests of --trim are skipped where the Bun under test cannot trim, and the one on the allocator also runs with glibc. highway-strings.test.ts lists what is wrong and calls expect() once a test: the same cases in half a second. One-loop rows for numbers that leave their slot. A way out of native code that the system refuses is left out of both runs of the N-API test. A test of lint.yml's kind runs check-unified-sources.py. Sabotages for calls that throw without a site and for bounds that are read twice. Pins of generated code that fell are restated. The pins and rows of objects of constructor functions that are not built are as they were before that step of SinkAllocations.
- Docs: a row of docs/compiler/compatibility.mdx for useDefineForClassFields under a nested tsconfig.json, and its table of platforms says what CI runs on each; a paragraph of docs/compiler/internals.mdx on calls without a site.
…, the program named at start on Windows; WebKit preview-pr-930-3fdfc9b1 [skip size check]
…named without its extension; typed rows of a query; WebKit preview-pr-930-bf1133d2 [skip size check]
…each executable behind its runs; each CI step names the build it tests; the Mach-O writer hands out owned parts of a mapping; bun check keeps the members of a named union; WebKit preview-pr-930-0cb47ca7 [skip size check]
…un without debug information that the build step uploads beside bun-profile; test/aot/fuzz and its test as in 5319f52 [skip size check]
…unction made by bind() of a proxy and what is read through a proxy are followed; tests for modules of both kinds in a ring; the runner gives the compiled programs a job for each processor, deals the dearest first and runs the quick test files in a bucket of their own; the watcher of memory has a thread of its own; WebKit preview-pr-930-c2d259b5 [skip size check]
…ld that asks nothing of the library reads no file of it; process.send and Bun.spawn bring the channel in only where they are called; children and parent of a module object that goes out of sight; a moved range keeps what follows it in its last page; the runner deals the compiled programs by all their files, and the dear ones share one library; WebKit preview-pr-930-02becc12 [skip size check]
… all modules run in the cargo lane; every lane builds the light programs alone; under ASAN the compiled programs run on a Bun without debug information; a following round is asked for only where it keeps more; a frame names a function made under a symbol key by its own name; the generators of the fuzzer are sources of run.ts; WebKit preview-pr-930-430afb3a [skip size check]
… traps; the printer is loaded for a printed value only where a class that needs it may have an instance; start-up functions are handed over by need; internal/fs/streams no longer loads node:fs/promises; a list that grows by push is checked in linear time; the tests build a trimmed and a whole executable in one build [skip size check]
…at it imports; the shards are dealt whole builds; long test files run beside one another under ASAN and on Windows; the texts of the library read the members of Object, Buffer, Reflect and Bun in one order; a list that grows by push is checked in linear time whatever lies between the pushes [skip size check]
…o ask for; the exports of a module of the library are handed to nobody new as the receiver of a member call; pushes with branches between them are checked in linear time; tables that only a build reads are made at first use; one compile writes the whole and the trimmed executable in two test files [skip size check]
…that cost the plain files their form [skip size check]
…described by their paths; a star or dynamic import of a library module brings what its getters load; a call of what is no function throws wherever it stands; a name out of a list that was changed in place counts as unknown; the long test files run two at a time on the Linux lanes; every test of the compiler runs on Alpine, the other lanes run those that ask the system, and the lane with a sanitizer leaves out those that compile one text again; an executable built on a PE template chooses its cuts anew; dependencies that no code of a crate uses are gone [skip size check]
…hem; three programs run half their turns; a test file that says it runs alone stays out of the bucket [skip size check]
…piler run on a copy of Bun without its debug information, and what fails is tried again on the whole file; bun check --timing prints its queries, and the test of long rings counts them [skip size check]
"classes with a method of one name that each call it on an instance of any of them" has had its growth in instructions and in two counts in budgets.linux-x64.json. Its growth in memory stands beside them now: macOS judges it, where Linux leaves it unjudged below 96 MiB.

[skip size check]
src/exe_format/macho.rs: the loop that hashes and writes pages calls `SHA256::hash(page, hash)`, as on main.

[skip size check]
…ative plugins run on every lane

- readMembersThatMakingStandardErrorReads(): the two lambdas are static functions with a ThrowScope of their own, and each call site checks or releases.
- scripts/runner.node.ts: the three test files that compile a plugin with the system's compiler and load it are among the tests that ask the system, so they run on every lane.
- test/aot/files-written-while-bundling.test.ts says why its plugins wait.

[skip size check]
On macOS, an executable made by `bun build --compile` started a detached
thread at boot that loaded the system trust store when a flag in the module
graph said the program might ask for it. Under `--aot` the flag was set by
reachability; otherwise it was set by searching the output for the text
`getCACertificates`.

This removes the thread, the flag, both places that set it, and the test for
the flag. The system trust store is loaded on first use again, on the thread
that asks for it.

[skip size check]
They checked that Security.framework was loaded before the program asked for
the system certificates, which no longer happens. Also updates a comment in
node:tls that described the prefetch.

[skip size check]
…IR tests [skip size check]

Pins WebKit to the preview of oven-sh/WebKit#934.

- Merges main.
- Wrong answers and compiler stops found by fuzzing, by the run-time checks and by CI are fixed, each
  with its reproducer under test/aot.
- Under --minify-identifiers a class with standard class decorators keeps its written name.
- The compile object of Bun.build() takes aotValidate, aotCheck and aotTesting, hidden like
  aotDisable and aotHeat, with the meaning of the command line's flags.
- --explain names where an escape begins.
- The React compiler's lowering reads every segment of a joined string.
- A compiled hello world starts with fewer instructions, page faults and system calls.
- mimalloc moves to 6da53c35 and takes no patches.
- The directives of an .ir test are checked strictly: a misspelled or misplaced one is an error.
- The test runner leaves the engine's shell, its test program and its tests packed in a shard that
  starts none of them, and unpacks with tar on Windows.
- The checks that read the sources are removed, with their two steps in lint.yml and the aot_check
  build rule.
- scripts/vs-shell.ps1 keeps variables of several lines away from the Visual Studio loader.
Jarred-Sumner and others added 15 commits October 30, 2026 19:07
…of CI found

The engine is autobuild-preview-pr-942-e2ad53a2.

Builds:

- Windows x64 did not link: "trim: the first link failed: duplicate symbol". The second link
  put the compiled objects in front of the archives. lld-link resolves a weak alias of a
  bitcode file (664 vector deleting destructors in JavaScriptCore.lib, `__global_delete`) from
  what it read when it parsed the bitcode, so without the bitcode the name was undefined, the
  archive's index offered it, and the bitcode member loaded beside its own compiled object.
  The driver now asks first whether any such name exists. If so, the second link takes the
  first link's inputs, as it did before. Only x64 has C++ as bitcode; arm64 is unchanged.
  link-coff-freestanding.test.ts has a case that fails in the same words without Windows.
- Windows did not compile: E::EDQUOT is asked for on unix alone.

CI:

- scripts/runner.node.ts kept a list by hand of the tests under test/aot that need jsc and
  testaot unpacked. Three new tests were missing from it and failed whenever they landed in a
  shard with no listed test. The runner now follows a test's imports; a new test holds that
  search against Bun's own import scan. Nothing changes for tests outside test/aot.
- Five findings of mordant: three functions generic over closures, a field that is valid only
  beside another, a public method nobody calls.

Fixed since the last commit, each with a test:

- `require.cache` reloads: a file taken out of the cache and required again is run again.
- `p.versions = 5` on `node:process` is refused however the program came to `process`.
- `import { x }` of a malformed module segfaulted.
- `class Thumbnail extends Bun.Image { label() {} }` lost `label`.
- Two programs with a cycle of ES modules and CommonJS files failed at start.
- `import(`a${a}`)` took in every installed package whose name begins with "a", and one file
  among them that did not parse failed the whole build.
- `bun build --compile --aot` takes one pass of the checker again; `bun test --aot` takes two
  only where the memory limit asks for it. The output is the same at one, two and three.

Tests that were wrong, not the product:

- Two programs counted refills of an allocator as calls into the run time, which a build with
  the address sanitizer makes more of.
- Two programs printed positions inside built-in functions, which a Bun with assertions
  records and a release Bun does not.
- calls/host-getters-and-string-builtins-value.js: plain Bun itself gives two answers,
  `'action()'` or `'f(a)'`, by how far its DFG thread has come. The program turns the DFG off.
- every-image-of-an-executable-has-its-map: its witness function is inlined away now.

Known to be wrong in this commit; each is fixed by an engine file younger than this engine:

- The compiler overflows its stack on typeorm's SelectQueryBuilder.buildWhere.
- `Reflect.ownKeys(f).filter(..).map(..).join(",")` aborts the compiler in InlineCalls.
- `"one" in barrel` behind `__exportStar` fails an assertion in builds that have them.
- Test runs over layers stop in builds that validate exception checks (placeInRealm).
…s and packages, mocks that change a file's branches

Moves the engine to autobuild-preview-pr-942-545a0509.

Known in that engine, and out of the next one: `let k = -1; for (const x of v) { k++;
if (x > g) { g = x; other = k; } }` over an Int16Array of 8 or more elements gives the last
index. Plain `bun run` is right.

Builds:

- Windows x64 did not link. The trim step links what the first link compiled from bitcode a
  second time, as files in front of the archives, and 664 weak aliases that lld-link resolves
  from bitcode came in twice (6,976 duplicate symbols). The second link takes the bitcode again
  where a name would load it twice, and the aliases are read out of the bitcode's own symbol
  table (scripts/build/trim/bitcode-symbols.ts).
- Windows arm64 did not compile: E::EDQUOT is spelled DQUOT there. Asked for on unix alone.
- cargo clippy (needless_pass_by_value in loads.rs) and six findings of mordant.
- CI unpacks jsc, testaot and the engine's tests for whatever test imports them. The list kept
  by hand in the runner is gone.

Crashes fixed, each with a test:

- `Object.setPrototypeOf(import.meta, { inherited: 1 }); import.meta.resolve("./x.mjs")` was a
  segmentation fault that no try catches. It is a TypeError, as in `bun run`.
- `where.map(item => this.buildWhere(item)).filter(..).map(..).join()`, a method that maps a
  list with itself at the head of a chain, overflowed the compiler's stack.
- `Reflect.ownKeys(f).filter(..).map(..).join(",")` aborted the compiler in InlineCalls.
- `"one" in barrel`, where barrel is `__exportStar(require("./leaf"), exports)`: an assertion.

Wrong answers fixed, each with a test:

- `new Pool(new URL("./w.ts", import.meta.url))`, `pool.start(url)`,
  `new Configured({ filename: url })`, `require("./spawn").start(url)`, and the same out of
  node_modules: the file of the Worker is followed to the `new Worker(entry)` and compiled.
- `export const level = 1; export const pick = () => level === 1 ? "one" : "other"` and
  `mock.module("./m.mjs", () => ({ level: 2 }))`: the file had decided its branch in the build.
- `writeFileSync(path, 'import { one } from "./lib.mjs"'); await import(path)` in a test, where
  lib.mjs is a compiled file of the run, said "Cannot load".
- One file hands out `new Proxy(WeakMap, {})` and another stores `proxy.prototype.get = f`:
  refused over layers, a plain store in `bun test`.
- `const made = { a: i }; try { slots[i] = made } catch { .. }` at a full heap: the catch got the
  error of the allocation in front of its try.
- `module.parent = 7` in a package outside the executable; a file that rebinds `module`, deletes
  `module.exports` or stores into `module.parent`.
- `process.versions.node = "mine"` in a .cjs file was dropped without a word. It throws there
  too, as it did in a module.
- `enum Mode { Fast = 1 }; const mode = Mode.Fast` is the constant that `bun run` makes it.
- `class Holds extends [Maybe, Other][n] { #number = 1 }; new Holds(new Holds())`, where the base
  returns what it is given: the second initializer throws, as in `bun run`.
- A cycle of private functions is entered by the same function whichever is asked for first.

Test runs:

- A run reports the peak memory of each of its processes. On Linux getrusage() gives a spawned
  process its parent's peak as a floor, so the peak is read from /proc/self/status.
- The second pass of the checker over every file of a large layer is out again: it cost 270 MB
  and 7 s. With it goes its test.

Faster, no answer changes: `require("./data.json")` in a loop (426 > 138 instructions),
a Map or Set written out in the program and asked in a loop, `list.map(f).find(g)`,
`holder[key] = value` where the name was added late, `new RegExp("^(?:" + words + ")$").test(w)`,
16-bit texts against patterns made at run time, a small function called in a loop of code that
runs once, `bytes[k] = i + k`, the text of a number asked for twice, 109 questions fewer at the
start of every executable.

Tests: three generated batteries are stored programs now (74 files, 4,084 cases): an object
that gets a second name by one of 191 roads, values compared twice, objects that escape on one
path. A program of five million operations takes the path for very large programs at default
flags. Listings of generated code follow the engine.
… time, CI's failures

WEBKIT_VERSION is autobuild-preview-pr-942-f61fbc65. The engine's commit says what it fixes:
a build that never ended, three crashes of test runs, JSON.stringify of a list that holds
undefined, objects that were kept alive. This is Bun's half of those, and what is Bun's alone.

Wrong answers fixed, each with a test:

- `console.log(process.stdout)` printed `{}` in an executable where `bun run` prints the
  stream. The same for process.stderr, process.stdin and console._stdout.
- `import { db } from "./data.sqlite" with { type: "sqlite" }` was a build error, and
  `import * as ns` lacked `db`.
- A name that two imports bind means the last import, as in `bun run`.
- `require("./names/" + [".hidden.js"][0])`: a file whose name begins with a dot was not found
  out of a list of names.
- `await import("./data/" + name)` of a .json file with a `//` comment says what `bun run` says.
- Under a tsconfig.json whose list takes in Same.ts and same.ts, a main.ts that reaches neither
  did not build (TS1149).
- `import a from "./data.json"; import b from "./data.json" with { type: "json" }` in one test
  file stopped the build under the verifier.
- With `--define`, `f.toString()` and error quotes read as in `bun build --compile`:
  `if (DEFINED) dead()` is `if (false) ;`, and a define of any kind prints as its value.
- A type without end as a member's type, `type Endless<T> = { next: [Endless<[T]>] }`, no
  longer keeps the build from ending.

Build time and memory, no output changes:

- `bun test --aot` beside very large package files: the step in front of loading took 90 to
  125 s where it had taken 8 to 13. Each of about 460,000 questions walked the same variables
  again. Each variable and parameter of a file is walked once.
- A chain of 3,000 untyped functions: 677 G > 7.5 G instructions. It grew with the cube.
- 4,000 functions on one line after non-ASCII text: 24,855 M > 8,472 M.
- Rows of constants: an expression that folds was still lowered whole, so that a name in its
  dead zone could throw. A name that has given way to its constant's value cannot throw.
  3,734 M > 2,658 M and 4,748 M > 2,958 M.
- Writing an executable: what lies behind the payload in the carrier was written in one call
  out of the mapping, so all of it stayed resident. A one-line build with a 359 MB carrier
  peaks at 84 MB where it took 273. `bun build --compile` without --aot runs the same code:
  22 MB where it took 218, the same bytes written.
- A CommonJS file that ends in `module.exports = exports.default`: the ring through the alias
  is entered from the first file in order, so the answer does not hang on the order of tasks.

Building Bun:

- testaot knew no locale on Linux. The prebuilt ICU's data are compressed, and ICU asks a weak
  hook to decompress them. Bun and jsc define it, testaot did not, so
  `Intl.NumberFormat.supportedLocalesOf("de-DE")` was []. Bun's build gives it the hook.
- scripts/build/trim/archive.ts: `toSorted` is not allowed by the lint.

Tests:

- Tests that judge memory print what they read and their bound when they pass too.
- Rows that musl is forgiven are no longer printed beside a real failure, so a log that showed
  six failures shows the one.
- Six stored tests that read process.env stay off Windows, where flow rightly leaves that
  object out. Six programs in which Windows x64 keeps an object alive are marked todo there.
- `one-line-changes-little`: one line is listed as known. Nothing grew: the base program
  shrank by 1,417 bytes, and the bound is a share of the base.
- Two node:http programs that failed now and then on macOS send again.
- 1,695 more generated cases: what a file asks about itself, and which file of a package loads.
- Listings of generated code follow the engine.
…__dirname, room set aside for test files

WEBKIT_VERSION is autobuild-preview-pr-942-e01a9a37. The engine's two commits say what they
fix: four silent wrong answers, a crash, mocks of data files, the depth of JSON.stringify, an
abort under FailPredictions. This is Bun's half of those, and what is Bun's alone.

The compiler crashed, since the commit before last:

- `const isDev = false; const url = isDev ? "http://localhost" : "https://example.com";
  console.log(url);` at the top level of a file ended the build with a segmentation fault. What
  `bun run` prints in place of what is written is told to the engine as pieces, and `a ? b : c`
  with `a` known is a piece over all three that copies the range of `b`. A piece that would
  copy itself is not written.
- The front end reads its finished lists of pieces and cuts again, for every module of an
  executable. A piece that lies in what it copies stops the build with the file's name.
- `const F = false; const b = F ? 0x10 : null; function one() { return b.x }`: the error quoted
  '16null.x' for 'null.x'.

Executables, `bun build --compile` is the measure:

- `fs.readFileSync(path.join(__dirname, "assets", "data.txt"))` was ENOENT. `__dirname` and
  `__filename` were lowered as import.meta.dir and import.meta.path, so they lay under
  /$bunfs/root. They say where the file lay when the program was built.
- Behind that, a compiled Next.js server answered 500 to every request.
  `require(path.join(__dirname, ".next/server/middleware-manifest.json"))`, import() and
  require.resolve() of an asset by its build-time path find it, in place and moved away. Where
  a program replaces `Module._resolveFilename`, what the hook answers is translated too.
- `require(" ./parts/two.js ".trim())`, `.replace("xxx", "two")`, `.replaceAll()`,
  `"./parts/".concat("two", ".js")`, `.slice(1)` and `String("./parts/two.js")` were
  MODULE_NOT_FOUND. The scan reads a name that a string method makes of a known text.
- `a[300_000_000] = 1; a[0] = 2; a.sort(f)` ran out of memory: the scratch list was as long as
  the array. It grows with the values that are found: 18 MB.
- `{ function inABlock(a) { return a + 1 } String(inABlock) }` answers `function(a) { .. }`, and
  `mock(inABlock).getMockName()` answers "".

Test runs, `bun test` is the measure:

- `const { mock } = require("bun:test"); mock.module("made-up", () => ({ v: "mocked" }))` in a
  preload, where no package or file has the name: `import { v } from "made-up"` said ENOENT
  wherever no import statement names `mock`.
- `import x from "./x.json" with { type: "json" }` in a test file and in its helper were two
  objects beside another file's plugin, and over layers with no plugin at all.
- `try { require("./mixes.cjs") } catch {}` where mixes.cjs holds an import statement beside
  `exports.v =`: `bun test` catches a BuildMessage and goes on. The compiled run printed
  TS100106 and ran no test. All nine mistakes that the transpiler finds when it visits a file
  are left to the loader.
- `import.meta.resolve("made-up")` of a name that only a mock has throws over layers too.

Memory of a test run:

- `--aot-memory-limit=5GB` held by luck: a build inside a test takes 2 to 2.6 GB within five
  seconds, faster than anything that reads what is resident can answer. A test file starts
  only where what is taken and what the file took last time fit under the limit.
- The watch over a build's memory asks the build before it ends it, as the time limit does, so
  the log says which function and pass it was in.

Build time, no output changes:

- A build that starts Workers loaded every file, searched them for `new Worker(file)`, threw the
  load away and loaded everything again. The load itself asks, once, for test runs and for
  executables that start node Workers.
- One file of 9 MB beside 5,000 small ones: a second thread prepares it while the first checks
  it, and no tree is packed away once a thread finds nothing to take.
- No tree is packed once every file is lowered.
- The threads of a build take turns at giving memory back.
- The test run says when it reaches each step.

Executables that held too much: `process.stdout.write("x")` beside `console.log(error.name)` of
a caught error compiled 497 functions for 169. The small stream's target has a getter and no
printer.

Building Bun:

- `linux x64-hardened` did not link since the previous commit: the hook that lets testaot read
  the bundled ICU's data was compiled without that build's flags. It is built with them. The
  same hook on Windows.
- A Bun built with `--aot=off` defines all that the bindings' header declares, and a test holds
  each of its 33 declarations against that half.
- What Linux alone measures in the parallel test runner is expected to lie idle elsewhere, so
  macOS and Windows compile again.
- clippy and the lock lint: std::sync::Mutex is not allowed here.

Tests:

- Three front-end listings follow the rows of constants of the previous commit (cargo-test).
- compile-trim-locales reads its locale from the arguments.
- Where Bun has assertions, `Crypto.prototype.subtle` is left out of the writes on accessors
  (`main` aborts there too), and the bytes of a comparison are not held to a bound.
- A leak that ended a part's builder under ASAN.
- The harness's `setDefaultTimeout(120_000)`, meant to raise bun:test's 5 s, lowered the 270 s
  that CI's runner gives a case on the ASAN lane. It never lowers what the runner gave.
- carrier-with-much-behind-the-payload asked for an exact file length. Where something stands
  behind the old section header table, its 2,752 bytes stay in the file.
- The witnesses of the wrap at 2 ** 31, upward and downward, are stored programs.
- Listings of generated code follow the engine.
…ic on one file spelled two ways

Merged with main at 34cbc70. GitHub's Rust checks build the pull request merged with main, and
that did not compile: main's node.rs asks `get()` of the cell that our hir.rs wraps. `LazyCell`
has `get()`, and `struct Checker` holds the fields of both sides.

mordant reports main's own `walk_bun_glob` (src/glob/lib.rs) as an unused `pub` on every pull
request since #47513: nothing outside its file names it. It is there to instantiate the walker's
methods once, so it stays, and `pub(crate)` would make it dead code. The finding is allowed on
it, under cfg(mordant) alone, as src/jsc/bindgen.rs does. No build changes by a byte.

WEBKIT_VERSION is autobuild-preview-pr-942-fbdf8d40. The engine's commit says what it fixes. This
is Bun's half of that, and what is Bun's alone.

The compiler stopped:

- `require(short ? "./a" : "./a.cjs")` spells two names and may load one file. In a file that a
  test takes out of `require.cache` the build panicked ("range end index"), and no test of the
  run ran. In an executable, one file spelled four ways in one `require()` aborted the build.
  Three places read the list of files with a range of the list of names.
- `new Number(5).toLocaleString("en-US")`, and the same of a class below Number, was an internal
  compiler error.

Executables, `bun build --compile` is the measure:

- `/** @import inside (@hit()) */` above a function of a .js file ran `hit()`. A sentence in a
  comment of `less`, "@import inside @-rules (@media, @supports, etc.)", threw "media is not
  defined", so `require("less")` died at start. A decorator that no class follows becomes a
  statement in front of the one being parsed, and a mistake in a comment of a JavaScript file
  stops nothing. What the parser collects inside a comment is dropped with the comment.
- `Module._extensions[".js"] = function (mod, filename) { .. }` threw, so `pirates`, and through it
  `@babel/register` and `sucrase/register`, died at start. The store goes through. A file of the
  executable never asks the loader, as in stock's executable.
- `import text from "./data.json?raw"` gave the parsed object, and `import("./code.mjs?raw")` and
  `require()` of it ran the file. A path with a query is a module of its own, named with the
  file's ending, and the loader was picked from that name. The query is asked first. `bun run`
  is the measure here: stock's executable resolves no path with a query.
- On Windows, `new Worker("data:text/javascript," + ..)` with CommonJS text read `__filename` as
  `data:text\javascript,..`. A text that is no file lay nowhere when the program was built.
- What an error message quotes of a condition that folding decides is the operand that `bun run`
  prints of it.

Test runs, `bun test` is the measure:

- `writeFileSync(join(import.meta.dir, "state.js"), text); await import("./state.js")`, the first
  load of state.js in the run, ran the text of the build. A file is compared with the disk before
  it first loads, whoever wrote it.
- `Module.prototype.require = function (id) { .. }` (require-in-the-middle) and
  `Module._resolveFilename = function (request) { .. }` (module-alias, tsconfig-paths) were never
  asked for a `require()` of a constant name or for `require.resolve()`: the wrapper patched
  nothing, silently.
- `import os from "node:os"` behind `mock.module("node:os", () => ({ EOL: "x" }))`: the default is
  the mock's, for ten built-in modules in five forms, behind a preload too.
- `bun test --aot --parallel=2 --cwd=sub` over layers: "Could not change directory to "sub"", and
  no test ran. The build of a layer starts where the run was started.
- With `--parallel=1`, or one test file, `bun test` starts no worker. A run over layers handed the
  files to one and ended like a run of many: exit 0 for 3 after `process.exitCode = 3`, exit 0
  for 1 for an inline snapshot that cannot be written, no `snapshots:` line.
- `Object.getPrototypeOf(function* () {}).added = 1` in a test file threw over layers and went
  through as one program. The same for async functions and async generators.
- `import "./bare.cjs"` of a script without `require`, `module` or `exports`: behind `import`,
  `bun test` goes by the package's "type", so the file is an ES module, strict, and runs behind
  the CommonJS file beside it. Over layers it was CommonJS.
- `import * as math from "./math.ts"; (math as any).double = f` anywhere in a test file was TS2540,
  and every test of the file was lost. That refusal is `bun build`'s. It throws a TypeError
  where it runs.
- `globalThis.__lookupSetter__("self").call({}, "x")` over layers did nothing and said nothing.
  It says that it is refused.

An executable built with `--aot-validate` died before its first line where a callback leaves
sight of the compiler in the frame that made it.

Build time and memory, no output changes:

- `class C1 extends C0 { f1 = 1; m() { return super.m() + 1 } }`, 2,000 in a row: the members of
  a class were a list with a copy of every member of every class above, 2 M copies. A member is
  found by its name. Asking whether a class has a base went up the whole row each time.
- `type Reversed<T> = T extends [infer F, ...infer R] ? [...Reversed<R>, F] : []` over tuples of
  up to 100 elements, one to a file, was accepted in one checker pass and refused in two.

Speed:

- `const b = [...xs]` where xs is proven to hold integers: the reads of `b` asked for the storage
  of fractions, a test that never passes, so every turn took the slow path.
- Spreading a list of fractions boxes an element in 8 instructions for 10.

Tests:

- cargo-test did not compile bun_aot's tests: two of them build `EngineOptions` by hand and
  lacked the new field.
- All 85 files of test/aot that set a default time limit under the 270 s of CI's ASAN lane go
  through one helper, which never lowers what the runner gave. A test fails if one comes back.
- require-cache-in-test-runs ran its 47 cases one behind another and met the file's limit under
  ASAN. They run at once and are dealt out over four files: 156 s for the slowest, of 360.
- 84 commands at once, 63 of them builds, held 75 GB under ASAN. As many at once as processors.
- The harness's memory guard has two reasons to end a run and named one. It says which.
- Two tests proved that compiled code ran by writing over a file and importing it, which now
  rightly loads the new text. They go by `BUN_JSC_crashIfInterpreterIsUsed`.
- "a later test file imports the file" expected two files of one sub-folder in the order of
  their names. `bun test` takes them as the disk lists them, and tmpfs lists the newest first.
- A stored program with ``import(`a${a}`)`` took in every package above it whose name begins with
  "a": 4,603 modules, 458 s, 2.7 GB. CI's runner starts the driver beside `bun install`, and only
  the Macs' install was fast enough to lose that race. The program asks nothing of what is above.
- carrier-with-much-behind-the-payload looked for the tail from the carrier's length on. A Bun
  that trims makes an executable shorter than its carrier.
- Miri runs the unit tests of the packed lists, and an entry that names no test is red.
- Flags that come off the wire go through their checked constructors (mordant).
- Stored programs: objects grown by computed keys and spreads, 105 scenes; text + number under
  collection stress; a build-scaling shape for sums of fractions alive past calls.
- Listings of generated code follow the engine.

[skip size check]: the compiler is in the one binary. Bun grows by 2.6 to 3.2 MB on seven targets and
by 0.9 MB on Windows x64.
…ft, what was red in CI

This is the previous commit's tree and only what fixes a failing test or a review finding. The
engine is the same.

The compiler stopped:

- `const x = ["a", , "c"][1]; console.log(x);` ended in "internal compiler error (LinkAndEmit)".
  A hole's expression ends one byte before it starts, and a length was taken by subtracting. Since
  the previous commit.

Compiled programs:

- The first read or write of `process.stdout` or `process.stderr` threw EMFILE when the process had
  no descriptor left. The light streams open a sink, which duplicates the descriptor. Where that
  fails they now write to descriptor 1 or 2 itself, as the whole stream does.
- `process.nextTick()` under an `async_hooks` init hook calls whichever copy of `node:async_hooks`
  enabled the hook. No build we can make today has two copies, so this changes no answer yet.
- `bun test --aot` aborted on a host that had reached its limit of threads. It goes on with the
  threads it has.

Test runs:

- `bun test --aot --parallel=1` over layers prints and stops as the one process of plain Bun does.
  Tests ran on behind `--bail`.
- A filter beside `--files-from` was dropped in silence. It's an error now.
- `import(`specs/build/${name}`)` or `import("@/pages/" + name)`: a name whose fixed beginning can be
  no module of Bun's own no longer makes a layered test run compile the whole library.

Plain Bun:

- `src/js/internal/worker/stdio.ts` is main's again, byte for byte, and our test of it is gone. What
  we had put there revived a Worker's stdout after an out-of-memory write, and the stream could then
  never end.

Smaller programs:

- `console.log(navigator, new Event(name), new TextEncoder())` took in `util.inspect`: 449 functions
  for 3. The table described 6 of the 12 getters of `Event.prototype`, and a getter it doesn't
  describe answers an object nobody knows. All twelve are described.
- `import("./own.js")` read `.resolve`, `.promise` and `.then` by name. One library function does
  those steps with the intrinsics.

CI:

- `test/expected-durations.json` has our files' seconds from the last round's 96 logs. 304 of our
  test files had no entry and counted as 0.4 s each: 122 s for 33,730 s on the ASAN lane. None of
  main's entries moves. A file without an entry now counts as the mean of the files beside it.
- A file that the harness killed because its own commands held too much memory isn't run again. It
  fails the same way each time. One file spent 13 of a shard's 45 minutes that way.
- Two test files that are thin against the time the ASAN lane gives them are dealt out over more
  files. No case changes.
- Two tests that we had deleted came back in the previous commit through a fault of the script that
  cuts it. They're gone again. One stored listing follows a change that was taken out.
… the compiler's size restated

The engine moves to oven-sh/WebKit#942 at 44bc299d. No source of Bun's changes: the pin and 19 test
files.

What the engine fixes, with the tests that come with it here:

- `list.push(value)` could lose `value` where the list had to grow by one element. The next read
  gave another value, or the program crashed. It was rare and wanted a loaded machine.
  `parallel/arrays/lists-that-grow-by-one-element-keep-it.js` fails once in some hundred runs
  without the fix.
- An async function or a generator with more parameters than fit in registers crashed when it set
  a late one to `null` behind an `await` or a `yield` and several calls were under way.
  Eight programs in `parallel/parameters-on-the-stack/`.
- Where `method` in `object.method()` is a getter that throws or reads a stack, the trace lacked
  the function that made the call.
- `bun build --compile --aot` aborted on lines of pako 3 that clear a typed array and read through
  `undefined`.
- One IR test of the flow analysis gave another answer under some orders of its rules. The last
  commit's change to the table of built-ins had moved such an order onto one that CI runs.
- Five native tests took the shared code for less than 1 MB, which it isn't under ASAN. A leak of
  ICU's in `Intl`. An assertion of the register allocator. An instruction bound on Alpine.

Known gaps, marked and not removed:

- `gc/locals-set-to-null-let-go.ts` and `async/objects-last-read-two-awaits-collected.js`, under
  ASAN alone: an object that nothing refers to any more is still alive when the program looks.
  Compiled code leaves a copy of the pointer in a stack word that it no longer uses, and whether
  native code overwrites that word goes by the layout of native frames, which moved in the last
  commit. Release builds pass. `run-parallel.ts` learns `// Todo under ASAN:` for them.
- `gc/parameter-that-came-on-the-stack-set-to-null-lets-go.ts`: the object now lives until the
  function returns. The store that let go of it earlier is what crashed above.
- `edge-cases/a-hole-read-by-a-constant-index.js`: two error messages quote the source otherwise
  than plain Bun does.

`test/bundler/compile-aot-size.test.ts`: the most bytes that the compiler may take of Bun goes from
22,750,000 to 23,750,000, once. It measures 23,647,988.
…ix engine fixes

`bun test --aot` mapped every source file of 8 KiB or more while it built what the test files of a
run share. A file saved in that time was cut short under the parser: the run stopped with "a source
file was made shorter while it was being compiled", or reported syntax errors that the file did not
have. Every file is read into memory again, as on main, and the code that mapped them is gone: 345
lines, among them src/sys's mapping and the SIGBUS case in the crash handler, which plain Bun ran too.
`a-file-that-is-saved-while-the-layers-of-a-test-run-are-built.test.ts` cuts a file shorter every
few milliseconds for a whole run. Found by the review of this pull request.

A package or a test that stores its own resolver, `Module._resolveFilename = f` or
`Module.prototype.require = f` (module-alias, tsconfig-paths, playwright, require-in-the-middle):

- Where a test run is built in layers, as under `--parallel`, a project file that required a project
  file never asked `f`. Where `f` redirects, the wrong file ran, in silence. A layer now tells the
  layers above it that it stores a loader function.
- As one program, a .cjs test file that stored one at its top and then required plain CommonJS files
  of the project stopped with "this executable can't load a module from a dynamic path".

The engine moves to oven-sh/WebKit#942 at 03194c54. What that fixes, with the tests that come here:

- After `import "./shared.mjs"; import "./throws.cjs"` had failed, `shared.mjs` never ran for
  whoever imported it next. The importer got a wrong answer in silence, "Cannot access 'x' before
  initialization", or a segfault where it read through `export * from`. Six programs in
  `parallel/modules/` and a case in `harness/modules-in-a-test-run.ts`.
- `function f() { return console instanceof Object; }` answered false, and as the initializer of a
  class field it aborted. The same for Bun.sql, Bun.postgres, SlowBuffer and node:console.
  `every-built-in-object-is-an-instance-of-what-plain-bun-says.test.ts` asks one question a program:
  several in one program passed on the broken build.
- `while (i < n) { .. n = n + 1; .. }` could abort `bun build --compile --aot`.
- Classes written as ES5 constructor functions ran 2.5 to 3 times slower since the last commit.
- Under ASAN about one validating process in 3,300 stopped with "FrameCellEscaped", in another test
  each time.
- The rest of the leak in `Intl`: 50 bytes in 2 objects, which `no-leak-records-at-exit.test.ts`
  still reported.
…equire, a build that overflowed its stack, and 97 tests back on every lane

The engine stays at oven-sh/WebKit#942 03194c54.

`bun test --aot --parallel`: what a package does to the module loader now reaches the project's own
files.

- A package that takes a file out of `require.cache` by a path it is handed
  (`delete require.cache[filePath]`: import-fresh, clear-module, proxyquire; decache, which walks
  `module.children`) failed the test when the file belonged to the project: "Cannot take
  '../src/dep.js' out of require.cache: the build found nothing that does".
- A package that stores a handler in `Module._extensions` (pirates, @babel/register, ts-node) failed
  it with "Cannot replace a loader in Module._extensions: imports in this executable were resolved at
  build time".

Plain `bun test`, and a run compiled as one program, were right. Only the program that held the
package knew what it does; now it tells the programs built above it.

`require.main` of a created require was `undefined` in an executable:

    const made = require("node:module").createRequire(__filename);
    console.log(arguments.length, typeof made.main, made.main === module);
    console.log(require(process.argv[2] ?? "./" + ["other", "cjs"].join(".")));

printed `5 undefined false`; Bun prints `5 object true`. It took both a file that uses its own
`arguments` and a `require` under a computed name: a module loaded by a computed name was counted as
code the build cannot see.

A `createRequire` whose argument calls what is being made overflowed the stack of the build:

    const load = createRequire(load("node:url").fileURLToPath(import.meta.url));

`bun build --compile --aot` followed `load` to what it is made of, found a call of `load` there, and
went round. Bun throws a ReferenceError at run time, and so does the executable now. One form more
is followed with it: `import url from "node:url"; url.fileURLToPath(..)`.

CI: tests outside test/aot and test/bundler/trim-by-use run on every lane again.
scripts/runner.node.ts splits the compiler's tests across the lanes. The pattern that decided what is
split had grown past those two folders and claimed 97 test files that main runs on every lane:
test/cli/compile, test/cli/aot-*, test/cli/run/run-aot, test/cli/test/test-aot*,
test/bundler/compile-aot-*, compile-trim-*, trim-runtime-icu, test/internal/builtins-table and
test/internal/trim. 45 of them ran on Debian alone, 8 more on no macOS or Alpine lane, all 97 on no
Ubuntu lane. test/internal/trim/link-coff.test.ts links only on Windows, or where it finds a Windows
SDK: in CI it ran nowhere. Now only those two folders are split, and
`tests-outside-the-folders-of-the-compiler-run-on-every-lane` holds that. Some of these files have not
run on macOS, Windows, Alpine or under ASAN for a while, so this commit's CI may show failures that
the split had hidden. Found by the review of this pull request.

The layered runs of modules-in-a-test-run are dealt over five test files instead of four.
…ten faults that the working tree had already fixed

The engine moves to oven-sh/WebKit#942 at 5803df62. What that fixes, with the tests that come here:

- A counted loop could run one turn too many.

      for (let i = a.length - 1; i >= 0; i--) { if (Number.isInteger(a[i])) a.fill(3, i, i + 1); s += a[i]; }

  over a typed array of one element took a second turn: a wrong sum, no error. It took a loop that the
  compiler splits into a fast copy and an unlikely way out of it, where the unlikely way counts for
  itself and comes back to the fast copy's test. Four programs under
  parallel/loops-values-behind-tests/.
- `bun test --aot --isolate` crashed where a test file caught an error thrown by a function of an
  earlier test file.

Executables:

- The text of a function around a function declared in a block did not parse again, a regression of
  this branch.

      function outer() { { function declared() { return 1 } return declared() } }
      (0, eval)("(" + String(outer) + ")")()      // SyntaxError: Function statements must have a name

  Bun and Node answer 1. That is how a function is sent to a worker or a page.
- A JSDoc tag with a dotted name inside a function stopped the build; lunr has them.

      (function () { var lunr = {}; /** @callback lunr.Token~updateFunction */ lunr.x = 1; })();
      // error: TS1235: A namespace declaration is only allowed at the top level of a namespace or module

- `createRequire(__filename).resolve("hides/hidden.js")`, where the package's "exports" hide that file,
  throws MODULE_NOT_FOUND under Bun. An executable built with `--root` above the importer, and run in a
  folder that has node_modules/hides/hidden.js, answered that file: it went by the working directory
  of the run.

`bun test --aot`:

- A file that a plugin loads is the plugin's for `require` too. With
  `b.onLoad({ filter: /\.custom$/ }, ..)` in a preload, a test that both imports and requires
  "./a.custom" got the plugin's module from `import` and the file's raw text, run as CommonJS, from
  `require`.
- `require.cache[require.resolve("./counter.cjs")].children` and `.parent` were undefined in an ES
  module and an empty list in a CommonJS file, so code that clears a module with all below it threw.
- `const { test } = require("bun:test")` counted as the whole of bun:test going out of sight, `mock`
  and `spyOn` with it, so every module of the run counted as mocked. nock 15 did not load: "Cannot
  assign to fetch because built-in objects are read-only in compiled code".
- `mock.module("bun:sqlite", () => made); require(name()); require("bun:sqlite")` answered `made` from
  the second require on; bun test answers the mock's namespace object each time.
- An installed package of test helpers that mocks or spies with what a test hands it failed under
  `--parallel`:

      exports.mockModule = (path, factory) => mock.module(path, factory)   // cannot replace a module that was compiled ahead of time
      exports.spy = object => spyOn(object, "name")                        // Cannot spy on 'name' because module exports can't be replaced

  In practice it is a shim that takes `vi` from "vitest" or `jest` from "@jest/globals", which Bun
  answers with its own test runner. Of eleven such uses nine were wrong under --parallel and none as
  one program; none is wrong now.

CI:

- test/internal/trim/link-coff-freestanding.test.ts timed out on Alpine x64, the first lane to show it
  since the file runs on every lane again. Its two links of 4,000 functions ran beside twenty other
  tests and got a twentieth of the processors: 40 and 46 of their 60 seconds on two processors, 4
  seconds alone. They run by themselves now. No limit is raised.
- Two tests of the compiler ask the system and now run on Alpine, macOS and Windows too: a file saved
  while the layers of a test run are built, and
  every-built-in-object-is-an-instance-of-what-plain-bun-says, which walks Bun's own built-ins, and
  those differ by platform.
- Seven new test files that take over fifteen seconds on some lane run in a process of their own.
- A correction to the last commit's message: link-coff.test.ts skips itself on CI's Windows machines
  too, which lack what it needs, so the split of the lanes took no real run of it away. What the
  change gave back is test/cli/compile on macOS and Alpine, run-aot on Windows, and
  internal/builtins-table and link-elf under ASAN.
…er layers, comments read as code; one fix taken back for Windows

The engine moves to oven-sh/WebKit#942 at 3b261100.

Taken back:

- The last commit's fix for `createRequire(__filename).resolve("hides/hidden.js")` in an executable built
  with --root. On Windows it made relative imports of code from disk fail. main.ts, built with
  --aot --aot-allow-code-from-disk, with plain.js in the working directory:

      await import([".", "plain.js"].join("/"))     // ResolveMessage ERR_MODULE_NOT_FOUND
      require([".", "plain.js"].join("/"))          // MODULE_NOT_FOUND

  for .js, .mjs, .cjs, .ts, .tsx, .jsx and .json alike. The executable built without --aot loads all
  fourteen. `import(path.resolve("typed.ts"))` went on working.
  A name that its importer does not find is looked for once more from the root of the executable, and the
  fix let that second look answer only with what the executable holds. The likely cause, read in the
  sources and not run: on Windows the folder of a module is spelled B:\~BUN\root\ and the root
  B:/~BUN/root/, so a module that lies in the root takes the second look too, and the file that is found
  there in the working directory was thrown away. test/aot/code-from-disk.test.ts caught it on both
  Windows lanes. src/jsc/VirtualMachine.rs is what it was before that commit, and the fix's own test goes
  with it. The fix comes again with both spellings handled.

Executables:

- `globalThis.Map = globalThis.Map || class FallbackMap {};` threw "Cannot assign to Map because built-in
  objects are read-only in a compiled executable". A store of the value that a property already holds
  changes nothing and is let through (`Object.assign = Object.assign || polyfill`), but the value was
  looked for only among the properties that the global object had made by then. Map, Set, WeakSet, Number,
  Boolean, ArrayBuffer and the typed arrays are made at their first read, and compiled code has the class
  without reading it.
- While file.cjs is being required, something calls `import("./file.cjs")`; then the file throws. Bun
  rejects the import with the file's error. Compiled it was fulfilled with what the file had exported by
  then, in six of eight forms. Where the host reads an imported file's text itself (plugins, the compile
  cache) Bun fulfills, and so does compiled code.

Comments are not code (JavaScript files with JSDoc):

- `/** @import { f } from "./nothing.js" */ function f() { return 1; } export { f };` and, elsewhere,
  `import { f } from "./lib.mjs"`: error TS100200: No matching export. With `import * as all` the build
  went through and the namespace lacked the name: Object.keys(all) was [].
- `/** @import Foo#bar */ export const one = 1;` stopped a build. A mistake inside a comment is no mistake
  of the program.
- a.test.cjs: `/** @license MIT */ const path = require("node:path"); test("x", () => {});` threw
  ReferenceError: Cannot access 'test' before initialization. In a CommonJS file that a test requires,
  behind such a comment, `typeof jest` was "undefined" for "object". react-dom's development build asks
  that to find out whether act() applies.

`bun test --aot`:

- A test file written for jest or vitest, without an import of the runner:

      jest.mock("validator", () => ({ isEmail() { return true; } }))
      vi.mock("../src/report.ts", () => ({ .. }))

  Over layers (--parallel): "TypeError: mock(module, fn) cannot replace a module that was compiled ahead of
  time", and every test of the file was lost. With `import { jest } from "bun:test"` it worked. `bun test`
  hands every file the names of the runner, and a build gives such a file the import; the look through the
  test files that tells the layers below what is mocked did not. Two files of xstate's suite: 0 tests ran,
  now 25 pass and 15 fail, which is what bun test says. One of clean-ts-api's: 0, now 3 of 3.
- A dependency's tsconfig.json stopped a whole run over layers. node_modules/pkg/tsconfig.json with
  `{ "extends": "../../not-published/tsconfig.json" }`: "TS5083: Cannot read file" and "None of the 1 test
  files ran". @redocly/openapi-core and @did-plc/lib ship such a file; openapi-typescript's own suite lost
  248 of 248 tests. A tsconfig.json under node_modules is never the configuration of the program. The
  package's own files are still checked by it.
- `require("./m.cjs"); process.stderr.write("x\n")` in an ES module test file failed with "'EventEmitter'
  was not compiled into this executable because your code never calls it", a regression of this branch.
  And a file that a test writes anew and requires again may ask a standard stream for more than the build
  saw (`process.stdout.listenerCount("drain")`).
- `const key = Buffer.from("75726c", "hex").toString(); require.cache[key] = { exports: { parse: 1 } };
  typeof require("url").parse`: bun test says "number"; compiled it threw "Cannot store an entry under 'url'
  in require.cache". The same through a helper in another file, Reflect.set, Object.assign and
  Module._cache. Over layers, a package that read a built-in module under such a key crashed.
- A package that patches fetch or the timers through a helper,
  `registry.applyPatch(globalThis, "fetch", wrap)` (@mswjs/interceptors, under nock), was refused where the
  test file keeps the whole of bun:test (`const runner = require("bun:test")`).

Eight new test files that take over fifteen seconds on some lane run in a process of their own.
…e; a test file's own time limit

The engine stays at oven-sh/WebKit#942's 3b261100.

CI:

- test/aot/the-default-time-limit-never-lowers-what-the-runner-gave.test.ts was red on every lane that
  runs it. A test file of the last commit set its default limit by itself, `setDefaultTimeout(120_000)`,
  under the 270 seconds that the runner gives a Bun with assertions. It says raiseDefaultTimeout(120_000)
  now, as the others do.

Executables, the file of a Worker:

- In a CommonJS file of the project:

      const worker = new Worker(__dirname + "/job.cjs");
      worker.onmessage = event => console.log(event.data);

  The Worker never ran, nothing was printed, and the executable ended with exit 0, a regression of this
  branch. Bun runs it, and so does an executable built without --aot. With an error listener it said
  "Cannot load '<where the file lay>/job.cjs': it isn't in this executable". The same as a template
  (`${__dirname}/job.cjs`), through a variable, with __dirname + "/deeper/../job.cjs" and
  __filename + "/../job.cjs". path.join(__dirname, "job.cjs") was right all along.
  The build takes the file of a Worker into the executable where the path comes from the file's own. It
  told such a path from one that the program simply wrote down by reading the expression twice, as the
  program sees its files and as they lie on disk, and asking whether the two differ.
  `__dirname + "/job.cjs"` had been folded to one constant before anybody saw its parts, so both readings
  agreed. Now the parts of a sum, a template or a variable say where the path comes from. A path that is
  written down whole is still a file of the machine that the program runs on, and what a package in
  node_modules names is still left out.
- `new Worker(__dirname + require("node:path").sep + "job.cjs")`,
  `const { sep } = require("node:path"); new Worker(__dirname + sep + "job.cjs")` and the same in a
  template gave an error event, "Cannot load ..: it isn't in this executable". `sep` of node:path, as a
  member or under a name of its own, is a text that the build knows.
- `new Worker(__dirname + "/none.cjs")`, a file that is not there, in an executable that holds another
  Worker's file: 670 bytes in two allocations were never freed for each such call. Where a Worker is
  created a compiled program asks the resolver twice for the Worker's own name, and what the resolver said
  of a name that it did not find went into a log that nobody dropped. Each of the two questions has a log
  of its own now. Both stand behind "is this a compiled program": Bun without --aot does not reach them.

Test: a-worker-starts-from-a-path-made-of-dirname-or-filename (six ways; the executable runs with its
sources moved away).
The global object adds the name of a class, and its `Bun`, at the first
read of the name, and the engine locks the object then. A program can
come to the object without the name: `instance.constructor`, the
exports of a module, require("bun"). The object was then open. A write
that the build does not follow was made, and the executable ended with
"AOT: built-in cell .. is not in this realm", or compiled code called
the replacement as if it were the host's function.

getDOMConstructor() now stores a new constructor through
Zig::GlobalObject::setDOMConstructor(), which reports it with
JSC::AOT::hostDidCreateObjectOfGlobal() under the class name of the
constructor. The initializer of the `Bun` object reports it the same
way, as the one of `process` does.

Not in this change: the constructor of a Temporal or WebAssembly class
that a program takes from an instance, and a DOM prototype that it
takes with Object.getPrototypeOf(instance). The engine hears of neither
where it is made. The test lists both as rows that are not met.
@robobun

robobun commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:04 AM PT - Oct 31st, 2026

❌ @robobun, your commit c93ba23 has 2 failures in Build #136084 (All Failures):

  • test/js/web/request/request-clone-leak.test.ts - code 1 on 🐧 13 x64-asan
  • 📦 Binary size — 7 over 0.50 MB
  • targetthis build canary: main #135971
    sizeΔ
    ❌ bun-darwin-aarch64117.27 MB115.09 MB+2.18 MB
    ❌ bun-darwin-x64127.57 MB125.09 MB+2.47 MB
    ❌ bun-linux-aarch64134.87 MB132.56 MB+2.31 MB
    ❌ bun-linux-x64133.54 MB131.39 MB+2.15 MB
    ❌ bun-linux-aarch64-musl120.02 MB117.65 MB+2.38 MB
    ❌ bun-linux-x64-musl121.94 MB119.58 MB+2.36 MB
    bun-linux-aarch64-android126.11 MB134.86 MB-8.75 MB
    bun-linux-x64-android133.21 MB141.68 MB-8.47 MB
    bun-freebsd-x64129.76 MB138.38 MB-8.63 MB
    bun-freebsd-aarch64133.02 MB141.80 MB-8.78 MB
    bun-windows-x64142.23 MB142.12 MB+109.5 KB
    ❌ bun-windows-aarch64126.96 MB124.68 MB+2.29 MB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 47545

That installs a local version of the PR into your bun-47545 executable, so you can run:

bun-47545 --bun

@robobun

robobun commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Closed, not merged. main refuses this write since #47316 (a47a315): getDOMConstructor reports each constructor that it makes, and the engine locks it there. #47942 keeps that code and the test test/aot/parallel/host-object-writes/a-static-written-in-evaluated-text-through-an-instance.mjs.

The problem, as reproduced on canary 1.4.4-canary.1+da867c60b, linux-x64, every run:

const u = Bun.pathToFileURL("/a");
globalThis.u = u;
(0, eval)(["u.constructor.canParse", "= () => 'mine'"].join(" "));
console.log(URL.canParse("x:y"));

bun build --compile --aot a.js --outfile a.out && ./a.out ended with AOT: built-in cell 952 (key 4165, URL.canParse) is not in this realm: 9, exit code 134.

On canary 1.4.4-canary.1+7ca0042d3 (main) and on #47942 at ce10cc2 the store throws TypeError: Cannot assign to URL.canParse because built-in objects are read-only in a compiled executable.

PR: #47545

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked two things and ruled them out: removing the non-const constructors() accessor leaves no stale callers (the only other reference in src/ is a commented-out line in ZigGlobalObject.cpp), and Identifier::fromString(vm(), constructor->classInfo()->className) atomizes from an ASCIILiteral rather than a shared StringImpl, so it does not atomize another thread's string in place.

Extended reasoning...

The change touches the DOM constructor cache in src/jsc/bindings/webcore/JSDOMGlobalObjectInlines.h and the AOT lock reporting in src/jsc/bindings/ZigGlobalObject.cpp/.h, plus one new test file; it touches no auth, crypto, or input-parsing surface. The new setDOMConstructor and Bun reports are gated on vm().hasCompiledCode() while the pre-existing process report at ZigGlobalObject.cpp:3154 is not, which is the gap the inline finding covers.

Comment thread src/jsc/bindings/ZigGlobalObject.cpp Resolved
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Closing: #47942 rewrites the front end of the ahead-of-time compiler and its tests for the engine's new compiler (oven-sh/WebKit#997, merged), so this no longer applies and would conflict with it. If the problem still shows once that is in, please open a new pull request against main.

@robobun

robobun commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator Author

Understood. I checked both builds. The problem shows on neither, so there is nothing to open a new pull request for.

  • main (canary 1.4.4-canary.1+7ca0042d3): AOT: bun test --aot on large suites, module loading fixes, fuzzer fixes #47316 took the fix in before it merged (a47a315, didCreateDOMConstructor in getDOMConstructor). In an executable the reported program throws TypeError: Cannot assign to URL.canParse because built-in objects are read-only in a compiled executable, with eval and with new Function. URL.canParse("x:y") then prints true.
  • AOT: front end, test runs and tests for the rewritten compiler #47942 at ce10cc2 (debug build, linux-x64): the same programs give the same answers. The branch keeps test/aot/parallel/host-object-writes/a-static-written-in-evaluated-text-through-an-instance.mjs unchanged.
  • On both builds the write is also refused through the Response of fetch(), the stream of Bun.file().stream(), an AbortSignal, a Temporal.Instant and require("bun"). A store that the file itself writes is still accepted.

One other thing from the run on #47942. The debug build crashed in 7 of 16 builds of a 21-line program, with Stopped while compiling in scan and Link. ASAN reports a read of address 0x8 in WTF::removeIterator (HashTable.h:1638), under LinkFlow::siteOf (AOTLinkFlow.cpp:5988, 5 times) or SetsOfFourInLinkFlow::numberOf (AOTLinkFlow.h:248, 2 times). Both functions release their spin lock before the AddResult of HashMap::add goes out of scope. Without NDEBUG a table keeps a list of its iterators, so the destructor of that iterator races with a rehash on another lane. LinkFlow::findWhatSomeLaneWasGiven has the same shape with the iterator of find. From the source, a release build is not affected. I did not run one.

Program and stacks

bun-debug build --compile --aot f.mjs --outfile f.out, engine autobuild-preview-pr-997-3c42aae2, 12 cores:

const attempt = (label, text, read) => {
  let how = "stored";
  try { (0, eval)(text.join(" ")); } catch (error) { how = `${error.constructor.name}: ${error.message.split(" because ")[0]}`; }
  let answer; try { answer = String(read()); } catch (error) { answer = `threw ${error.constructor.name}: ${error.message}`; }
  console.log(`${label}: ${how} | ${answer}`);
};
globalThis.response = await fetch("data:text/plain,x");
attempt("Response.json", ["response.constructor.json", "= () => 'mine'"], () => typeof Response.json({}));
globalThis.stream = Bun.file(import.meta.path).stream();
attempt("ReadableStream.from", ["stream.constructor.from", "= () => 'mine'"], () => typeof ReadableStream.from([1]));
globalThis.signal = new AbortController().signal;
attempt("AbortSignal.any", ["signal.constructor.any", "= () => 42"], () => AbortSignal.any([]));
attempt("AbortSignal.prototype.throwIfAborted", ["delete", "Object.getPrototypeOf(signal).throwIfAborted"], () => typeof signal.throwIfAborted);
globalThis.load = import.meta.require;
attempt("Bun.pathToFileURL", ["load('bun').pathToFileURL", "= () => 'mine'"], () => Bun.pathToFileURL("/a"));
globalThis.instant = Temporal.Now.instant();
attempt("Temporal.Instant.compare", ["instant.constructor.compare", "= () => 'mine'"], () => Temporal.Instant.compare(instant, instant));
globalThis.headers = response.headers;
attempt("Headers.prototype.get", ["headers.constructor.prototype.get", "= () => 'mine'"], () => new Headers({ a: "b" }).get("a"));
globalThis.blob = await response.blob();
attempt("Blob (constructor.prototype.text)", ["blob.constructor.prototype.text", "= () => 'mine'"], () => typeof new Blob(["x"]).text());
ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008 (READ), thread "Bun Compile 0"
#0 WTF::removeIterator<WTF::HashTable<std::pair<unsigned long, unsigned int>, ...>>  wtf/HashTable.h:1638:37
#1 ~HashTableConstIterator  #2 ~HashTableIterator  #3 ~HashTableAddResult
#4 JSC::AOT::Typed::LinkFlow::siteOf(MakerOfSite, unsigned, unsigned, BuiltinClassID)  aot/facts/AOTLinkFlow.cpp:5988:1
#5 JSC::AOT::Typed::CallInLinkCalls::siteOf  aot/facts/AOTLinkCalls.cpp:4038:19
#6 CallInLinkCalls::addInstanceOf ... #13 CallInLinkCalls::resolve
#14 LinkFlow::resolveCallsThatWereToldIn  #15 LinkFlow::tellInLane  #16 LinkFlow::tellOnLanes()::$_1
#4 JSC::AOT::Typed::SetsOfFourInLinkFlow<FunctionID>::numberOf(std::span<const FunctionID>)  aot/facts/AOTLinkFlow.h:248:5
#5 LinkFlow::typeOfFunctions  AOTLinkFlow.cpp:1025:50
#6 LinkFlow::foldFunctions  AOTLinkFlow.cpp:3046:30
#7 LinkFlow::receiveInLane  AOTLinkFlow.cpp:3176:13
#8 LinkFlow::goAlongEdges()::$_0

When a build succeeds, the executable prints eight lines that each hold TypeError: Cannot assign to or TypeError: Cannot delete, and exits with 0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants