Skip to content

AOT: follow-up to #972: crashes, wrong answers, bun test --aot, build time - #974

Merged
Jarred-Sumner merged 194 commits into
mainfrom
aot-follow-up-1101
Nov 1, 2026
Merged

Jarred-Sumner merged 194 commits into
mainfrom
aot-follow-up-1101

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #972: 194 commits, one for each fix, the fixes for crashes and wrong answers first. Each commit message has the JavaScript that broke, what plain Bun does and what compiled code did. The tests and stored programs for these come with Bun's half.

All but 52 files are under Source/JavaScriptCore/aot. Those 52 are listed at the end, by commit.

Crashes, compiler aborts and builds that were refused

  • A built-in function of a native module that was replaced, redefined or deleted before its first use is refused with a TypeError that names the member, where the executable printed Bun's crash banner
  • A loop that counts down over a list under an unsigned test (do { .. list[i] ..; if (i === -1) break; if (i === 0) break; i--; } while (i > end)) read in front of the list and could segfault. The clamp of loop bounds no longer takes such loops. Bun's main has it too.
  • Reflect.construct() with an heir as its target gave the object the heir's field, and could segfault. Bun's main is right here.
  • The compiler failed or aborted on a loop whose counter has two bounds
  • The compiler aborted on a copy of a list that is kept under a second name and written in the loop that reads it
  • SIGABRT "exception check validation failed" where preload modules were evicted over two layers
  • A function with 14,000 live small objects was refused for its stack frame
  • No pass writes outside a function's tables of values and blocks in a release build
  • "ASSERTION FAILED: m_moduleEnvironment" under bun test --aot in a build with assertions
  • bun test --aot over forked export * with a mocked module at the end was killed at 52 GB
  • vi.spyOn(EventTarget, "prototype", "get") ended the whole test run
  • "Unchecked JS exception" from spyOn() of an export behind two export *
  • A hardened arm64 executable starts where a page has 64 KB
  • A test that a branch narrows by stays in its loop, wherever the branch stands
  • "this assignment always throws" is a build error only if every run reaches the write
  • The compiler's own check stopped the build where an object without a prototype is handed to a parameter declared as a list
  • Three more things ahead of a locked write make "this assignment always throws" a warning
  • "Stopped while compiling" says that an earlier step may be at fault

Miscompilations: an executable at default flags gave a wrong answer without an error

  • Modules that hidden code fetches by name are held, not escaped: new Function('process.getBuiltinModule("node:buffer").atob = ..') was not seen. It replaces an earlier commit of this pull request, which let the objects of all public modules escape and cost a large application 8% of its build. Bun's main is right on all the forms.
  • What a copy writes onto an object of the realm escapes
  • The mark of the deleted prefix is of compact tables alone. Found in review; an earlier commit of this pull request brought it.
  • Lists made by one literal of constants were one cell though structuredClone() tells them apart
  • A copy of a list is read, written through a second name, and read again
  • A store to module.children did not drop the children that were waiting
  • A built-in that has escaped and climbs the prototype chain lets out what stands above
  • SSA repair no longer gathers the edges into a wide join (no program is known to show it; found in review of this pull request)
  • A loader that is out of sight hands out the objects of Bun's own modules (Bun's main is right here)
  • SSA repair gathers the equal edges into a wide join again, and compares them behind the renaming (it was switched off in the second push; it now compares behind the renaming, with a release assertion and a test in IR)
  • Values copied onto an object of the realm escape
  • $<name> in a replacement text reads any name off the groups
  • The answer of a loop's first question has one reader
  • No copy of a hot loop's reads for a list that may hold undefined

Errors in executables: "was left out of the executable" and its like

  • The build warns of names that a copy adds to a built-in that takes no other names: Object.assign(Response.prototype, make()) works where the build can read make(), and is warned of at the build where it cannot
  • JSON.stringify with a list of names did not count as reading those names
  • using h = monitorEventLoopDelay() failed: "[Symbol.dispose] was not compiled into this executable"
  • Accessors copied onto a built-in prototype were left out of the executable
  • globalThis[holder].__defineGetter__(key, f) was refused on console, Intl, JSON, Temporal and WebAssembly
  • new WebAssembly.Suspending() and new WebAssembly.Tag() without an operand gave another error
  • An executable's TypeError for a name that a built-in only inherits gave false advice
  • A name that a compiled module's table lacks is refused in plain's class and words
  • Getters that a bundler's __export lays on an object of the realm escape

bun test --aot

  • A retired realm forgets its unfinished loads: a test file that ended with an import() unsettled kept its realm for good, and a large suite was killed at 60 GB
  • The table of stand-ins holds no realm: typeof Bun["s" + "ql"] in each test file kept each file's realm
  • While a mock stands that left a loaded module alone, the module keeps its own namespace object
  • The state of a held import is handed on, not left in a member. Found in review.
  • An export of another image is kept before a mock is laid over it
  • A file of the project that a mocked package's original requires gets the mock. Found in review.
  • The module object that a require() made goes with the mock
  • The original of a file under a mock has the names behind its stars
  • The required file takes back all that the waiting mock set
  • A second mock factory waits, as the first does
  • The mock of a package ends with its test file
  • What the loader has let go is mocked ahead of its load
  • A mock of a built-in module that a test ends itself goes on for whoever was linked
  • The way to a mock goes on through a table that lacks the name. Bun's main is right here.
  • What lies behind a module that a mock stands for is not asked for its names
  • A helper that mocks a package which the test file comes to through other files
  • vi.doMock() leaves alone what a test file has loaded, through two re-exports too
  • The accessors of a library module that the test runner hands out are entered
  • Every image is told of a file that a plugin's onLoad() filter names
  • The factory of a mock that imports the module it mocks, and fails, was called twice
  • The mock of one test file reached the next
  • vi.spyOn() through a barrel over a mocked module stored the spy where nobody reads it
  • In the second test file that mocks a module, vi.spyOn() reached the first file's mock
  • With a mocks folder, a require() as the first load of a mocked file got the real file
  • The original of a file that had run before a mock was laid over it
  • After a test file resets the modules, the next one finds what ran before the first test file
  • The body of a linked module did not run when a helper mocked it
  • Four walks over "what the loader holds" went through a mocked module to a file it hides
  • A mock that lacks a name which a file of the project hands on
  • spyOn(Response.prototype, "status", "get") was refused though written out in full
  • A name the build left out of a compiled module is named in the error
  • What the test runner hands out is held by native code
  • An import that went on behind mock factories lost its referrer's order
  • A test file stuck on the top-level await of a compiled module was told nothing
  • Any mock of a file cost a closed program a tenth of its likeness to the executable
  • Under a standing mock, a file that was linked before the mock read the mock's values
  • A module that a failed graph left linked runs its body ahead of a mock
  • A test file's load is noted in the layers below
  • The mock of a built-in module outlived its test file
  • A test that ends the mock of a built-in module itself
  • Behind export *, a mock that took the original had no names
  • A mock of a JSON file never ran over layers
  • A mock laid over an ES module that has run adds no name to it
  • vi.mock("node:fs") without a factory failed with "Circular import" in a run compiled as several programs
  • Two imports that wait for one asynchronous mock factory
  • spyOn(Bun.CryptoHasher.prototype, "algorithm", "get") failed in a test run compiled as two layers
  • In a test run a built-in object takes a name that it only inherits
  • Seven spies on a prototype are taken again
  • The image lists the modules that a test may mock and that are loaded under another name
  • require() of a module whose import had been mocked failed once the mock had ended
  • Behind the mock of a built-in module its importers read the module again
  • A mock in the middle of a chain of re-exports answers
  • What a mock replaced behind the stars of its file comes back when it ends
  • A require() answers the original until an asynchronous mock factory has settled

Build time, each quadratic or cubic in the size of one function or program

  • Build time: two new blocks take their parameters without asking who enters them
  • Build time: what a use means to a cell of the frame is asked once
  • Build time: whoever asks through tests holds the tables
  • Build time: what a spread copies is read once for a set of sources
  • Build time: three quadratic steps in SSA repair and the greedy register allocator
  • Build time: the owner of a block's parameter and the cycle of a block are looked up, not searched for
  • Build time: the layouts of the receivers of a key are found once
  • Build time: the tables of a loop live as long as the pass
  • Build time: phi placement skips up the dominator tree
  • Build time: what is copied onto a literal is asked by the set
  • Build time: three quadratic steps of PlaceTypeTests and SimplifyControlFlow in one function
  • Build time: a comparison walks what its arm dominates and asks the graph once
  • Build time: the new objects behind a store are found by one walk
  • Build time: tmps passed on the stack through a loop are priced block by block
  • Build time: an assumption is found by its value
  • A quadratic step in Air's releaseDeadValues
  • Build time: what may change a list's length is looked for once
  • Build time: the parameter of a wide join waits for its sources
  • Build time: a store asks the lengths read above it, the nearest first
  • Build time: what runs each turn of a loop is looked for inside the loop

Size

  • The run time reaches the mocks through a table that the host answers, so that base code holds no pointer to mock code and a trimmed executable drops it
  • The run time reads no overloads: 234,241 bytes of tables that only the compiler reads had come to be kept in executables

Speed of compiled code

  • Two Object.create(target) sites made reads of the target's getters 20 times dearer
  • A release hint kept a list out of the frame
  • Deleting the oldest entry of a Map or Set stepped over every entry deleted before it
  • A read of a copy of a list expects the storage of the list it was copied from
  • A body asks for the room of its array methods with its own

Precision of flow analysis

  • A fused method call declares what its call did
  • An import awaited at once begins behind its call
  • An import awaited through a cast
  • A store to an object that native code holds does not enter setters hidden behind unknown twice
  • Where a generic copy of a loop comes back, a test that narrows gets no other definition

Verifying builds (--aot-validate and the verifier's flags) and lowered limits; default builds do not change

  • The end of a birth between the stores
  • console.log(fs.statSync(".")) stopped a validating build
  • Closures made behind an allocation
  • The verifier stopped a build on a type test hoisted in front of a loop
  • The verifier stopped a build on a union member whose tag no member can hold any more
  • f.apply(x, arguments), inlined, with an unknown callee: the list guards the reads of its own elements
  • A call that Select loses stops the build
  • --aot-validate: a claim inside the value's own type asks nothing
  • --aot-validate: what was there before an object was made is not that object
  • A verifying build goes on after a verification fails
  • "TypeOutOfDate" says which way
  • The check of moved loads prints and goes on
  • A verifying, layered test run stopped with "InvalidType: type 915 is not canonical"
  • An input of the verifier that nobody read is gone
  • The row of a run-time function holds what its operation declares
  • Nine rows of the table of run-time functions say what their bodies write
  • The compiler aborted in ExpandIteration under a lowered limit
  • One visitor names every member of ExpandIteration's arm state
  • The verifier refuses a store that may create a field below an accessor
  • Five counts of the front end become rows of the table of limits

Planted faults, counters, logs and tests

  • A planted fault for what stores behind former entry checks hold
  • The state of a mock, computed and counted at the doors
  • --explain=statistics counts the method calls that are not fused
  • Two IR tests of the counter need Bun's table and move to Bun's tree
  • Four planted faults and a statistic for three passes that decide things at the top of a file
  • Two planted faults for Map and Set lookups
  • Two planted faults with their counters
  • The bounds put on tallies get a planted fault and a counter
  • A planted fault for literals of constants
  • One IR test's hand-written Check made its generic version ill-typed
  • valueflow-solve every-order also applies flow's results under every order
  • The phase log says what an image's tables are made of
  • The objects that native code makes by itself go into both dumps of the table of built-ins
  • The warning for require inside node:vm is back, narrowed
  • Frames of a constant text given to eval or new Function ignored its //# sourceURL=
  • The list of planted faults in compile-call.def is laid out anew
  • Two planted faults and a counter for two rules of flow analysis
  • A planted fault and two counters for lists that a loop leaves alone
  • A counter for the walks over a Map or Set that begin behind the deleted entries
  • A comment says why a wide join begins at 16 edges
  • Three planted faults for what lies in a frame
  • The enums of compile-call.def are generated as five Rust modules
  • The image that runs is called so
  • The schema's generator no longer refuses a long file
  • A planted fault for the copy of a hot loop's reads

What a run without compiled code no longer enters

  • Three calls into the compiler's run time that a run without compiled code made ask first
  • Six more calls into the compiler's run time that a run without compiled code made ask first
  • Five more calls into the compiler's run time that a run without compiled code made ask first

The second of them reaches into three folders that this pull request had not touched, bytecode/RepatchInlines.h, llint/LLIntSlowPaths.cpp and interpreter/StackVisitor.cpp, and into runtime/JSObjectInlines.h (two places), which nearly every file includes: +7 -5 in all, every line inside #if ENABLE(AOT).

The run time

  • The run time stops a program that changes a compiled object in a way the compiler concluded never happens

B3

  • PromoteLoopMemory resets the owners of values, and two checks of assertion builds ask the right thing
  • continuesAfterMovedLoadReports for reportsOfMovedLoadsComeBack
  • Two graphs take their arguments eagerly

Several images in one realm

  • Each image's "is asking the host" is put back to its own
  • Every image is asked before any is told of a copy from disk

Doors for Bun

  • A door for Bun to say whether a program's texts are held, the engine's half
  • A door for the original of a mocked package, the engine's half

Reverted and switched off

  • Revert "A JSON.stringify in a run without compiled code no longer watches levels", a commit of this pull request. With it, in a run without compiled code, JSON.stringify of an object that holds itself, with an indent, took about 0.65 s a call, as in Bun 1.4.3; main finds the cycle early (300 calls in 0.1 s). Every answer was right. runtime/JSONObject.cpp is as on main again.
  • Switch off the clamp of loop bounds. A later commit of this pull request ("don't clamp the bound of a loop that counts down under an unsigned test") has the cause and switches it on again.

Soundness of flow analysis

  • A receiver that leaves may be any object

Builds with assertions or the exception-scope validator

  • "ASSERTION FAILED: moduleData.isESModuleOfProgram()" at mock.module() over a CommonJS file that has run. An earlier commit of this pull request brought it; release builds answer the same with and without.
  • In a build with assertions a value's number says which renumbering handed it out
  • The road of an asynchronous mock factory checks its exceptions
  • The run-time counters ask whether a VM still has instances
  • Three more places look at the exception first

Names

  • Idiomatic names in the B3 files that the JIT shares

Outside aot/

  • runtime/CyclicModuleRecord.cpp, +7, and runtime/AbstractModuleRecord.h, +1 ("the factory of a mock that imports the module it mocks, and fails, was called twice"): seven lines inside #if ENABLE(AOT) on the path of an evaluation that threw. They begin with vm.hasCompiledCode() and act only on an AOT::ModuleRecord. The header gets an inline clearTopLevelCapability(), which only that block calls.
  • runtime/AbstractModuleRecord.cpp, +13 -3 ("vi.spyOn() through a barrel over a mocked module stored the spy where nobody reads it"): two hunks inside #if ENABLE(AOT), each acting only where dynamicDowncast<AOT::ModuleRecord> answers, which is null for every ordinary record. The first also closes the USE(BUN_JSC_ADDITIONS) block one line earlier; what a build without the compiler compiles is the same text.
  • runtime/CyclicModuleRecord.cpp, +8 ("a name the build left out of a compiled module is named in the error"): two hunks inside #if ENABLE(AOT) on the "export not found" path. Both ask globalObject->aotInstance() first, an inline read, so a run without compiled code enters neither.
  • runtime/JSModuleRecord.cpp, +5 (""ASSERTION FAILED: m_moduleEnvironment" under bun test --aot in a build with assertions"): an include, and four lines inside #if ENABLE(AOT) that begin with vm.hasCompiledCode(). An import that resolves to a compiled record has no module environment: the block that fills import slots for a shared executable leaves it out.
  • runtime/JSOrderedHashTable.h, +6, inside #if ENABLE(AOT) ("Deleting the oldest entry of a Map or Set stepped over every entry deleted before it"): a uint32_t m_deletedPrefix { 0 } in the class behind every Map and Set, and its offset. It lies in what the object's size class leaves unused: sizeof stays 32, by a static_assert. Every Bun stores a 0 there at construction; nothing outside the compiler reads it.
  • runtime/ObjectConstructor.cpp +1 -1, runtime/CallData.cpp +3 -1 and runtime/CloneSerializerBase.h +2 -1 ("Three calls into the compiler's run time that a run without compiled code made ask first"): each puts hasCompiledCode() in front of something that stood inside #if ENABLE(AOT) and that a plain run therefore did. No answer of a plain run changes.
  • runtime/JSModuleLoader.cpp +7 -6 ("Each image's "is asking the host" is put back to its own"): fetchSynchronously() calls fetch() in one of two arms. The arm inside if (vm.hasCompiledCode()) raises and restores the flags round it; the other is the one call that stood there.
  • runtime/JSMicrotask.cpp +3 -2, runtime/JSObject.cpp +2 -2, runtime/RegExpConstructor.cpp +2 -1, runtime/RegExpGlobalData.cpp +2 -1 ("Six more calls into the compiler's run time that a run without compiled code made ask first"): each line stood inside #if ENABLE(AOT) and now asks vm.hasCompiledCode() first.
  • bytecode/RepatchInlines.h +2 -1, llint/LLIntSlowPaths.cpp +2 -1, interpreter/StackVisitor.cpp +1 -1, runtime/JSObjectInlines.h +2 -2 ("Five more calls into the compiler's run time that a run without compiled code made ask first"): the same. The first two took a process-wide lock at every "x is not a function".

Shared with the JIT

35 files under b3 and b3/air, in ten commits. B3 and Air are shared with the JIT, so this code also runs in a Bun that has no compiled code. Four of the six remove steps that are quadratic in the size of one function. One renames identifiers and rewords comments that earlier AOT commits added. One adds a boolean, false by default, to a checking mode.

What was run on them: the machine code generated for our corpus of programs was compared function by function before and after each of the four, and is identical. testb3 passes whole in a release build (219,096 cases). testair (807 cases) passes under ASAN with assertions. JSC's stress tests give the same results with and without them.

  • "a quadratic step in Air's releaseDeadValues": b3/air/AirReleaseDeadValues.cpp +4 -0.
  • "build time: three quadratic steps in SSA repair and the greedy register allocator": b3/air/AirAllocateRegistersByGreedy.cpp +51 -13.
  • "build time: phi placement skips up the dominator tree": b3/B3CompilationReport.h +2 -1, b3/B3FixSSA.cpp +164 -14, b3/testb3_bun_thread_jumps.cpp +243 -0.
  • "idiomatic names in the B3 files that the JIT shares": 27 files, names and comments alone: b3/B3CompilationReport.h, b3/B3LikelyPaths.cpp, b3/B3LikelyPaths.h, b3/B3LoopInvariance.cpp, b3/B3LoopRangeChecks.cpp, b3/B3LowerToAir.cpp, b3/B3MemoryNumbering.cpp, b3/B3Policy.cpp, b3/B3Policy.h, b3/B3PromoteLoopMemory.cpp, b3/B3PromoteLoopMemory.h, b3/B3ReduceStrength.cpp, b3/B3UnrollLoops.cpp, b3/B3VectorizeLoops.cpp, b3/air/AirAllocateRegistersByGreedy.cpp, b3/air/AirAllocateStackByGraphColoring.cpp, b3/air/AirFoldTrapsIntoAccesses.cpp, b3/air/AirOptimizeBlockOrder.cpp, b3/air/AirOptimizePairedLoadStore.cpp, b3/air/AirReleaseDeadValues.cpp, b3/air/AirShrinkWrapFrame.cpp; and 6 test files.
  • "build time: tmps passed on the stack through a loop are priced block by block": b3/B3CompilationReport.h +2 -1, b3/air/AirAllocateRegistersByGreedy.cpp +73 -41, b3/air/testair_bun_allocation.h +79 -0.
  • "the check of moved loads prints and goes on": b3/B3MovedLoadCheck.cpp +13 -4, b3/B3Policy.h +1 -0.
  • "PromoteLoopMemory resets the owners of values, and two checks of assertion builds ask the right thing": b3/B3PromoteLoopMemory.cpp +1, b3/B3SwitchValue.cpp +1 -1 (inside an ASSERT), b3/testb3_bun_promote_loop_memory.cpp +17, b3/testb3_bun_vectorize_loops.cpp +9 -4. promoteLoopMemory runs only under the compiler's policy; the JIT's pipeline never runs it.
  • "continuesAfterMovedLoadReports for reportsOfMovedLoadsComeBack": b3/B3Policy.h +1 -1, b3/B3MovedLoadCheck.cpp +6 -6. A rename.
  • "testb3: two graphs take their arguments eagerly": b3/testb3_bun_placement.cpp +1 -1, b3/testb3_bun_selection.cpp +1 -1.
  • "switch off the clamp of loop bounds": b3/testb3_bun_policy.cpp +2 -1, b3/testb3_bun_random.cpp +2, b3/testb3_bun_random_program.h +1. Tests alone: the random policy leaves the clamp off as the ahead-of-time compiler's does, and two seeds become regression cases.
  • "don't clamp the bound of a loop that counts down under an unsigned test": b3/B3LoopRangeChecks.cpp +2, b3/testb3_bun_loop_plans.cpp +175; b3/testb3_bun_policy.cpp and b3/testb3_bun_random_program.h go back to what they were. The two lines stand behind canClampBound, which only the ahead-of-time compiler's policy sets, so the JIT does not reach them. The test builds 576 kinds of counted loop and runs each from starts and bounds at the extremes against a model: 48 fail without the two lines.
  • The last commit of the second push takes out every comment line that this pull request had added under b3, b3/air and runtime: 50 lines in 13 files. No line of code changes.

What was checked

  • It compiles and links in Bun's build graph on Linux x64, with the matching Bun sources.
  • Every source of testaot compiles by itself, without the precompiled prefix that Bun's graph gives it, as this repository's CMake compiles it. The two that do not (saturatingSum in AOTResolveAccesses.cpp, Options in AOTValueFlowAnalysis.cpp) did not on AOT: crashes and miscompilations found since #971, Map/Set index slots, a bounds test on split() #972 either, and get both names from the prefix header that CMake force-includes.
  • All 89 unified sources compile with -mms-bitfields, so the size assertions hold under MSVC's layout of bit-fields; a control with one assertion tightened fails.
  • No added line uses a name that a Windows header defines as a macro.
  • testaot passes whole on a native Linux arm64 build (8,880 tests).
  • Not built at home for macOS, Windows, musl, FreeBSD or Android.

…d its `//# sourceURL=`

A constant text given to an indirect eval or to `new Function` is compiled ahead of time. Its frames were shown
under the URL of the file that evaluates it, whatever directive the text holds:
`at named (file:///$bunfs/root/main.js:1:36)` for `at named (webpack://app/a.ts:1:36)`.

The engine's half. The build reads the directive by the lexer's own rule; the test comes with Bun's half.
…s told nothing

Seven files that each await forever: plain `bun test` prints five `note: unsettled top-level await in ..` and
"and 2 more". A compiled run printed no note.

Bun walks JSC's module registry to find them. The engine keeps the records of an asynchronous graph's compiled
modules outside that registry. `AOT::forEachFileAwaitingAtTopLevel()` walks them by the same conditions.
…ed file got the real file

    jest.mock("./src/old.cjs");                      // src/__mocks__/old.cjs lies beside it
    import.meta.require("./src/old.cjs").who;        // "real old" for "manual old"

No error in a run compiled as one program. Over layers, and for a written `require()`, the result was undefined
and then a TypeError. An `import()` right after it was right.
Whoever asks for the original of a file of the program that has already run now gets what the file exported
before the mock: `module.exports` of a CommonJS file, a new object with the names of an ES module.
So too while the factory runs and nothing is laid yet. For a file that never ran the answer is empty.
…sole, Intl, JSON, Temporal and WebAssembly

With `holder` and `key` out of data, the write was refused as a write to a read-only built-in. A read of a holder
under a key nobody knows notes the member objects it may give, and it noted the host's members alone: `console`
and `Intl` are the engine's entries.

The engine's members are noted only where their holder is the receiver of the read (`globalThis[k]`), not where
the read climbs to it. Every `object[key]` climbs to Object.prototype, and noting there made `Object` and its
methods answers of every such read, so `new contexts[i & 1].Request(..)` no longer knew its callee.
…where nobody reads it

    // dep.js is mocked by vi.mock("./dep.js", () => exports); star.js: export * from "./dep.js"
    vi.spyOn(star, "fn").mockReturnValue("spied");

Plain: `exports.fn === spy`, and dep's and star's `fn()` say "spied". Compiled: false, "fn", "fn". The spy was
stored into dep.js's own variable, which nobody reads while the mock stands.

Outside aot/: runtime/AbstractModuleRecord.cpp, two hunks inside `#if ENABLE(AOT)`, each acting only where
`dynamicDowncast<AOT::ModuleRecord>` answers. What a compiled module's record resolves while a mock stands is
not cached.
…n before the first test file

A test runner hands each test file what ran before the first of them. Where a test file resets the modules, the
next one now finds the instances of that time again, as under plain `bun test`.

Three calls for the host: when the last preload has run, for each file that a reset takes out, and between two
test files, which names each file that is back.
    // linked.test.ts
    import { price } from "./mocks-tax";     // mocks-tax.ts: vi.mock("./tax", factory)
    import * as tax from "./tax";

Under `bun test` the graph is loaded before any module of it begins, so tax.ts is linked when the helper mocks
it. Plain runs its body there and then, once, and lays the mock over it. Compiled code now does the same.
…s were evicted over two layers

Giving back the preloads held a throw scope of its own. A throw scope that ends asks whoever comes next for a
check, and after an early return that was the caller's own scope. It is a top exception scope now, asked after
each of the two calls, and `AOT::bringBackKeptFiles()` asks once more after its walk over the instances.
…ack frame

    function big() { let v0 = c ? { a: 0 } : { a: seed }; /* .. 14,000 of them, all alive .. */ }

`error: TS100057: 'big' (f1) needs a stack frame of 1120000 bytes, over the limit: MaxFrameSizeInBytes`. Stock Bun
runs it. The limit stays. What filled the frame was objects allocated in it: a cell's stack slot is shared with
nothing, so the cells take the sum of their sizes. The planner now stops putting cells into a frame at the room
the frame has.
…the first file's mock

a.test.js and b.test.js each `vi.mock("./dep.js", () => exports)`. In b, in a run compiled as one program,
`vi.spyOn(ns, "fn")` on dep's own namespace object or through a star left `exports.fn === spy` false, made the
first file's `exports.fn === spy` true, and `fn()` was not spied. Plain and a layered run were right.

A record made for a search alone holds no status, so the end of a mock never let it go.
…in a release build

`Function::at()` and `edit()` index those tables through a span's data. The bound was asked under assertions
only, so in a release build a pass that held a stale number wrote outside the allocation and went on. The
compiler now stops on such a write. Reads stay with assertions.
A module object reached through `module.require`, `import.meta.require` or `createRequire()()` in a test run is
handed out by the runner, not by code the build sees. Flow analysis now takes such a module's namespace and
object as held by native code. One field of the program's records says which modules those are.
…dule to a file it hides

lib.mjs imports hidden.mjs; `mock.module("./lib.mjs", () => ({ v }))`; `await import("./main.mjs")`, which imports
lib. A file that only a mocked module imports is not loaded, yet:

- `Bun.plugin({ setup(b) { b.onLoad({ filter: /hidden\.mjs$/ }, ..) } }); (await import("./hidden.mjs")).e` gave the
  file's value for the plugin's, and `require.cache` listed hidden.mjs.
- Three more forms on the same walks.

No error. `bun test --aot` alone. main has these too.
…se names

    class B { constructor(x) { this.x = x; } get size() { return this.x + 1; } }
    console.log(JSON.stringify([new B(1)], ["x", "size"]));

threw "TypeError: 'size' was left out of the executable" for `[{"x":1,"size":2}]`. Without an error: a getter that
stores was not seen to store, and `C.prototype.q = { toJSON() { return "tj"; } }` gave `[{"q":{}}]` for
`[{"q":"tj"}]`. One row of the table of built-ins.
…er can hold any more

`type A = { tag: "a" }; type B = { tag: "b" }`, a member made in the module body whose tag only typing knows to be
"z", and `if (x.tag === "b") { kept = x; }`: "verification failed after NarrowConstantVariables .. TypeOutOfDate".
…cturedClone() tells them apart

    const initial = { rows: [0, 1, 2].map(() => ["", "", ""]) };
    let state = structuredClone(initial);
    state.rows[0][0] = "x";

gave [x][x][x] for [x][][]. No error, default flags. main has this bug too.

All lists that one literal of constants makes become one cell where flow analysis says that they never change
and are never told apart. A built-in counted as telling apart only an operand that is itself a list. What an
operand reaches now counts too.
… order

`Modules::startImport()` parked an import behind factories with the module and the caller's kind alone, and
resumed it without the referrer's asynchronous order and with the default text of the import. Every other path
in the file carries both. Found in review. The hang it could have caused was not reproduced.
…s not compiled into this executable"

    import { monitorEventLoopDelay } from "node:perf_hooks";
    { using h = monitorEventLoopDelay(); h.enable(); }

Eight of twelve ways failed: `using`, `await using`, `const d = h[Symbol.dispose]; d.call(h)`, a pattern,
`Reflect.get` on the object and on its prototype, `.bind(h)()`.

A function of the host that a lookup finds through an object nobody knows is marked as taken off its holder.
The mark is for functions written in JavaScript alone: marking every one kept six more features under `--trim`
in an elysia server.
…st guards the reads of its own elements

The list that `apply` makes guards the reads of its own elements by its birth, so the verifier no longer stops
such a build.
In the table of what each pass answers for, Select answered for every operation that goes away and might take
every handler away. It now answers for 11 opcodes, each read in the pass, and may take the handler from a loop
hint alone.
definition-of-a-field-that-takes-any-value-in-a-proven-layout.ir: `Check v3 expectedType=L0` becomes
`RequireObject v3`. What the test is about, the definition of the field, is as it was.
`found().map(part => part.size + i).length`: the list that `found()` makes lay in the frame and cost no cell, until
a hint that names it was taken for an escape: one cell of 48 bytes more a turn. A hint never counts against the
frame now.
…d again

The second read gave a wrong value. An executable, default flags, no error. main has this bug too.
    // x.test.mjs: import * as u from "./user.mjs"; mock.module("./lib.cjs", () => ({ v: "mock v" }));
    //             user.mjs takes lib.cjs by createRequire()(..)
    // z.test.mjs, which mocks nothing:
    import * as ns from "./lib.cjs"; ns.v;           // "mock v" for "real v"

So too for `(await import("./lib.cjs")).v`. `import whole, { v } from "./lib.cjs"` failed with "SyntaxError: Missing
'default' export in module".
MethodCallsNotFused is the number of places in a program that held the shape of the stale-variable bug fixed in
#972. It is 0 in eight servers (express, fastify, hono, koa, elysia, Bun.serve, node:http, a thin
one).
…rror

Code that the loader reads from disk (the original of a mock, a file taken out of `require.cache`) links to
compiled modules by their tables, and the build trims a table to what something names. An import of a trimmed
name failed with "export 'x' not found". It now says that the name was left out of the executable.

Outside aot/: runtime/CyclicModuleRecord.cpp, two hunks inside `#if ENABLE(AOT)` on the "export not found" path.
Both ask `globalObject->aotInstance()` first, so a run without compiled code enters neither.
…ounds

    function f(list, o) {
      const n = o.n; let i = 1;
      while (i + 1 < n) { if (i > list.length) break; i += 1; }
      return [i];
    }

called with n 2147483656 and with 3: "internal compiler error (ChooseRepresentations): UnsupportedConversion". With
`t += list[i]` in the loop the compiler aborted. Of 96 such plain loops 52 did not build, 8 by an abort. main has
this bug too.

Where `i + 1` is tested ahead of the step, the turn handed on what that test let through in place of the step.
But the step stood behind a second test of the counter.
To say what each place holds where paths meet, the joins of a block were filled from each later predecessor by
going again through every name that changed. They are now filled by the shorter of two ways.

Outside aot/: b3/air/AirReleaseDeadValues.cpp, +4, inside `#if ENABLE(AOT)`.
a.test.js: `mock.module("esm-dep", ..)`. b.test.js mocks nothing. src/uses.js and node_modules/reads/index.js both
`import { value } from "esm-dep"` and call `require("esm-dep").value()`. In b.test.js, inside `reads`: plain gives "real",
compiled over layers gave "mocked", without an error.
`runBodyOfRequiredFileAheadOfMock()` took the file out of the set of mocked files and left two other sets as they were,
so until the mock was laid the file was in no state of the table of a mock's states. No answer is known to change.
leaf.cjs: `exports.x = "real x"`. barrel.js: `export * from "./leaf.cjs"; export const own = "own"`. The test has imported
barrel.js, then `mock.module("./barrel.js", () => ({ x: "mock x" }))`: `(await vi.importActual("./barrel.js")).x` was
undefined and its keys were `own`; plain gives "real x" and own, x. So too for the parameter of a factory, Vitest's partial
mock.
An `import()` that is held behind an asynchronous mock factory went on through a function that left the factory's state
in a member; `startImport()` took it only behind calls that run script, and behind three early returns. It is the last
parameter of `startImport()` now. Found in review of #974. The test, in Bun, says "never settled" without it.
…used

A built-in function of a native module that was replaced, redefined or deleted before its first use is refused with a
TypeError that names the member, where the executable printed Bun's crash banner.
Under `bun test --aot` a test file that ended with an `import()` unsettled kept its realm for good. A large suite was
killed at 60 GB. Run time alone; no generated code changes.
The commit of #974 that let the objects of all public modules escape where a loader has escaped cost a large
application 8% of its build and 37% of the accesses in dependencies that were proven without a test, and it did not fire
for text that sees globals alone: `new Function('process.getBuiltinModule("node:buffer").atob = ..')`. They are held by
native code now. Bun's main is right on all the forms; eighteen stored programs are in Bun.
barrel.js says `export * from "pkg"`; a test calls `mock.module("./barrel.js", () => ({ x: "mock x" }))` after the barrel has
been loaded. Plain `bun test` stores the mock's values into the package's variables, and the next test file reads the real
ones again. Under `bun test --aot`, where a large run splits into layers, the mock was ignored: "real x". Bun's half tells
the layer that holds the packages. The store then reaches a variable of another image, whose modules know nothing of the
mock, so nothing was kept to give back at the mock's end: that is this half. The two go together.
…keeps its own namespace object

    import * as ns from "./dep.js";
    test(.., async () => {
      vi.doMock("./dep.js", () => ({ flag: "mock", added: 1 }));
      const mocked = await import("./dep.js");
      JSON.stringify(ns); ns.other; Object.keys(ns); mocked === ns;
    });

Plain: `ns` stays the module's: {"flag":"real"}, undefined, its own names, false. Compiled code had one namespace object
for both. The schema of one operation gains two flags, so Bun's half goes with this.
…ng handed it out

A ValueID carries, in its six top bits, which renumbering of its function handed it out; `Function::at()` and `edit()` stop
the build on a number from before the last one: "A number of a value from before a renumbering: v18 was handed out at
renumbering 2, the function stands at 3 and has 15 values". Builds with assertions alone.
A verifying build stopped where `FinishBirth` is laid between the stores behind a late allocation. In AOTGuardFacts.cpp,
behind "closures made behind an allocation". Default builds do not change.
…wers

Base code holds no pointer to mock code. `struct MockOperations` is a table of functions; ModuleHost has a new last
member, `mockOperations`, null in a host without mocks, and Modules' constructor asks the host once. Bun's
half goes with this: the host's table has one member more.
…who enters them

`for (let k = 0; k < a.length; k++) s += a[k];` many times in one function: a second quadratic cost in
SeparateUnlikelyLoopPaths. With a statistic, BlocksReadToBuildControlFlowGraphs. No generated code changes.
…es no other names

`const make = () => ({ upper() { return "up"; } }); Object.assign(Response.prototype, make()); new Response("").upper()`: plain
gives "up"; the build said nothing and the run threw "Cannot assign to Response.prototype.upper because built-in objects
are read-only". So too `Object.defineProperties()`, and Request, Headers, Blob, URL. Bun's half goes with this.
…dule()` over a CommonJS file that has run

A build with assertions aborted, exit 134. An earlier commit of #974 brought it. Release builds answer the same
with and without this. Three lines.
`typeof Bun["s" + "ql".slice(0)]` at the top of each of eight test files; at each file's end `Bun.gc(true)` and the count of
GlobalObject in `heapStats()`: plain 1 2 2 2 2 2 2 2; `bun test --aot` 1 2 3 4 5 6 7 8.
@Jarred-Sumner
Jarred-Sumner merged commit 5b150c0 into main Nov 1, 2026
87 checks passed
Jarred-Sumner added a commit that referenced this pull request Nov 1, 2026
* AOT: the key of a `for in` stored into a field crashed a very large executable

A crash at default flags in an executable above 4,194,304 values, and in test runs over layers. Bun's main has it too. In
AOTStringPasses.cpp. Two stored programs are in Bun.

* AOT: build time: two more quadratic costs under the limit on tested types

Behind each copied range of a block, PlaceTypeTests read the block's values one by one for the first point where the copy
may come back: ranges times values. And the greedy register allocator added up the moves on every edge of a loop though
the sum had passed the bound. No output changes.

Shared with the JIT: b3/air/AirAllocateRegistersByGreedy.cpp +14 -6. `forEachEdgeToWhereLive()`'s functor answers
IterationStatus, and its one caller ends the walk as soon as the sum has reached the bound. The costs are not negative.

* AOT: four more functions of the mocks behind the table

Behind "the run time reaches the mocks through a table that the host answers". Every name and call site stays; the bodies
move behind the table. No behavior changes.

* AOT: a read off a namespace object goes past no mock

leaf.js: `export const x = "real x", y`. barrel.js: `export { x, y } from "./leaf.js"`, loaded.
`vi.doMock("./barrel.js", () => ({ x: "over x", y: "mock y" }))`. barrel2.js: `export { x, y } from "./barrel.js"`, first loaded
now: `const m = await import("./barrel2.js")`. Plain gives "over x" however it is read; compiled code did not for `m.x`
and for a destructuring parameter. The raw output of four servers is identical.

* AOT: a deletion or a new prototype on a module's object through an unknown receiver is seen

* AOT: the mock of a module that is linked below mocks alone waits

l imports k imports j imports i. `vi.doMock('./k.js', ..); await import('./l.js'); vi.doMock('./j.js', () => ({ y: 'mock-j' }))`:
the mock of j never ended: behind `vi.doUnmock('./j.js')` an `import()` still answered "mock-j".

* AOT: the verifier's net answers for nothing behind three more passes

The rows of ConvertCountersToIntegers, RetypeValues and SplitUnions go out of the table of operations that a pass answers
for. Each let every operation through. Verifying builds alone.

* AOT: a mocked file of the program below counts

barrel.js: `export * from "pkg"; export * from "./clash.js"`, x in both. `vi.mock("pkg", () => ({ x: "mock x", f }))`.
`import * as ns from "./barrel.js"`: plain, and a run compiled as one program, give "mock x": the first star that has the name
gives it. Over layers, where the package lies in a program below the barrel's, it gave "clash x", without an error.

* AOT: refusals are counted where they are made

With `BUN_JSC_aotCountsPath` set, every error object whose sentence is one of the compiler's refusals is counted where it is
made, caught or not. A suite can pass while a library catches the refusal and takes another road.

Outside aot/: runtime/ErrorInstance.cpp +5, inside `#if ENABLE(AOT)` in `ErrorInstance::finishCreation()`. The option is asked
first, then `vm.hasCompiledCode()`: without the option a run reads the option and goes no further.

* AOT: a stand-in refuses a write

`import n from "node:fs/promises"; new Function(text)(); n.access("/")`, where text, out of the build's sight, is
`process.getBuiltinModule("node:fs").promises.access = () => "mine"`. So too `defineProperty`, `delete` and a new name. A
write to the stand-in of a member that was left out is refused.

* AOT: a built-in function is known by the name it was made with

A test file: `vi.spyOn(fetch, "preconnect").mockReturnValue("spied")`; a product file: `fetch.preconnect(url)`. Plain, and a run
compiled as one program, give "spied"; over layers: "TypeError: Cannot spy on 'preconnect' of a built-in function". So too
`Bun.fetch` and `globalThis.fetch`.

* AOT: the verifier's net answers for one opcode behind two more passes

The rows of EliminateTailRecursion and StartGeneratorsAtFirstStep answered for every operation that goes; now for
CallDirect alone. Only what `--aot-check=verify-each-pass` judges; no generated code changes.

* AOT: a reference to a value is not held across the insertion of a new one

`sayWhatStoresBehindFormerChecksHold()` in AOTEntryChecks.cpp held a `const Value&` into the function's values while it
inserted claims in front of the user. A claim is a new value, and the values move when one is added: a use after
reallocation in the compiler. The reference is fetched anew for each operand. An earlier commit of this pull request brought
it; found in review. No known program reads through the stale reference.

* AOT: the throw scope stands in the two arms of tryReplaceExport() that need it

`spyOn()` of an export that is not replaceable answers with a TypeError. Under JavaScriptCore's exception-scope validator the
compiled run ended by SIGABRT: "This scope can throw: tryReplaceExport .. unchecked as of this scope: spyOn". An earlier
commit of this pull request laid a throw scope over the whole function. It shows only with the validator on, as on Bun's
ASAN lane.

* AOT: a type test is typed by the operand it has

    function f(x, z) { let d = x; if (z === 1 && d < x) { } else { d = x; } return ((x + d) | 0) + (d | 0); }

A type test that was made over a value tested on one path only kept its answer when it was handed the value of every
path. On Bun's main the executable crashes on `f(1.5, 1.5)`. With a planted fault and a statistic.

* AOT: a mock that has ended stays for whoever was linked against it

`vi.doMock("./dep.js", f1); const { seen } = await import("./user.js"); vi.doMock("./dep.js", f2); seen()`: plain gives the
first mock's value, until something loads the module again. Compiled code gave the second's, without an error, and behind
"a second mock factory waits, as the first does": "Cannot access 'flag' before initialization".

* AOT: the compiler crashed on a RegExp made of `Object.keys()` of a literal, in a file required inside a `try`

    var table = { theme: b };
    function b(r) { r = c(r); }
    function c(e) {}
    exports.pattern = new RegExp(Object.keys(table).map(e => `${e}\\(`).join("|"));

`let m; try { m = require("tailwindcss"); } catch (e) {}` (tailwindcss 4.3.3): `bun build --compile --aot` ended by SIGILL:
"Stopped while compiling in CollectRegExpLiterals". Bun's main builds it. The list of what `Object.keys()` of a literal
answers asked for the key of every field of the literal's layout, and a field of a layout need not have a name. Such an
object is left alone now: the pattern is made at run time.

* AOT: a pass that has cut blocks says so

PlanArrayCapacities answered "unchanged" with blocks cut: it cuts the region of reads out and has two ways out behind the
cut that make no copy, and its caller counted a change only where there was a copy. The runner goes by that answer. With a
test in IR.

* AOT: the flow graph's helpers each take at least eight indices of a wave

Build time alone; no output of the compiler changes.

* AOT: the verifier's net answers for the accesses that ResolveAccesses declares

Its row answered for every operation that goes; now for the set that the pass declares for itself, by which the runner
decides whether it runs at all. Only what `--aot-check=verify-each-pass` judges.

* AOT: the verifier's net answers for the walks that ExpandOwnKeys expands

Its row answered for every operation that goes; now for the walks over own keys that the pass declares and expands:
CopyDataProperties, CopyObject, CallBuiltin. Only what `--aot-check=verify-each-pass` judges.

* AOT: the refusal of a changed built-in has a test that can fail

One condition in AOTValueFlowRulesHost.cpp on a planted fault that is there already. No output changes at default flags. The
test is in Bun.

* AOT: the namespace of a built-in module takes own properties alone

Behind "modules that hidden code fetches by name are held, not escaped": what flow analysis answered for one stored program
depended on the order in which its rules ran. One line.

* AOT: the verifier's net answers for what PlanArrayCapacities and ResolveAwaitedCalls work on

Their rows answered for every operation that goes; now for the seven operations that PlanArrayCapacities names as appends,
and for AwaitBySavingFrame, CallBuiltin and CallDirect behind ResolveAwaitedCalls. Only what `--aot-check=verify-each-pass`
judges.

* AOT: what the next test file sees of a file that stars what was mocked

barrel.js: `export * from "pkg"`, and pkg is mocked before the load by `vi.mock`. a.test.js and b.test.js both import user.js,
which reads barrel.js four ways. In b.test.js `require("./barrel.js").x` was undefined and `require("./barrel.js") === ns` false;
plain gives "real x" and true. With an ES package the value was right and the identity false.

* AOT: a user that is imported anew behind a mock reads the mock

dep.js and two users of it are loaded; `mock.module("./dep.js", ..)`; then `vi.doMock("./dep.js", () => ({ flag: "three", .. }))`;
then `(await import("./user-late.js")).values()`: plain gives three, three, three; compiled code gave real, real, real, without
an error.

* AOT: a module that is owed to an importer runs though the importer is mocked meanwhile

r.js imports m2.js imports x.js. Test file 1: `vi.doMock("x.js", ..)` ahead of its load, `await import("r.js")`: r and m2 run,
x's body doesn't; the mock ends. Test file 2: `vi.doMock("m2.js", ..)`, which has run, then `require("r.js")`. Test file 3:
`require("r.js")`, `r.read()`: plain gives "real x" and x ran once; compiled: "ReferenceError: Cannot access 'value' before
initialization", and x ran 0 times. Found in review of #974.

* AOT: over layers, the second of two imports that wait for one asynchronous mock factory failed

`mock.module("./lib.mjs", async () => (await later(), { v: "mock v" }));
await Promise.all([import("./user.mjs"), import("./other.mjs")])`, both import lib.mjs: "Cannot access 'v' before
initialization". Plain, and compiled code as one program, hand both the mock. Bun's main has it too over layers.

* B3: only pointers from outside may be promised dereferenceable

B3's validator refuses a promise of dereferenceable bytes on a value that is computed from others, as it already does for
known ranges; under B3's default policy no promise is ever stored, so the new line is never reached in the JIT.

Shared with the JIT: b3/B3Validate.cpp +3 -1, b3/B3KnownProperties.h, and two testb3 files.

* AOT: the table of the states of a mock, second edition

What the observer of a mock's states reports, not what a mock does: "left to the host" is said beside the body's status, and
two states are new: `in place while another waits` and `fills`.

* AOT: a file that was linked before `vi.doMock` of a CommonJS file keeps the real values

old.js: `import { who } from "./m.cjs"`; the test: `import { read } from "./old.js"; vi.doMock("./m.cjs", () => ({ who: "mock" }));
read()`. Plain: old.js keeps `real`; import() and require() by name, and a file linked behind the mock, get the mock; the factory
runs at the first load that comes to it. Compiled: the factory ran at the call and its values were laid over the copies that ES
importers read, so everybody read the mock, and the namespace object kept the file's names. Without an error.

* AOT: a package behind two files that only hand names on runs for the next test file

node_modules/pkg/index.js: `export const x = "real x"`; src/inner-names.js: `export { x } from "pkg"`; src/names.js:
`export { x } from "./inner-names.js"`; src/uses.js imports x and the namespace of names.js. a.test.js mocks pkg before the load;
b.test.js mocks nothing and imports src/uses.js. Plain: `real x`; compiled: "ReferenceError: Cannot access 'x' before
initialization". With one file between it was right.

* AOT: over layers, a mock of a module that the loader had let go was not seen

reader.mjs: `import { ofBody, real } from "./above.mjs"`. a.test.mjs mocks leaf.mjs and imports reader.mjs. b.test.mjs:
`mock.module("./body.mjs", () => ({ ofBody: "mock of body" })); (await import("./reader.mjs")).read()`. Plain:
`mock of body undefined`; compiled, over layers: `of body real`. As one program it was right. Behind "what the loader has let go
is mocked ahead of its load".

* AOT: a name that a mock gives ahead of the load of a compiled module makes nothing ambiguous

A barrel that re-exports the same name from two modules lost that name under `bun test --aot` once one of them was mocked ahead
of its load; plain `bun test` gives it the first module's, as Vitest does. A mock laid over a module that has already run leaves
the name ambiguous, as in plain.

barrel.js: `export * from "./clash.js"; export * from "pkg"`, x in both; `vi.mock("pkg", () => ({ x: "mock x", f: "mock f" }))`;
`import * as barrel from "./barrel.js"`. Plain: `x=clash x`, keys f, other, x. Compiled: `x=undefined`, keys f, other, without an
error. Over layers, with the package, the next test file: plain `x=undefined`, ambiguous again; compiled `x=clash x`.

Outside aot/: runtime/AbstractModuleRecord.cpp, inside `#if ENABLE(AOT)`, in ResolveExport's merge: one read of
vm.hasCompiledCode(); the rest is a function of its own that a run without compiled code never calls. On 40,000 ambiguous names a
plain run's instructions are level.

* AOT: a user that is required anew behind a mock reads the mock

As "a user that is imported anew behind a mock reads the mock", but the user is required:
`import.meta.require("./late.js").values()`, behind an import() of the mocked module. Compiled code gave real, real, real,
without an error.

* AOT: a package that imports a mocked ES module keeps the real one

node_modules/reads/index.js: `import { value } from "dep"; export const seen = () => value();`; the test:
`import { seen } from "reads"; vi.mock("dep", () => ({ value: () => "mocked" }))`. Plain: `real`, as Vitest leaves packages as
they are; compiled: `mocked`.

* AOT: the asynchronous mock factories of one load take turns

`vi.doMock("./k.js", async () => { say("k begins"); await later(40); say("k ends"); return { k } });
vi.doMock("./m.js", async () => { .. later(5) .. }); await import("./a.js")`, and a.js imports k and m. Plain: k begins, k ends,
m begins, m ends; compiled: k begins, m begins, m ends, k ends. Also a factory that answers at once between two that wait,
factories at different depths, and `vi.mock` below a statement of the test file.

* AOT: with assertions on, the reader makes no checked number out of an unchecked word, and two tests use numbers that a stamped number can hold

Behind "in a build with assertions a value's number says which renumbering handed it out": four native tests ended by an assertion in every build with
assertions. A release build compiles the same code as before.

* AOT: a native test calls its operation from an entry frame

compact-table/ordinary-table-has-no-prefix called operations that are no leaves straight from C++ and ended by an assertion in
builds with assertions. A test alone.

* AOT: a trimmed executable keeps the run-time operations that its image lists

The run-time table takes the address of all 518 operations that compiled code may call, so each stood in every executable with
all that it reaches, though an image lists the entries that it uses. 356 operations are now in 39 families by what they are for
(proxies, typed arrays, private names, import(), maps and sets, object spread, generators ..), each a feature that only the list
of an image keeps. An operation that nobody names there stays in every executable, so whoever adds one need do nothing.

Goes with Bun's src/trim/features/engine.def: the two lists must agree, or neither builds.

* AOT: the original that a mock factory asks for brings what is owed below it

a.test.mjs: `mock.module("./leaf.mjs", () => ({ leaf: "mock leaf" })); await import("./first.mjs")`. b.test.mjs:
`vi.doMock("./body.mjs", async importOriginal => ({ ...(await importOriginal()), ofBody: "mock of body" }));
await import("./above.mjs")`, body.mjs > dep.mjs > leaf.mjs by `export *`. Plain passes; compiled: "ReferenceError: Cannot access
'leaf' before initialization." out of the spread.

* AOT: build time: the exit of an integer update in memory is kept where it is found

In updateIntegersInMemory() each update that is applied invalidates the graph of blocks, and the next one asked for the exit that
dominates it, so B3's dominators were made anew for each update, to find an exit that the search for updates had already found:
updates times blocks. An update now holds each user with its exit, taken while the dominators hold. x86-64. No output changes.

* AOT: over layers, `vi.doMock` reaches a module that an earlier test file loaded by import()

dep.js: `export const value = "real"`. subject.js: `import { value, get } from "./dep.js"; export const f = () => [value, get()].join()`.
f1.test.js, no mock: `const { f } = await import("./subject.js")`. f2.test.js:
`vi.doMock("./dep.js", () => ({ value: "mocked", get: () => "mocked" })); const m = await import("./subject.js"); m.f()`.
Plain, and compiled code as one program: `mocked,mocked`; over layers: `real,real`, without an error.

* B3: what foldPathConstants rewrites by a fact of one path forgets what was promised about it

foldPathConstants replaces an operand by what a branch proved on one path. A value changed so kept the facts the compiler was told
about it ("these bytes can be read", "it lies in this range"), which are meant to hold wherever the value is computed; hoisted off
that path, a load from it could read address 0. Now such a value, and what is computed from it and may move, forgets those facts.
No compiled program changes: identical machine code on 408 programs, 1.37 million functions.

Shared with the JIT: b3/B3FoldPathConstants.cpp. All hunks but one lie inside our own class FoldPathConstantsInValueOrder, which
the JIT never makes; the one on the JIT's road adds `|| !proc.knownProperties().isEmpty()` to the condition in foldPathConstants()
that chooses the class: under B3's default policy setKnownRange() and setKnownDereferenceableBytes() store nothing, so the table
is empty, the condition reads as before and upstream's class runs.

* AOT: under `--aot-check=verify-each-pass`, SinkAllocations says why a test for an object goes

`for (const n of source) if (n === 4) break;` beside a module with `class Pretender extends Iterator`: "verification failed after
SinkAllocations; v49 (RequireObject) in b18 may throw or run code, and is gone". It could not have thrown: the pass lets a cast go
only where every variant of an object that it holds passes it. It now says so. A build without the flag compiles what it compiled.

* AOT: under `--aot-check=verify-each-pass`, a pass that answers "unchanged" has changed nothing

What a pass answers decides what the runner does behind it: it drops analyses and gives other passes work only behind "changed".
Where every pass is verified, a run that answers "unchanged" is now held against the body as it was. One pass over strings that
the rule still accused is cured. With a planted fault, PassesChangeNothing, and its witnesses in Bun. No function's machine code
changes.

* AOT: a mock without a factory is made of what the original exports, with what is owed below it

a.test.mjs: `mock.module("./leaf.mjs", () => ({ leaf: "mock leaf" })); await import("./first.mjs")`. b.test.mjs:
`vi.doMock("./body.mjs"); (await import("./body.mjs")).leaf`, body.mjs > dep.mjs > leaf.mjs by `export *`. Plain: `real leaf`;
compiled: `undefined`, without an error. A sibling of "the original that a mock factory asks for brings what is owed below it".

* AOT: the key of a `for in` over number keys and computed names crashed a very large executable

Behind "the key of a `for in` stored into a field crashed a very large executable", which missed every name that is neither a
field's key nor an operation's: an index (`{ 200: .. }`), "true", "null", "undefined", NaN, Infinity, a computed name. A crash at
default flags in a very large executable; on Bun's main a wrong answer without an error. A member of a type that the program
hands on and nobody spells is no longer left out of the table of texts. 33 more stored programs are in Bun.

* AOT: two modules that import each other, both loaded by import() calls in one turn, failed in a test

Two modules that import each other, both loaded by `Promise.all([import("./a.mjs"), import("./b.mjs")])` in a test, failed with
"Cannot access 'fromB' before initialization". The change that ordered two held import() calls by their last shared file is taken
back. Three lists of known differences in Bun go with it.

* AOT: test files that the front end has read are no hidden text

With test files run from disk, an export that no test file takes, on a file that a test spies on, was readable where the run
refuses it ("Cannot read the export 'usedByNobody' of .."). Behind "modules that hidden code fetches by name are held, not
escaped": the imported `spyOn` also held the unknown object, so the namespace that it was handed escaped, and every export with it.
robobun added a commit that referenced this pull request Nov 2, 2026
#974 to #983 changed how a compiled import() waits and is made again. Three files of aot/runtime had conflicts.

- TextThatWaits has the new fields of main and then the async context of the call. makeCallAgain() installs that async context around startImport(), and the steps that count the jobs behind the first call carry it.
- The counted import that main adds for the loader keeps the async context of the call, and its job carries it.
- The job of a round belongs to no import() and has no async context.
robobun added a commit that referenced this pull request Nov 2, 2026
…xt of its call

#974 holds an import() behind an earlier load where the two share a cycle that is left to run, or where the earlier graph has the module. resumeImportsBehindLoad() makes the call again in a job of the earlier load, so the modules that only the later graph has read the store of the earlier import(). ImportBehindLoad keeps the async context of the call, and the call is made again under it, as for a call whose text waits.
robobun added a commit to oven-sh/bun that referenced this pull request Nov 2, 2026
…f its call in a compiled executable

A program for test/aot/parallel. With oven-sh/WebKit#974 the second import() waits behind the first load and is made again in a job of that load. Without the commit of oven-sh/WebKit#941 that keeps the async context in ImportBehindLoad, three of its seven lines print the store of the first import().
robobun added a commit that referenced this pull request Nov 3, 2026
main has eight commits since 4d45755, the commit this branch started
at: #967, #971, #972, #974, #978, #979, #982 and #983.

Two files conflict, each in one place. Both sides changed a line that calls
one of WTF's atomic functions: this branch gave it the MemoryOrder template
argument (WebKit/WebKit@6ab0aebc81), main changed the code beside it.

- aot/analysis/AOTCallGraph.cpp: main's new loop over
  body.expectedFunctionTypes, with atomicLoad<MemoryOrder::Relaxed>() and
  atomicStore<MemoryOrder::Relaxed>().
- aot/runtime/AOTInstance.cpp: main's `if constexpr (hasFaultingLoopPolls)`
  in setPollsEnabled(), behind atomicStore<MemoryOrder::Relaxed>().

No conflict, and the same change: aot/valueflow/AOTFlowApply.cpp has three
calls in the old form that main added. They take the template argument.

The other 21 files that both sides changed merge as they are.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant