Repository navigation
chore(upstream): merge WebKit AOT compiler rewrite - #53
Draft
vincentkoc wants to merge 21 commits into
Draft
vincentkoc wants to merge 21 commits into
vincentkoc wants to merge 21 commits into
Conversation
…ilds of large functions (oven-sh#967) * AOT: crashes and wrong answers found since oven-sh#942, and faster builds of large functions Its tests that need Bun are in the Bun PR that goes with this one. Crashes of compiled programs: let b = null, c = null; if (n) c = { k: 1.5 }; while (b !== b) b = [b]; if (y != b) { [b, y] = [y, b]; return c === y ? c : 1; } ended by SIGSEGV, and ten lines of the same kind aborted the compiler. SinkAllocations analyses a function, counts and then rewrites it, and all three must decide alike. A helper read constants out of the function as it stood, so the rewriting saw constants that the analysis had not seen. oven-sh#942 mended one of its five callers. Now the question is answered from a table that is filled before anything is rewritten, and that mend is gone. try { null.x } catch (e) { e.constructor.list = { length: 3 } } // TypeError.list was an array before ended by a signal, gave `undefined`, or called the function that had been replaced. Flow took the stores that it could follow for all the stores there are. A store through an object that nobody knows is now looked for on every built-in class, and on the prototypes above crypto.subtle, Bun.s3 and a request's signal. class Holder { held = stream[Symbol.for("nodejs.stream.readable")]; } // a ReadableStream aborted, and in a function the read gave `undefined`. A symbol out of Symbol.for() was treated like one out of Symbol(), which the host cannot reach. Wrong answers: - After `delete A.prototype.twice`, `new A(8).twice()` still answered. With a setter on B.prototype alone, `const o = k === 0 ? new A(k) : new B(k); o.y = 5` in a loop ran B's setter for the first A. B3 moved the load of a class's guard bits across the allocation that makes the class. - `require("./config.json")` in a .cjs file that runs while the graph loads answered the ES namespace. - `Object.assign({}, source)` stored names before symbols for a source with room for elements. - Closures of one function that call each other in tail position: a stack frame had the name of the function object that was called, not of the one that ran. - With one optional pass off, `o.a` was folded where `o` may be null, and another pass aborted the build. - Two runs of one executable made different caching decisions: a table was hashed by an address. Memory: the slow path of a stub has a frame as large as what it saves. Objects were kept alive longer than in plain Bun. A store of "" or of a string of one Latin-1 character needs no write barrier. Build time. A generated function of 1,000 statements builds in 7,891 M instructions, from 12,473 M, with the same output: liveness gave up only after a fixed number of visits and then started over, a dataflow kept a counter for each condition in each block, and the guards' analysis asked each write about each proof, where 4,304 writes had 28 different answers. Speed: `a[i] === b[j]`, case conversion sixteen characters at a step, a read by name of a slot that holds a raw double, and 226 stepped benchmark rows through the stubs' new form. Checks: the proof checks compare a function's identity and are no longer skipped in forwarding code. `--aot-check=ClaimsOfFlow` tests every claim of flow where it is made. Under the verifying flags SinkAllocations checks that its three steps decided alike. * AOT: built-ins reached by computed names, RegExp fixes, a vectorizer crash Crashes - `Bun.gzipSync.list = [1, 2]; Bun[JSON.parse('"gzipSync"')].list = { length: 3 }` and then `Bun.gzipSync.list.length` segfaulted. So did the same through `Event.prototype[name]`, names out of `Object.keys(Bun)`, and `Bun.Glob.prototype` reached by three computed names. `Object.setPrototypeOf(Object.getPrototypeOf(Bun.s3), { .. })` was not seen. A read under a computed key, or of `prototype`, off an unknown object in the program's own code now reaches the host prototype objects. This replaces the wider rule of the previous commit, which also counted such reads in library code and cost `await request.json()` its typed parse. - The compiler died with "Floating point error" on `while (a == 65536 && i0 < 9) { i0++; c = c > 0 ? d : c; }`. B3VectorizeLoops divided by the number of memory accesses of a loop that has none. - A loop counter beyond int32, read from a property and used to index a typed array, trapped. - `RegExp::matchWithImageCode` dereferenced a null image record when jsc runs its tests with regular expressions compiled ahead of time (six JSTests). Wrong answers - `/^(?=(.*))\1$/` said "no match" for a subject with characters outside Latin-1. The pattern was taken to consume 8-bit characters only: what a lookahead captures counts once a back reference can consume it. - `new RegExp("\\B" + text, "u")`: `\B` matched inside a surrogate pair. - `function make(k) { return k === 0 ? new A(0) : new B(1) }` with a getter on `B.prototype` alone: `make(0).size` ran `B`'s getter when the first `A` was made behind a test of both classes. - `x.f += n` where `f` starts on the prototype: the arm that reads `f` by name may run a getter, and came back into code that relied on a guard from before the read. - `Interface.prototype[Symbol.asyncIterator]` read in a function was a different function object than `Reflect.get()` returned (node:readline). - `await import("sass")` in an executable threw "Cannot assign to Function.prototype.call$0". Programs out of dart2js add such names. - `function twice(f, n) { return f(f(n)) } twice({}, 1)` quoted `f` instead of `f(n)` in the error message. - `import * as ns`; `Reflect.setPrototypeOf(ns, ..)`, `Object.freeze(ns)`, `"added" in ns`: a guard for compiled objects read a module namespace. Windows - `hasChanged |= std::exchange(member, copyOfClass(member)) != member` compares the old value with itself where the right operand is evaluated first, as on both Windows targets. Copied classes and closure factories kept the types of the original there: correct, but less precise. Memory - `for (let i = 0; i < n; i++) a.push(f(i))` reserved the whole array up front even where `f` may throw and end the loop. Faster - `let current = first; while (current) current = current.run(this)` with `first` typed by an interface: objects that have the interface's members but none of its layouts stay in the typed loop. Effect's run loop: -20% instructions per turn. Smaller - The compiler's rarely run files are built for size: -600 KB. Tools - The map and `--aot-report` say "proven" or "missed optimization" where the final IR knows the receiver's class, instead of "unknown". - False alarms under `--aot-validate` and `--aot-verify-each-pass`: a field that holds its own object, what an `await` holds, `Object.keys()` of an object with a computed key. * AOT: FlowSiteLevel stays within 24 bytes on Windows The previous commit added a bool at the end of FlowSiteLevel. With the Microsoft ABI the two bit-fields at its start take a unit of their own, so the struct was already 24 bytes there and grew to 28, and the static_assert failed every Windows build. The bool now stands behind the two uint8_t members, in what was padding on every target. * AOT: a build aborted where a generic copy rejoins at a block entered by a test's own edge `bun test --aot` on NestJS's suite stopped with "Stopped while compiling printMessages .. in PlaceTypeTests". A block's parameters are its explicit arguments followed by the results of the edges that enter it, such as the narrowed value of a type test. Two places indexed an edge's arguments with a parameter's index, which is out of bounds for an edge result. Both now ask incomingValueOf(). An edge's own result counts as proven when a rejoin is planned, and as unknown when the first values of loop variables are looked for. The second commit of this pull request brought this. * AOT: the first error in a long statement, and the memory of a build with a long chain of classes Two fixes for two red tests of oven-sh/bun#47571. The first error at a place costs the same in a longer file Forty places inside one `switch` of 12,000 cases each throw their first TypeError ("undefined is not an object (evaluating 'a.b')"). To quote the source in such a message the run time decodes the entries that say what was printed in place of the original text. It decoded every entry of the kept piece of source at each first error, and a long statement or function is one piece. The entries of a piece are now decoded once and kept, however many places in it throw. A compiled test run decoded every entry of the module each time: now once. The long file decodes 2,162 entries where it decoded about 357,000. Forty places in one statement: 2.3 > 1.5 ms. In a test run: 4.05 > 0.59 ms. With AddressSanitizer it was 130 to 141 ms against 2 to 3 ms in the short file, which is what failed the-first-error-at-a-place-costs-the-same-in-a-longer-file on the x64 ASAN lane. No message changes. The counts file has a new line, `decoded:TextPrintedInPlace`. A build keeps the partition of a call's receivers as words `function ask_i(x) { return x.weigh() + ask_(i-1)(x) }`, 800 in a chain, each given an instance of one class more. No two calls of `weigh` see the same classes, so the cache kept one partition per call and key, each with an arm per class: 320,000 arms of 112 bytes. The cache now keeps a partition packed: 24 bytes for an arm with one method and one layout. And "may this call enter a table look-up" built and kept a whole partition to answer one bool: it now asks class by class, stops at the first no and keeps nothing. 800 classes: 189 > 166 MB, 20.14 > 20.01 G instructions. The generated code is identical on 1.16 million functions. build-scaling-4 failed on macOS arm64 with "twice as many classes cost 2.58 times the peakBytes; allowed: 2.5". On Linux that ratio goes from 2.29 to 1.85. Not measured on macOS.
… never ended, faster loops (oven-sh#971) * AOT: miscompilations and crashes found since oven-sh#967, builds that never ended, faster loops Wrong answers of compiled programs: - A list of numbers with one `undefined` in it, iterated by two loops. The first adds the numbers up, the second counts the `undefined`s: it counted 0 instead of 1. - In a generator, a value read before a `yield*` and used after it was wiped when a parameter that held an object was set to `null` or `undefined` in the same loop: 12 instead of 22. A second program never ended. - A `RegExp` made from text out of a list was compiled from the text the build had predicted, even when the list was read under two names in one step and then changed. - `main.mjs` imports `m.mjs`, which imports `x.cjs`, which calls `require()` on `m.mjs` under a name made at run time: `main.mjs` ran after `x.cjs` had ended, not before the `require()` returned. - An `import()` made while other `import()` calls were waiting, and whose graph shares a module with one of them, was held back behind all of them. Crashes of compiled programs: - A segfault in eleven lines: a loop reads and stores a static field that holds an object, after the field was deleted while a Proxy was on the class's prototype chain and was defined again through the trap. - A segfault at address 0 after an out-of-memory error that the program catches: a rope of 2^30 two-byte characters that cannot be flattened, ahead of a short loop in a `try` with `catch` and `finally`. Builds that failed or never ended: - Thirteen lines did not build in two minutes: two nested loops of eight iterations, a swap in a `try`, an xor in its `finally`. B3's `Value::returnsBool()` recursed into both operands without remembering what it had asked, so its cost was exponential. - One function spent 214 of 246 seconds in type propagation: its queue was last in, first out. - The compiler aborted on `for (;;) { if (source.take === never) return ..; const got = source.take(..); .. }`. A loop that reads the same property of an unchanging object on every iteration gets a version that asks ahead of the loop, and a type test on that read was turned into a cast of the copy. Where both sides of the test's branch leave the loop, the branch still hands on what the test proved, so its condition has to stay a test. - Fourteen lines stopped the build with `InvalidGuard`: a read of a typed array at the top of an iteration relied on the bounds check at the end of the previous one, and the loop's counter was then replaced by a constant. - Quadratic time in large functions: three places in Air's register and stack allocators, two in the ahead-of-time compiler's own passes (one is flow analysis), and the lookup of a regular expression's symbols (arm64: twice as many alternatives cost 9.5 times as much, now 1.74 times). `bun test --aot`, following oven-sh/bun#44833: - An `import` that reaches a mock whose factory threw gets the error that the loader kept. `require()` of a mocked module returns what the factory returned. A later mock of a CommonJS file doesn't change what an earlier `require()` returned. - The engine's own tests no longer depend on how many built-ins Bun has. Speed: - New B3 pass, promoteLoopMemory: `a[k] += v[j]` and `o.total += x` reloaded on every iteration what the previous one had stored. The word is read once ahead of the loop and carried in a Phi. The stores stay where they are. Off by default; only the ahead-of-time compiler's policy turns it on. - `map.has(k)` followed by `map.get(k)` searches once. Keys that are objects are searched inline in loops. - A generator function made for each call: 2,585 to 175 instructions. tslib's `__awaiter`: 3,876 to 1,441 per iteration. - Helpers that Babel and TypeScript emit: `Reflect.construct` behind tests, a built-in bound and called in one function, descriptors read and defined again, `for..in` copies. - Remainders and quotients of whole numbers stay integers. A loop polls with one faulting load on arm64, macOS x64 and Windows arm64 too. * AOT: a library function that a test run leaves out is refused, not aborted on `import { test, vi } from "vitest"; test("t", () => { vi.useFakeTimers(); })` under `bun test --aot` ended in `abort()`. The code that throws "was not compiled into this executable" looked the function up in the table of built-ins to ask whether it is an accessor's getter, and a function that the table leaves out has no record there. It asks `contains()` first now, so the error is thrown. * AOT: a member copied off `require("bun")` under a computed key is followed to its call `bun = require("bun"); copy = {}; copy[key] = bun[key]; copy.plugin(options)`: the build now sees that `copy.plugin` may be `Bun.plugin`. The test, `plugin-copy-late-mark.ir`, comes with Bun's half. * AOT: two segfaults: a store through a caught error's `constructor.prototype`, and to a method found on `Bun.s3` `TypeError.prototype.list = [1, 2]`, then `catch (e) { put(e.constructor.prototype, { length: 3 }) }` with `put(where, what) { where.list = what }`, then `TypeError.prototype.list.length` read in a function: the read took the list for what the build had seen stored by name, and the program ended in a segfault. The same with `Bun.S3Client.prototype.file.list = [1, 2]; Bun.s3.file.list = { length: 3 }`. Where no list was involved, the read answered `undefined`. Both stores are now seen by a read that names the object. * AOT: promoteLoopMemory is not switched on yet The pass and its tests stay in B3. The ahead-of-time compiler's policy no longer turns it on. What broke, in a loop: holder.n = (holder.n + 1) | 0; if (j === when) other.n = 50; if (holder instanceof Other) bump(holder); with `function bump(o: Other) { o.n = (o.n + o.k) | 0 }`. Where `holder` and `other` are one object, a `Plain`, the store of 50 was lost: the function answered 7:29 instead of 55:317. The copy of `bump` in the loop never runs for that object, but its access named a narrower heap for the word, and the pass let that one heap decide that nobody else writes it.
…ex slots, a bounds test on split() (oven-sh#972) * AOT: miscompilations and crashes found since oven-sh#967, builds that never ended, faster loops Wrong answers of compiled programs: - A list of numbers with one `undefined` in it, iterated by two loops. The first adds the numbers up, the second counts the `undefined`s: it counted 0 instead of 1. - In a generator, a value read before a `yield*` and used after it was wiped when a parameter that held an object was set to `null` or `undefined` in the same loop: 12 instead of 22. A second program never ended. - A `RegExp` made from text out of a list was compiled from the text the build had predicted, even when the list was read under two names in one step and then changed. - `main.mjs` imports `m.mjs`, which imports `x.cjs`, which calls `require()` on `m.mjs` under a name made at run time: `main.mjs` ran after `x.cjs` had ended, not before the `require()` returned. - An `import()` made while other `import()` calls were waiting, and whose graph shares a module with one of them, was held back behind all of them. Crashes of compiled programs: - A segfault in eleven lines: a loop reads and stores a static field that holds an object, after the field was deleted while a Proxy was on the class's prototype chain and was defined again through the trap. - A segfault at address 0 after an out-of-memory error that the program catches: a rope of 2^30 two-byte characters that cannot be flattened, ahead of a short loop in a `try` with `catch` and `finally`. Builds that failed or never ended: - Thirteen lines did not build in two minutes: two nested loops of eight iterations, a swap in a `try`, an xor in its `finally`. B3's `Value::returnsBool()` recursed into both operands without remembering what it had asked, so its cost was exponential. - One function spent 214 of 246 seconds in type propagation: its queue was last in, first out. - The compiler aborted on `for (;;) { if (source.take === never) return ..; const got = source.take(..); .. }`. A loop that reads the same property of an unchanging object on every iteration gets a version that asks ahead of the loop, and a type test on that read was turned into a cast of the copy. Where both sides of the test's branch leave the loop, the branch still hands on what the test proved, so its condition has to stay a test. - Fourteen lines stopped the build with `InvalidGuard`: a read of a typed array at the top of an iteration relied on the bounds check at the end of the previous one, and the loop's counter was then replaced by a constant. - Quadratic time in large functions: three places in Air's register and stack allocators, two in the ahead-of-time compiler's own passes (one is flow analysis), and the lookup of a regular expression's symbols (arm64: twice as many alternatives cost 9.5 times as much, now 1.74 times). `bun test --aot`, following oven-sh/bun#44833: - An `import` that reaches a mock whose factory threw gets the error that the loader kept. `require()` of a mocked module returns what the factory returned. A later mock of a CommonJS file doesn't change what an earlier `require()` returned. - The engine's own tests no longer depend on how many built-ins Bun has. Speed: - New B3 pass, promoteLoopMemory: `a[k] += v[j]` and `o.total += x` reloaded on every iteration what the previous one had stored. The word is read once ahead of the loop and carried in a Phi. The stores stay where they are. Off by default; only the ahead-of-time compiler's policy turns it on. - `map.has(k)` followed by `map.get(k)` searches once. Keys that are objects are searched inline in loops. - A generator function made for each call: 2,585 to 175 instructions. tslib's `__awaiter`: 3,876 to 1,441 per iteration. - Helpers that Babel and TypeScript emit: `Reflect.construct` behind tests, a built-in bound and called in one function, descriptors read and defined again, `for..in` copies. - Remainders and quotients of whole numbers stay integers. A loop polls with one faulting load on arm64, macOS x64 and Windows arm64 too. * AOT: a library function that a test run leaves out is refused, not aborted on `import { test, vi } from "vitest"; test("t", () => { vi.useFakeTimers(); })` under `bun test --aot` ended in `abort()`. The code that throws "was not compiled into this executable" looked the function up in the table of built-ins to ask whether it is an accessor's getter, and a function that the table leaves out has no record there. It asks `contains()` first now, so the error is thrown. * AOT: a member copied off `require("bun")` under a computed key is followed to its call `bun = require("bun"); copy = {}; copy[key] = bun[key]; copy.plugin(options)`: the build now sees that `copy.plugin` may be `Bun.plugin`. The test, `plugin-copy-late-mark.ir`, comes with Bun's half. * AOT: two segfaults: a store through a caught error's `constructor.prototype`, and to a method found on `Bun.s3` `TypeError.prototype.list = [1, 2]`, then `catch (e) { put(e.constructor.prototype, { length: 3 }) }` with `put(where, what) { where.list = what }`, then `TypeError.prototype.list.length` read in a function: the read took the list for what the build had seen stored by name, and the program ended in a segfault. The same with `Bun.S3Client.prototype.file.list = [1, 2]; Bun.s3.file.list = { length: 3 }`. Where no list was involved, the read answered `undefined`. Both stores are now seen by a read that names the object. * AOT: promoteLoopMemory is not switched on yet The pass and its tests stay in B3. The ahead-of-time compiler's policy no longer turns it on. What broke, in a loop: holder.n = (holder.n + 1) | 0; if (j === when) other.n = 50; if (holder instanceof Other) bump(holder); with `function bump(o: Other) { o.n = (o.n + o.k) | 0 }`. Where `holder` and `other` are one object, a `Plain`, the store of 50 was lost: the function answered 7:29 instead of 55:317. The copy of `bump` in the loop never runs for that object, but its access named a narrower heap for the word, and the pass let that one heap decide that nobody else writes it. * AOT: fix crashes and miscompilations found since oven-sh#971, free Map/Set index slots, turn promoteLoopMemory back on Stacked on oven-sh#971. The stored programs and tests for everything listed here come with oven-sh/bun#47605. Crashes and failed builds: - `TypeError.list = [1, 2]; JSON.parse("0", () => Reflect.set)(e.constructor, "list", { length: 3 })` with `e` a caught error, then `TypeError.list.length` in a function: the compiled program segfaulted. The same happened with a hidden `Object.assign` or `Object.defineProperty`, and when the writer was called as a member of a hidden object. A write through a value the compiler can't identify is now also applied to built-in objects, even when flow analysis sees no path to them. - `Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key))` with a `key` object whose `toString()` returns "a" and then "b": plain prints 3, compiled code segfaulted. With methods the wrong one ran; with getters it threw "'a' was left out of the executable". - Every `--aot` executable aborted before its first statement on a kernel with 64 KB pages (RHEL 8 aarch64, Oracle's UEK, the 64k kernels for NVIDIA Grace). - The compiler aborted with "Stopped while compiling run in ConvertCountersToIntegers" on `for (..) { if (j === w) other.n = j; holder.n = (holder.n + 1) | 0; if (holder instanceof Q) bump(holder); }`. - `vi.doUnmock("./k.js")` while a plugin's async `onLoad` is still loading another import segfaulted under `bun test --aot`. - `bun test --aot` aborted under `validateExceptionChecks`, which CI's ASAN lane sets, when a mock was installed in a layered run: `vi.doMock("./m08.js", async () => { await sleep(80); return { value: "mocked" }; }); import("./m08.js")`. Miscompilations: - The code that creates, fills and stores a module variable's value is deferred until the variable is first read. An assignment to a plain object or function created by that initializer was deferred with it. But a program may call `Object.freeze(Object.prototype)` or `Object.defineProperty(Object.prototype, "toString", { writable: false })`, and after that an assignment to an inherited name fails: a TypeError in strict code, silently dropped in sloppy code. Imports run first and the main module hardens afterwards, so plain did the store before the freeze and compiled code did it after. main has this bug too. - A loop's stores to a variable were delayed to the loop's exit edges, even across an operation that throws only when memory runs out (`text.charCodeAt()` on a rope). That error can be caught, and then the loop is left through none of those edges. - A Map or Set lookup with the same key on every iteration was hoisted out of the loop, even when a `try` covered the lookup but not the loop's preheader, and even when the loop might not run: `for (let i = 0; i < 0; i++) map.has(rope)` threw a RangeError. - A loop that does `const list = []`, pushes, reads `this.state.type`, reads `list[0]` and stores the list into a node at the end of the iteration: a value read back out of an open object was taken for this iteration's list. Also with the push on one arm of an `if`. - `seen.push([a])` in one loop and `for (const kept of seen) show(kept)` in the next: the inner list was attributed to `show()`. - `const o = Object.create(Blob.prototype, {}); o.constructor = { get name() { return "Stored" } }; o.text()`: plain throws ERR_INVALID_THIS ".. received an instance of Stored"; compiled code threw "'name' was left out of the executable". The same for `Buffer.concat(v)` when `v` is an object that isn't an array. - `const { module } = await WebAssembly.instantiate(..)`: the compiler didn't know what the promise resolves with. Modules: - `for (3 times) try { require("./t.cjs") } catch {}` where t.cjs throws: the body ran once and the first error was thrown three times. It now runs again each time, as in node and `bun run`. - `Object.isExtensible(require("./e3.mjs"))` after an import of the same file: plain true, compiled false. - `require("./m3.mjs"); Object.keys(require("./d.json"))` where m3.mjs imports d.json: plain `a,default`, compiled `a`. - Loading a file that only a plugin can provide, under a name computed at run time. - A test registers `Bun.plugin` with an `onLoad` that matches slow.js and then does `await import("./user.js")`, where user.js imports k.js and slow.js: "Cannot access 'x' before initialization" for k.js's export. Quadratic behavior: - `map.delete(k); map.set(k, v)` took a new slot in the table's index each time and never freed the old one, so each iteration cost the size of the table. A deletion now frees the slot when no search can miss it. This costs 5 more instructions on a set and on a delete. - `class Below extends mix(Base)`: the whole caller was walked for every such call. At 3,200 classes that step went from 8,306.6 M instructions to 119.7 M; the generated code is byte-identical. - Two more build steps that grew with the square of the program: choosing representations around a web of values, and finding who reads a parameter that is only passed on. Speed: - The AOT policy turns promoteLoopMemory on again, with the fix for the aliased store that got it turned off in oven-sh#971. What guards it: 14 stored programs that each gave a wrong answer with an earlier version of the pass, a fuzzer family written for it, 8,815 programs with no differing result, and a review by someone who didn't write it and tried to break it: 0 of 7,728 programs wrong. - The low word of a sum that was built from doubles is computed as a 32-bit sum. Outside aot/: 13 files. The JIT compiles b3/ too; the changes below run there only where noted. - b3/B3PromoteLoopMemory.cpp and its testb3 cases: the pass. It runs only when `loops.promotesMemory` is set: false by default, set only by the AOT policy. - b3/B3LikelyPaths.{h,cpp}: how the nearest common dominator over likely paths is found. Used by hoistLoopInvariantValues() under `loops.hoistsOnlyFromLikelyPaths`: false by default, set only by the AOT policy. - b3/B3ReduceStrength.cpp and testb3_bun_reductions.cpp: `Trunc(Op(a, b))` becomes `Op(Trunc(a), Trunc(b))` when this walk built the operation from doubles. Under `narrowsIntegers`: false by default, set only by the AOT policy. - b3/B3LoopRangeChecks.cpp, b3/B3Policy.h: a switch that lets a test sabotage the compiler (`omitsLimitOfClampedBound`, false by default) and a client callback (null by default). Loops are versioned only under `loops.versions`: set only by the AOT policy. - b3/B3CompilationReport.h: 16 more counters in a Procedure's report. That is 64 more bytes per Procedure, in the JIT too; nothing there increments them. - heap/HeapSubspaceTypes.h: an #include under the existing `#if ENABLE(AOT)`, for the generators' own subspace. - llint/LLIntEntrypoint.cpp, runtime/VM.cpp: inside `#if ENABLE(AOT)`: code that runs in the interpreter is also counted in a process without an image, under an option that tests set. - runtime/ExceptionHelpers.cpp: inside `#if ENABLE(AOT)`: a clearer error for a global function declaration that reuses the name of a global that compiled code treats as a constant. * AOT: a bounds test on the result of split() or splice() compared against the wrong length function f(h: string | null): string[] { if (h == null) return []; const accepted: { type: string; count: number }[] = []; for (const part of h.split(",")) { const [type, ...params] = part.split(";").map(v => v.trim()); accepted.push({ type, count: params.length }); } return accepted.map(e => e.type); } Built with --aot-validate, f("a/b") and f("c/d, e/f") returned [] instead of ["a/b"] and ["c/d", "e/f"]: the whole for-of loop was removed. No error, exit code 0. main has this bug too. When only the first few elements and the length of an array returned by split, match, exec or splice are used, allocation sinking never builds the array. It becomes a virtual object whose shape counts only the elements that are kept separately. evaluateBoundsTest() compared an index against that count as if it were the array's length. Nothing in this function reads a constant index, so the count was 0, every index was "out of bounds", the edge into the loop body was dead, and the body held the only use that would have asked for the real array. For such an array, an index past the shape's count is now unknown instead of false. The wrong answer has only been seen under --aot-validate, where a type check keeps the array alive. At default flags, iterating over split()'s result becomes a loop over indexOf and substring and no array is made. But the rule itself was reached at default flags too, so a wrong answer there can't be ruled out for earlier builds. The same shape with splice() was wrong as well. The generated code of ten servers and the TypeScript compiler benchmark is unchanged at default flags. The tests come with oven-sh/bun#47605: test/aot/parallel/strings/split-list-walked.ts and test/aot/parallel/arrays/spliced-list-walked.ts. * AOT: include what AOTValueFlowSolveTestCommands.cpp uses The file uses StringPrintStream and makeString() without including their headers. It compiled only where the unified build put it next to a file that does. The -aot lanes failed with "unknown type name 'StringPrintStream'" and "use of undeclared identifier 'makeString'". * AOT: don't name a variable `small` in AOTValueFlowCoreTests.cpp The Windows SDK's rpcndr.h has `#define small char`, so `ValueSets small(1u << 10);` didn't compile on the Windows -aot lanes ("cannot combine with previous 'type-name' declaration specifier"). * AOT: a module whose mock ended before its body ran has not begun to load Under `bun test --aot`, two test files each mock the same ES module before it loads (`vi.mock("./dep.js", ..)` hoisted, or `mock.module()` in a test ahead of the `import()`). A third file, which mocks nothing, read the second file's mock, by name and through the namespace object, and dep.js never ran. Plain `bun test` runs dep.js and reads the real values. When the first mock ended, its module still counted as having begun to load, because that goes by the group it was loaded with, which is never taken back. So the second mock's factory didn't wait for the first import. It ran at once, and its values were laid by name over a module whose body had never run. Modules::hasBegunToLoad() now answers no for such a module. The bit it reads is set only where a mock that stood in place of a module ends, so an executable never gets here. The test comes with oven-sh/bun#47605: test/aot/mock-second-file.test.ts. * AOT: check for an exception after a mock in place is taken back AOT::didEndMock() had no throw scope, and went on after setMockInPlace(null), which can throw. Under `validateExceptionChecks`, which CI's ASAN lane sets, `bun test --aot` aborted in two test files: "setMockInPlace @ AOTModules.cpp .. unchecked as of captureExports @ AOTModuleLinking.cpp". // a.test.js vi.mock("./dep.js", () => ({ value: "mock" })); // the mock ends with its test file; b.test.js then imports ./dep.js * AOT: build the same program at one thread as at many The `typescript` package built at `--aot-threads=1` and at 4 gave two different executables from the same front-end output. One line in one function of 16,981 differed: for one property read, the predicted receivers were empty at one thread and listed at two or more. No proof differed, and 4, 4 again and 16 threads agreed, so it was never run to run. findOnAllThreadsAndDo(), in the pass that computes predictions after the solver's rounds, was the only place in the flow analysis with a test for exactly one thread. With one worker each item of a wave ran in place and saw what the items before it had done. With more, all steps of a batch are computed from the state at its start and replayed in order. Both end at a fixpoint, but the pass has caps, so not always at the same one. One thread now takes the same path as many. Batch and share sizes don't depend on the thread count. At two or more threads nothing changes. Six thread counts from 1 to 32, eight times each: one hash, the one that 4 and 16 threads gave before. test/aot/same-program-however-the-threads-fall.test.ts in oven-sh/bun covers it. * AOT: a read copied for another kind of array storage kept its proof across a store through a second name function clean(src, dst, n) { for (let i = 0; i < n; i++) { if (src[i] < 0) dst[i] = NaN; // a bad sample else dst[i] = src[i] * 2; } } clean("1,2,3,4,5,6".split(","), [], 6); const samples = [1.5, -1.5, 2.5, 3.5, 4.5, 6.5]; clean(samples, samples, 6); console.log(samples.join(" ")); // 3 NaN 5.5 7 9 13, should be 3 NaN 5 7 9 13 Silent, at default flags, in plain JavaScript and in TypeScript. main has this bug. PlanArrayCapacities copies the reads behind a test of an array's kind of storage for the other kind, as far as the test's proof goes. addReadsOfOtherStorage() copied the stores between those reads with them and never asked whether the copy's proof outlives such a store. A store through another operand that may be the same array ends it. The guards are now asked again behind each copy, and a copy that may lose its proof is taken back. New: --aot-sabotage=CopiedReadsKeepTheirProof, the statistics ReadsCopiedForOtherStorage and CopiesOfReadsTakenBack, two IR tests. The 30 stored programs come with oven-sh/bun#47605. * AOT: SplitBehindEntryChecks kept value ids across a retyping that renumbers the values function outer(p: any, parent: number | null): string { const head = parent === null ? "" : "<a " + parent + ">" + "<b " + parent + ">"; return head + (p as Kind).name; } // called hot as outer(given, null) An assertion build of the compiler aborted on `id.index() < values.size()`. A release build read outside the function's table of values. No release build has been seen to give other code. typeValuesFromNever() may cut dead blocks, and then it compacts the function and gives every value a new id. sayWhatOperandsOfFormerChecksHaveProven() went on with the ids it had taken before. The retyping is now handed a Renumbering: the checks' ids are mapped, and those cut with the dead code are dropped. The bound at the head of that function is a RELEASE_ASSERT. * AOT: the second import() of a CommonJS file whose mock factory throws asks the factory again mock.module("./lib.cjs", () => { throw new Error("boom " + ++n); }); // import(), require(), import(), require(), each caught Under `bun test --aot` as one program the errors read boom 1, 2, 1, 3. Plain `bun test` has 1, 2, 3, 4. An import() of a CommonJS file begins at the file's ES face. The walk then comes to the file itself, which isn't the root, so the throw counted as "behind an import statement" and the error was kept on the file for the loader. The file behind the root's face now counts as the root. * AOT: a mock factory that takes importOriginal and fails is asked again at the next import In plain `bun test` a factory that takes `importOriginal`, sync or async, is laid over the original. Where it fails the loader keeps no record: the next import asks it again, and the module that imports the mocked one then loads. A factory without the parameter keeps its error. Compiled code kept the error for both kinds. deferMock() is now told which kind the mock is, and runMocksBelow() keeps no error for the first kind. Where such a factory's promise rejects a turn later, the mock waits for the next load again instead of being taken away. The test comes with oven-sh/bun#47605: test/aot/mock-factory-retry.test.ts. * AOT: vi.doUnmock() in a test, behind a hoisted vi.mock() of a module that never ran vi.mock("./dep.js", () => ({ value: "mock", fn() { return "mock-fn"; } })); import { value, fn } from "./dep.js"; test("t", () => { vi.doUnmock("./dep.js"); fn(); value; }); Under `bun test --aot` the imported function answered `real-fn` silently, and consts, `ns.value` and `Object.keys(ns)` threw "before initialization". In plain `bun test` whoever was linked keeps the mock. An ended mock that stood in place of a module is now let go when a load next comes to that module. At the end of the test file, while a graph is being loaded, and for .cjs and library modules it is let go at once, as before. So it is where a module that a require() has already answered imports the mocked module: a compiled require() of a module that has run asks nobody, so no load would ever come by. * AOT: `export * from "<a CommonJS package>"` in a mocked module, in layered test runs // lib.mjs export * from "driver"; export let v = 1; // a.test.js mock.module("./lib.mjs", () => ({ v: "mock v" })); await import("./lib.mjs"); In a layered `bun test --aot` run (`--parallel=2 --aot-memory-limit=1KB` is enough) this failed with `TypeError: The module ".../driver/index.js" is not loaded`. The same error came in a second test file that only imports a user of a module which the first file had mocked. A regression of this branch. Three places went behind the `export *` of a mocked module to take names, and evaluateModulesOwedBelow() ran a module that a mock had kept from running without asking the loader for what only that module imports. A mock that came ahead of the load now stands for the whole module. * AOT: the first require() of an ES module gave its namespace object a Structure shared with others import { x } from "./a.mjs"; // read hot const all = ["./a.mjs", "./b.mjs", "./c.mjs"].map(p => import.meta.require(p)); function readX(ns) { return ns.x; } // over the three objects, hot In a layered `bun test --aot` run readX() came to answer a's x for b. Silent. A regression of this branch, found in review. willRequireESModule() set the Structure that all extensible namespace objects share, also on an object that had a Structure of its own, and hasOwnStructure() went on answering true. A property cache keyed on the shared Structure then carried the first module's binding to the next. Such an object now gets a new extensible Structure of its own. * AOT: the compiler aborted on `o.self = o` where o may be a subclass of Map class C extends Map {} function see(o) { return o.self; } function run(o = new C()) { see(o); o.self = o; } run({}); run(); "Stopped while compiling run in PlaceTypeTests." Also over Set, WeakMap, WeakSet, Array and Error. main has this bug. Behind a store's tests, rewriteTestedPropertyAccess() takes the last TypeTest for the test of the stored value, which says nothing about the receiver, and leaves it out. Here the stored value is the receiver, so its only test was left out and the access couldn't be resolved. The test is now left out only where the receiver hasn't passed it too. * AOT: an import() waits for the async factory of a mock below it vi.mock("./a", async importOriginal => ({ ...(await importOriginal()), a: "mocked-a" })); import { a } from "./a"; // and user.ts: export const atLoad = a; Under `bun test --aot` as one program: "Cannot access 'a' before initialization", or the real value, and the next test file "never finished loading". Which of the three came went by when a thread had read the original from disk. A hoisted vi.mock() turns the test file's import statements into such import() calls, so Vitest's commonest partial mock met it. main has this bug. startImport() called the waiting mock's factory, held the import for one turn and delivered it whatever the factory had done by then. Only the loader's own fetch waited. Where factories wait, the import now goes on behind them, and is rejected with what one of them throws. * AOT: a bounds test on the kept prefix of split()'s result compares against the real length Follows "a bounds test on the result of split() or splice() compared against the wrong length". That commit made an index past the kept elements unknown instead of false. The other side had the same flaw: an index inside the shape's count was taken for in bounds, though the array may be shorter than the number of elements that the function reads. The test becomes `position < the array's real length`. New: --aot-sabotage=ListsOfBuiltinsEndWithWhatIsKept and the statistics ListsOfBuiltinsNeverBuilt and ObjectsOfBuiltinsNeverBuilt. It costs 0.15% on one router idiom. * AOT: three IR tests check the new counters of arrays and objects that are never built * AOT: a ring of `export *` with a package in it hung the link of a layered test run // index.ts: export * from "./a"; export * from "./b"; export * from "pkg"; // a.ts, b.ts: export * from "./index"; import { missing } from "./index"; In a layered `bun test --aot` run the link never ended (485,000 frames when it was killed). It should throw the SyntaxError for the missing export, as it does as one program and in an executable. A regression of this branch, found in review. Modules::isMockedInItsImage() called itself for every dynamic `export *` with no set of visited modules. It is one loop with a BitVector now, and asks each module once. * AOT: a name that a barrel hands on from a mocked CommonJS file is the mock's // barrel.js: export { name } from "./lib.cjs"; vi.mock("./lib.cjs", () => ({ name: "mock" })); import { name } from "./barrel.js"; Under `bun test --aot` as one program `name`, `ns.name`, Reflect.get and `(await import("./barrel.js")).name` read `undefined`. Object.getOwnPropertyDescriptor and a spread of the same object were right. findNamespaceExport() leaves a member to the namespace object where the module that declares the variable may be mocked. The names of a CommonJS file are variables of its ES face, and the mark sits on the file, so the read was bound to a variable that nothing fills while the mock stands. Both are asked now. * AOT: a function of a built-in module replaced by a writer the build can't see import found from "node:module"; JSON.parse("0", () => Reflect.set)(found, "SourceMap", mine); function use() { return found.SourceMap(); } use(); // plain: mine's result; compiled: SIGSEGV With `buffer.atob`, `Buffer`, `Blob` and all of `node:util/types` the write went through, a read saw it, and the call by the module's name silently reached the original. main has this bug. Flow analysis reasoned that start-up locks every member of a built-in that no statement writes. Nobody locks the object of a module, and nobody should: stubs and instrumentation replace such members. A holder that is a module's object, has no lock under the name and has escaped to code nobody sees now also answers what was stored through the unknown object, so the call is tested and not bound. * AOT: a variable read stale behind a method picked by a name known only at run time const handlers = { open(done) { return { run() { done({ ok: true }); } }; }, close(done) { return { run() { done({ ok: false }); } }; }, }; const type = JSON.parse('{"type":"open"}').type; let result = null; handlers[type](r => { result = r; }).run(); console.log(JSON.stringify(result)); // null, should be {"ok":true} Silent, at default flags. Neither the verifier nor --aot-validate sees it; --aot-check=MovedLoads does. main has this bug. In cold code Select fuses a property read and a call into CallMethod. That operation's effects are "unknown code", without its known callees. "Unknown code" is what functions entered by unknown code do, and a function whose callers flow analysis all knows isn't among those. So B3 was told that the call doesn't write the variable's word, and forwarded the earlier store to the load. The read and the call are now fused only where the call's effects lie within those of unknown code. New: --aot-sabotage=FusedCallsEnterOnlyUnknownCode. * AOT: a setter laid by Object.defineProperty that stores through `this` import "node:util"; class C { z = 0; } function run(given) { const o = given ?? new C(); Object.defineProperty(o, "p", { set(v) { this.z = v; } }); o.p = 1; return o.z; } console.log(run(), run([])); // 0 1, should be 1 1 Silent. Where a TypeScript class declares `z?: number` the executable aborted. Twenty built-in modules arm it by being imported anywhere in the program. main has this bug. The library's text holds a function that lets accessors escape, so the setter escapes, rightly. For a store with several receivers the setters found are then {unknown}, which covers the known setter, and the branch that hands each receiver to the setters on its own chain asked the covered view and was skipped. The instance was neither handed to the setter nor made to escape, so its field counted as stored once. For a store, where unknown stands among the setters found, the receivers that haven't escaped are now told apart, and each is handed to the setters laid on its own chain. A getter that writes through `this` and is run by a spread had the same hole in the rule for copies. What flow analysis hands the optimizer is unchanged on express, fastify and the typescript package. * AOT: the test of a branch that passes on a proof stays in the branch's block function f(x, n) { let fuel = 3, b = x, c = 1n, e = x, a = x; const gt = { get v() { a = b; return 1; } }; for (let i = 0; i < n; i++) c = e; while (c === b && fuel-- > 0) { gt.v; } b++; return e + 1; } f(1, 2); f("a", 2); "Stopped while compiling f in SimplifyControlFlow", and the step that the message advises to switch off isn't the one at fault. main has this bug; a neighbour with a value out of Map.get built on main and aborted on this branch. HoistLoopInvariantReads lifted the loop's test out of the block of its Branch, which passes a proof on its taken edge. PlaceTypeTests then copied a part of the loop, and the SSA repair turned the Branch's condition into a block parameter, which proves nothing. Of 58 varied forms 23 didn't build and none built and answered wrongly. Such a test is no longer hoisted. Those loops now make an unchanging test in every iteration: 1,031 functions of the public corpus change, 3,361 bytes fewer. * AOT: a class is no longer recognized by its prototype's own `constructor` property const Base = t => class { constructor() { this.a = t; } }; const Heir = S => class extends S { constructor() { super(); this.b = 5; } }; for (let i = 0; i < 2; i++) { const D = Heir(Base(i)); delete D.prototype.constructor; console.log(i, JSON.stringify(new D())); // the second iteration: SIGSEGV } With class fields it is silent: `{"a":1}` for `{"a":1,"b":5}`. Besides `delete`: Reflect.deleteProperty, an accessor, Object.defineProperties, and a mixin helper that copies another prototype's own properties. Of 3,432 generated forms 472 were wrong, 164 by SIGSEGV. main has this bug. From a class's second evaluation on, where no member guard is left, its class object and prototype object are left out of the table of holders. Asked later whose prototype an object is, ClassTable::classOfHolderLeftOut() read the object's own `constructor`. With that gone the answer was "nobody's", and the base's constructor kept the object it had allocated in the base's shape. A prototype object that is left out now names its class object under a private name of the class table: read-only, not enumerable, not deletable, and seen by nothing that a program can ask. * AOT: include what AOTCallRules.cpp uses matchMethodCall() now asks context.build().isSabotaged(..), and BuildContext was an incomplete type there. Bun's build precompiles a prefix header that brings AOTBuildContext.h in; WebKit's own CMake build does not.
…, build time (oven-sh#974) * AOT: frames of a constant text given to eval or `new Function` ignored 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. * AOT: a test file stuck on the top-level await of a compiled module was 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. * AOT: with a __mocks__ folder, a require() as the first load of a mocked 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. * AOT: the original of a file that had run before a mock was laid over it 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. * AOT: `globalThis[holder].__defineGetter__(key, f)` was refused on console, 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. * AOT: vi.spyOn() through a barrel over a mocked module stored the spy 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. * AOT: after a test file resets the modules, the next one finds what ran 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. * AOT: the body of a linked module did not run when a helper mocked it // 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. * AOT: SIGABRT "exception check validation failed" where preload modules 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. * AOT: a function with 14,000 live small objects was refused for its stack 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. * AOT: in the second test file that mocks a module, vi.spyOn() reached 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. * AOT: no pass writes outside a function's tables of values and blocks 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. * AOT: what the test runner hands out is held by native code 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. * AOT: four walks over "what the loader holds" went through a mocked module 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. * AOT: JSON.stringify with a list of names did not count as reading those 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. * AOT: the verifier stopped a build on a union member whose tag no member 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". * AOT: lists made by one literal of constants were one cell though structuredClone() 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. * AOT: an import that went on behind mock factories lost its referrer's 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. * AOT: `using h = monitorEventLoopDelay()` failed: "[Symbol.dispose] was 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. * AOT: `f.apply(x, arguments)`, inlined, with an unknown callee: the list 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. * AOT: a call that Select loses stops the 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. * AOT: one IR test's hand-written Check made its generic version ill-typed 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. * AOT: a release hint kept a list out of the frame `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. * AOT: a copy of a list is read, written through a second name, and read again The second read gave a wrong value. An executable, default flags, no error. main has this bug too. * AOT: the mock of one test file reached the next // 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". * AOT: `--explain=statistics` counts the method calls that are not fused MethodCallsNotFused is the number of places in a program that held the shape of the stale-variable bug fixed in oven-sh/WebKit#972. It is 0 in eight servers (express, fastify, hono, koa, elysia, Bun.serve, node:http, a thin one). * AOT: two IR tests of the counter need Bun's table and move to Bun's tree * AOT: a name the build left out of a compiled module is named in the error 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. * AOT: the compiler failed or aborted on a loop whose counter has two bounds 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. * AOT: a quadratic step in Air's releaseDeadValues 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)`. * AOT: four planted faults and a statistic for three passes that decide things at the top of a file NarrowConstantVariables, DeferDefinitions and DeferClosuresInFields had no planted fault, and one had no counter. No behavior changes without `--aot-sabotage`. * AOT: the verifier stopped a build on a type test hoisted in front of a loop "UnspecifiedValueInspected (TypeTest)" after ConvertCountersToIntegers. A default build gave the right answer before and after this change. The pass takes a type test that the head of a loop asks of its parameter in every turn and asks it once, of the first value, in front of the loop. Where an object is carried out of a counted loop in parts into a second loop, the head looks at the word through an assumption, and the new test did not. It asks through what the head asks. * AOT: any mock of a file cost a closed program a tenth of its likeness to the executable `mock.module("../knob.ts", () => ({ knob: 2 }))` of an unrelated one-line file: 89.7% of hono's functions compiled as in the executable, for 99.1%. The original beside a mock is the file once more, from disk, as code the build has not compiled, and every mock of a file raised that possibility. It is now raised only where something may ask for the original. * AOT: `spyOn(Response.prototype, "status", "get")` was refused though written out in full "TypeError: Cannot spy on Response.prototype.status because built-in objects are read-only in compiled code". So for every getter of Response.prototype, Request.prototype and Blob.prototype, and `Headers.prototype.count`: 17 of 42 accessors and methods asked. * AOT: a built-in that has escaped and climbs the prototype chain lets out what stands above An executable at default flags gave another answer than plain Bun. The thirteen stored programs, classes-reached-through-instances/hidden-*, come with Bun's half. * AOT: build time: what a spread copies is read once for a set of sources `const all = [{ id: 0, k: 0 }, ..]` and n functions `function f7(i) { return { ...all[i], x: 7 }; }`: the step that works out in which orders a literal's keys may come walked, for each spread, each object of the set of sources, and for an object of an array written as data the whole data literal: n spreads x n objects x n nodes. No generated code changes. * AOT: build time: three quadratic steps in SSA repair and the greedy register allocator A use that names a definition which dominates it is reached by that definition alone, so no join gets a parameter for it. What reaches a join on each of its edges is found when the join is first seen to reach a use and kept for the variables that have the same joins. No generated code changes. Shared with the JIT: b3/air/AirAllocateRegistersByGreedy.cpp. * AOT: build time: the owner of a block's parameter and the cycle of a block are looked up, not searched for Two steps quadratic in the size of one function. Flow analysis looked through all blocks of the function for the owner of a block's parameter, and through all again for the edges into that block, for the callee and the receiver of every call. Both tables are now made once for a function. * AOT: `--aot-validate`: a claim inside the value's own type asks nothing Of the 367 B3 values and 112 blocks of a line `t(add, V[1], V[2])` in a validating build, 205 values and 77 blocks belonged to three validations that asked what had just been asked. Default builds do not change. * AOT: build time: the layouts of the receivers of a key are found once For each key that is read and each set of receivers listed for it, flow analysis walked the set's members and the lists of layouts that the members select. `{ ...defaults, ...parse(text) }` in a large program made that quadratic. * AOT: the warning for `require` inside node:vm is back, narrowed A loader that leaves the program with no witness gives the warning only if some record of the program admits code that the build has not seen. * AOT: build time: the tables of a loop live as long as the pass SeparateUnlikelyLoopPaths made, filled and destroyed, for every loop, tables with one entry for each value of the whole function: loops x values. * AOT: two planted faults for Map and Set lookups `--aot-sabotage=GetsBehindHasOutlastWrites` and `--aot-sabotage=ObjectsWithTheTagOfAnotherAreAbsent`. No behavior changes without them. * AOT: build time: phi placement skips up the dominator tree `let v0 = 0; .. for (3 turns) { v0 = v0 + a[1]; .. }` with n variables: three loop passes each demote 2 to 3 n values, and the SSA fix-up after each asks of every variable where it is live. 90% of that was one inner loop. Shared with the JIT: b3/B3FixSSA.cpp, b3/B3CompilationReport.h, and a new b3/testb3_bun_thread_jumps.cpp. Generated code is identical. * AOT: build time: what is copied onto a literal is asked by the set `function f7(o, d) { return d <= 0 ? o : f8({ b: d, ...o, a: 7 }, d - 1); }`, n such functions in a ring: n turns for each of n layouts. * AOT: build time: three quadratic steps of PlaceTypeTests and SimplifyControlFlow in one function "One literal of many names, each read by name". The uses of an object stand in the order of the function; a binary tree over them holds a bit for each type that a use below a node names. The same bytes and answers on every program asked. * AOT: the phase log says what an image's tables are made of After "AOT size of the image:" it prints "AOT size of the tables:" (17 sections) and "AOT size of the program tables:". These numbers reached only `--aot-report`, which `bun test` refuses. * AOT: two planted faults with their counters For the store of a variable that waits for the end of a loop unless something else in the loop sees the place, and for one rule more. No behavior changes without `--aot-sabotage`. * AOT: idiomatic names in the B3 files that the JIT shares Names and comments alone, in 36 files. No old name is left under Source. Shared with the JIT: b3/B3LikelyPaths, B3Policy, B3PromoteLoopMemory, B3ReduceStrength, B3LoopInvariance and others, and their testb3 files. Generated code is identical. * AOT: `--aot-validate`: what was there before an object was made is not that object `try { for (const a of JSON.parse(..)) run(a) } ..` where `run()` makes a class and `new I()`: the new object counted as escaped and might be the list whose storage was tested ahead of the loop, so a verifying build stopped with GuardNotAvailable. Default builds do not change. * AOT: build time: a comparison walks what its arm dominates and asks the graph once EliminateGuards went from 6 M to 774 M instructions between 9,600 and 19,200 statements of one function: past a limit on tested types lay a quadratic walk. * AOT: the objects that native code makes by itself go into both dumps of the table of built-ins 28 host objects and functions, as `hostObjects`: cell, function, prototype, fields. * AOT: a verifying build goes on after a verification fails The first failure of each function is kept and that function is verified no more. Where the pipeline ends the build fails, no code is generated, and the errors begin with one line that counts them. Default builds do not change. * AOT: "TypeOutOfDate" says which way The message now ends with whether the type that stands there is narrower or wider than what the rule gives. * AOT: an import awaited at once begins behind its call Precision of flow analysis alone. IR tests come with it. * AOT: an import awaited through a cast An `import()` awaited through a type cast is taken as one that is awaited at once. With `jest.mock`, 89.0% of hono's functions compile as in the executable, for 71.7%. * AOT: build time: the new objects behind a store are found by one walk `let o = {..}; if (c0) o = {..}; else o.b += 0; if (c1) o = {..}; else o.b += 1; ..`: for each store the scanner walked back through every join parameter behind its receiver and asked a list whether a parameter had been seen: cubic in the branches of a function. * AOT: `valueflow-solve every-order` also applies flow's results under every order Test code. It compared the records of unseen code and the removable features; it now also applies the results to the program, read anew, under 128 orders. * AOT: two `Object.create(target)` sites made reads of the target's getters 20 times dearer const target = { get one() { return this.b + 1; }, two() { .. } }; const a = Object.create(target), c = Object.create(target); a.b = 1; c.b = 2; (i & 1 ? c : a).one; // 105 instructions a read; 5 to 15 with one call site Every `Object.create` call site got a layout of its own. * AOT: build time: tmps passed on the stack through a loop are priced block by block With 8,000 typed sums in one function, the greedy allocator asked 16,384 times about one and the same Tmp, each time from cost 0, walked all blocks to make its list and only then began to add up costs. The limit was passed at block 161 of 8,900 on average. Shared with the JIT: b3/air/AirAllocateRegistersByGreedy.cpp, b3/B3CompilationReport.h, b3/air/testair_bun_allocation.h. * AOT: build time: an assumption is found by its value `for (let k = 0; k < a.length; k++) a[k] = { v: k + 1 };` n times in one function cost n x n x n in flow's look at writes of arrays. No generated code changes. * AOT: the check of moved loads prints and goes on A debugging flag. No code at default flags, none under the check alone. Shared with the JIT: b3/B3Policy.h, one bool, false. b3/B3MovedLoadCheck.cpp is reached only where the compiler's policy logs moved loads. * AOT: the bounds put on tallies get a planted fault and a counter `--explain=statistics` says TalliesBounded; `--aot-sabotage=TalliesOutrunTheirCounters`. No behavior changes without it. * AOT: a fused method call declares what its call did The effects of a fused method call are unknown code united with what the call's known callees do. No size of program decides any more whether a call is fused. This replaces the narrower fix for the stale variable in oven-sh/WebKit#972. * AOT: a planted fault for literals of constants `--aot-sabotage` word alone. The stored programs come with Bun's half. * AOT: a mock that lacks a name which a file of the project hands on `vi.doMock("pkg", () => ({ v: "mock v" }))`, pkg a package in node_modules; user.mjs hands on a name that the factory left out: `export { v, w } from "pkg"`. A mock of a module of another image is known there alone. * AOT: accessors copied onto a built-in prototype were left out of the executable Object.defineProperties(Array.prototype, Object.getOwnPropertyDescriptors({ get extra() { return 41; } })); Reflect.get(new Proxy([1, 2], {}), "extra"); threw "'extra' was left out of the executable" for 41. Setters too, stored to by `Reflect.set` through a Proxy. * AOT: a store to `module.children` did not drop the children that were waiting require("./lib/child.cjs"); module.children = [5]; module.children.length; // 2 for 1 module.children = 5; module.children; // TypeError: children.indexOf is not a function Under `bun run` each `require()` lists its child at once and the setter clears the list. A compiled module's children wait in the engine until `children` is read. The setter now drops them. * AOT: the compiler aborted on a copy of a list that is kept under a second name and written in the loop that reads it function tidy(list) { const copy = [...list]; const result = { values: copy, changed: 0 }; for (let i = 0; i < copy.length; i++) { if (copy[i] < 0) { result.values[i] = Math.abs(copy[i]); result.changed++; } } return result; } `bun build --compile --aot` ended with "abort() called". main has this bug too. * AOT: Reflect.construct() with an heir as its target gave the object the heir's field, and could segfault function Meter(size) { this.size = size; this.n = 0; } function Sub(size) { Meter.call(this, size); this.extra = "x"; } Sub.prototype = Object.create(Meter.prototype); new Sub(2); const o = Reflect.construct(Meter, [9], Sub); `o.extra` was "x" for undefined and `"extra" in o` was true: the heir's field, which was never stored. A larger program of the same shape, which is what Babel's helpers write for a transpiled class, segfaulted. An executable, default flags. Bun's main, which pins an older engine, is right here. An object of `new.target`'s class was allocated and the parent's body run on it, on the assumption that nothing is claimed of how the objects of such a layout are built. That does not hold where the heir's fields are said to be filled. No heir's class is selected there now. * AOT: an input of the verifier that nobody read is gone `VerifierInputs::otherFunctionsMayChange`: its declaration and the one line that set it. It belonged to effect summaries computed on demand; the summaries that are handed over ready took their place and its reader went. * AOT: under a standing mock, a file that was linked before the mock read the mock's values import { who } from "./m.js"; test("..", async () => { vi.doMock("./m.js", () => ({ who: "mock" })); who; // plain: still the real one (await import("./late.js")).read(); // plain: the mock }); Under plain `bun test`, `who` stays real for the file that was linked before the mock, late.js, which is linked after it, reads the mock, and the factory runs at the first load that comes to it. Compiled, the factory ran at the call and its values were laid over m.js's variables, so everybody read the mock. So too for what a preload had loaded under a test file's `vi.mock()`, and for a package. * AOT: build time: the parameter of a wide join waits for its sources Type propagation joined the parameter of a block with many incoming edges again each time one of its sources was typed: 24.0 of 30.7 G instructions at 8,000 states. A parameter of a block with 16 incoming edges or more that is taken again now goes back into the worklist below the queued values that lie in front of the last of its sources, and is joined once when they have been taken. * AOT: deleting the oldest entry of a Map or Set stepped over every entry deleted before it map.delete(map.keys().next().value); // how a hand-written cache lets its oldest entry go const [k] = set; set.delete(k); // a queue Each walk began at entry 0 and stepped over every entry that had gone before, so a turn cost the size of the collection between two tables. The Map or Set object now holds how many entries at the front of its table are known to be deleted. Outside aot/: runtime/JSOrderedHashTable.h, +7, inside `#if ENABLE(AOT)`: a `uint32_t` member in what the object's size class leaves unused, and its offset. * AOT: build time: a store asks the lengths read above it, the nearest first For every store into a list, flow analysis asked about every read of `.length` of that list in the function, at about 9,000 instructions for a question that fails: stores x reads. A length bounds the position only where its read dominates the access. A store now goes up the dominator tree from block to block that reads a length and stops at the first proof. No generated code changes. * AOT: where a generic copy of a loop comes back, a test that narrows gets no other definition Where the generic copy of a loop comes back to the typed code, it tests again the values that the pass narrows and hands each proven value over as another definition of that value. A comparison that stands ahead of the loop and uses what an earlier test has proven was such a value; SSA repair merged the two into a block parameter. * AOT: the row of a run-time function holds what its operation declares B3 is told the intersection of an operation's effects and the declared effects of the C++ function or stub that it calls. A heap that the function's row leaves out is lost however right the operation's row is, and nothing tied the two tables together. The schema's generator now checks, in every build, that what an operation with `runtime = F` is declared to write, read or call lies within F's row, unless a table says why not; a line of that table that no longer applies is an error too. The check found four mismatches, in the rows that two throwing operations call. Those rows are fixed. No generated code changes. * AOT: a test file's load is noted in the layers below // a.test.mjs import "./setup.mjs"; // vi.mock(path, async importOriginal => ..) of two packages and of a barrel import demo from "./demo.mjs"; // imports that barrel In a test run compiled as several layers, which is the form a large run takes under `--parallel`, this printed something else than plain `bun test` and a run compiled as one program do. * AOT: two planted faults and a counter for two rules of flow analysis `--aot-sabotage=BuiltinsLookAtTheirOperandsAlone` undoes the fix for lists of one literal of constants that `structuredClone()` tells apart. One word and one counter more. No behavior changes without `--aot-sabotage`. * AOT: a planted fault and two counters for lists that a loop leaves alone HoistLoopInvariantReads reads a list's length once, and tells the tests of positions that length, where every list that the loop writes is held apart from the measured one. It had no counter and no planted fault. No ordinary build changes. * AOT: a comment says why a wide join begins at 16 edges One comment line in aot/ir/AOTTypePropagation.cpp. * AOT: promoteLoopMemory resets the owners of values, and two checks of assertion builds ask the right thing promoteLoopMemory reads `->owner` in three places and never reset the owners. The TypeScript compiler's bundle enters the pass with null owners in 90 procedures, on values placed by insertion sets. No program is known to crash on it. `m_proc.resetValueOwners()` now stands at the head of `run()`, with a test that the old pass dies on. `SwitchValue::fallThrough(const BasicBlock* owner)` asserted `hasFallThrough()` without its argument. A check in testb3_bun_vectorize_loops.cpp disassembled the whole allocation, which is rounded up, and so read the remains of an earlier compilation; it now disassembles the bytes of the link buffer. No release build changes by these two. Shared with the JIT: b3/B3PromoteLoopMemory.cpp +1, b3/B3SwitchValue.cpp +1 -1 (inside an ASSERT), and two testb3 files. * AOT: three calls into the compiler's run time that a run without compiled code made ask first Outside aot/: runtime/ObjectConstructor.cpp +1 -1, runtime/CallData.cpp +3 -1, runtime/CloneSerializerBase.h +2 -1. Each stood inside `#if ENABLE(AOT)`, which is on in every Bun, so a plain run made the call; none of them changed an answer there. Each now begins with `vm.hasCompiledCode()`. * AOT: a verifying, layered test run stopped with "InvalidType: type 915 is not canonical" A project whose file calls `spawnSync()` of node:child_process under @types/node, run over two or three layers with the verifier on: "internal compiler error (ReadAndVerify): InvalidType: type 915 is not canonical, or its set of layouts is empty or open", and none of the test files ran. A field of a record of a lower layer that takes no object now has a type without objects. * AOT: a counter for the walks over a Map or Set that begin behind the deleted entries One statistic, counted where the code for the first step of such a walk is emitted. No generated code changes. * AOT: `bun test --aot` over forked `export *` with a mocked module at the end was killed at 52 GB Two modules a level, each `export *` of both of the next level; the last level stars m.mjs; `mock.module("./m.mjs", () => ({ m: "mock m" }))`; main.mjs: `import { m } from "./l0a.mjs"`. 12 levels took 1 s. At 26 levels a run compiled as one program was killed at 118 s with 52.7 GB resident. Plain takes 0.1 s. Bun's main is right here. Walks guarded against rings by their path alone went down every way: two to the power of the levels. This is the run's half; the build's half is in Bun. * AOT: a read of a copy of a list expects the storage of the list it was copied from const xs = [1.5, 2.5, NaN, 4.5]; const b = xs.with(2, 9); b[i & 3]; // in a loop A list of numbers with NaN among them is known to hold values of any kind. Reads of its copy looked the storage up each time. Speed alone. * AOT: a store to an object that native code holds does not enter setters hidden behind unknown twice test/aot/servers/a-test-run-compiles-what-an-executable-compiles-whatever-the-order-of-the-rules-of-flow-hono.test.ts in Bun went red with oven-sh/WebKit#972: a test run and an executable compiled 98.3% of hono's functions alike, for a floor of 99%. It is 99.4% now. Where no code outside the image can run, the library's export holders are kept out of what has escaped and are held by native code. The fix for a setter made with `Object.defineProperty` took "missing from escaped" for "nobody covers this receiver" and entered eleven of the program's setters with a library prototype as `this`. One line. * AOT: a module that a failed graph left linked runs its body ahead of a mock all.ts: `import "./throws"; import "./tax"`. `await import("./all")` rejects with what throws.ts threw. Then `vi.mock("./tax", () => ({ rate: 0.5 }))`: plain runs tax.ts first; compiled code never ran it. The next test file's `import("./tax")` then read 0.5 for 0.2, without an error, and where tax.ts itself throws, plain's `vi.mock()` throws that error and ours returned. * AOT: `new WebAssembly.Suspending()` and `new WebAssembly.Tag()` without an operand gave another error `new WebAssembly.Suspending()`: "Argument 0 must be a function" for "new WebAssembly.Suspending() requires 1 argument". `new WebAssembly.Tag()`: "undefined is not an object" for "WebAssembly.Tag constructor expects the tag type as the first argument." An operand that is not written was handed over as undefined. Two rows of the table of built-ins. * AOT: a JSON.stringify in a run without compiled code no longer watches levels Outside aot/: runtime/JSONObject.cpp +8 -4. `#if ENABLE(AOT)` is on in every Bun, so in a plain run a `JSON.stringify` whose text passed 64 KB turned the compiler's watch on levels on, and from there every nested value took the unlikely branch. No answer changed. `m_vm.hasCompiledCode()` now stands in front at both places. * AOT: build time: what may change a list's length is looked for once `{ const l = new Array(n); for (let k = 0; k < n; k++) l[k] = { v: k + 1 }; r.push(l); }` n times in one function: 400 such statements cost 84 G instructions. Every list is made with the same number, so each loop's `k < n` may speak of any of them, and they were tried in turn. No generated code changes. * AOT: the run time stops a program that changes a compiled object in a way the compiler concluded never happens With a plain message, in every build and not only in the checking ones. Compiled code is unchanged. One planted fault comes with it. * AOT: SSA repair no longer gathers the edges into a wide join The edges into a join of 16 or more were grouped by their arguments, and the grouping compared the arguments before they were renamed. No program is known to show a wrong answer by it. Found in review of this pull request. The gathering is switched off. * AOT: nine rows of the table of run-time functions say what their bodies write The table now names every heap that these C++ bodies write, among them the field that a deferred closure is stored into and the words that moving a field to the extra properties changes. * AOT: "ASSERTION FAILED: m_moduleEnvironment" under `bun test --aot` in a build with assertions An import that resolves to a compiled record has no module environment. The block that fills import slots for a shared executable now leaves it out. It made runner-suites/mock-module-4, -5 and -6 red on Bun's ASAN lane. Outside aot/: runtime/JSModuleRecord.cpp +6: an include, and five lines inside `#if ENABLE(AOT)` that begin with `vm.hasCompiledCode()`. * AOT: the list of planted faults in compile-call.def is laid out anew The same words. Two groups of them change places and three get lines of their own. With the former layout the generated compile_call_enums.rs had 1,001 lines, and the schema's generator refuses that file: "people read it, so its emitter has to split it". * AOT: no added comments in the files that are shared with the rest of JavaScriptCore The comment lines that this pull request added under b3, b3/air and runtime go: 50 lines in 13 files. Two comments in b3/B3Policy.h that it had reworded get their old words back. No line of code changes. * B3: `continuesAfterMovedLoadReports` for `reportsOfMovedLoadsComeBack` A rename, and nothing else: the member of ValidationPolicy at four places, and the local `reportComesBack` becomes `continuesAfterReport` at six in B3MovedLoadCheck.cpp. Shared with the JIT: b3/B3Policy.h +1 -1, b3/B3MovedLoadCheck.cpp +6 -6. * AOT: `spyOn(Bun.CryptoHasher.prototype, "algorithm", "get")` failed in a test run compiled as two layers "TypeError: Cannot spy on Bun.CryptoHasher.prototype.algorithm because built-in objects are read-only in compiled code, and the build didn't see a test replace this one", with the spy written out in full. So too `Bun.CryptoHasher.prototype.byteLength` and `Bun.SHA1.prototype.byteLength`. The spy's question went to one image, the one that ran when `spyOn()` was called, and another had locked the holder. Every image of the realm is asked now. * AOT: the compiler aborted in ExpandIteration under a lowered limit `function f(l) { return [...l].length; }`, called with a list, a Set and a string, built with `--aot-limit=MaxVersionedIterationBytes:1`: exit 134, "Stopped while compiling in ExpandIteration". The limit promises a loop that is not optimized. Default builds do not reach it. * testb3: two graphs take their arguments eagerly An ArgumentValueList appends a new ArgumentReg to its block at every `arguments[i]`. Fetched late, it landed behind the Check that uses it in one test and behind the block's Branch in the other. Both are faults of the tests' own graphs, seen only where B3 validates. Upstream's own cases write `.eager()`. Shared with the JIT: b3/testb3_bun_placement.cpp +1 -1, b3/testb3_bun_selection.cpp +1 -1. * AOT: a hardened arm64 executable starts where a page has 64 KB It aborted at start: exit 134. Found in review of oven-sh/WebKit#972. Generated code changes in hardened Linux arm64 builds alone; every other build gets a macro's new name and nothing else. * AOT: the image that runs is called so A local in AOTBuiltinCells.cpp is renamed from `first` to `running`: it is the image that runs, not the first of the realm. No behavior changes. * AOT: two imports that wait for one asynchronous mock factory `mock.module("./lib.mjs", async () => (await later(), { v, kept }))`, then `await Promise.all([import("./user.mjs"), import("./other.mjs")])`, where both import lib.mjs: "ReferenceError: Cannot access 'v' before initialization". Plain gives both the mock. The first import that waited took the factory's promise off the list, and the second found nothing pending. * AOT: `vi.mock("node:fs")` without a factory failed with "Circular import" in a run compiled as several programs So too `vi.mock("node:path", { spy: true })`, `jest.mock("node:os", () => ({ ...jest.requireActual("node:os"), .. }))` and a `__mocks__/fs.js` that calls `jest.createMockFromModule("fs")`: 'error: Circular import: "node:fs" is imported by a module that its own mock factory loads while it runs', and the file ran no test. Plain, and a run compiled as one program, are right. The library's own request for one of its modules runs no waiting mock. With it, one line of flow analysis: what `jest.requireActual()` hands out of a module that native code makes has escaped. The front end's half in Bun needs it. * AOT: each image's "is asking the host" is put back to its own `JSModuleLoader::fetchSynchronously()` raised the flag for every image of the realm and restored all of them to the OR of what they were, so one image in such a scope left the others stuck at true. With one image nothing changes. And where the loader's registry has nothing, the other images of the realm are asked for a record that is not compiled. Outside aot/: runtime/JSModuleLoader.cpp +7 -6, inside `if (vm.hasCompiledCode())`. * AOT: "Unchecked JS exception" from `spyOn()` of an export behind two `export *` JavaScriptCore's exception-scope validator aborted the run: `Modules::tryReplaceExport()` had no throw scope. It shows only with the validator on, as on Bun's ASAN lane, where it made test/aot/three-layers-agree.test.ts and three-layers-agree-2.test.ts red. * AOT: six more calls into the compiler's run time that a run without compiled code made ask first Outside aot/: runtime/JSMicrotask.cpp +3 -2, runtime/JSObject.cpp +2 -2, runtime/RegExpConstructor.cpp +2 -1, runtime/RegExpGlobalData.cpp +2 -1. Each stood inside `#if ENABLE(AOT)`, which is on in every Bun. Each now asks `vm.hasCompiledCode()` first. No answer of a plain run changes. * AOT: in a test run a built-in object takes a name that it only inherits `spyOn(Uint8Array.prototype, "set")`, `Uint8Array.from = f`, `Object.defineProperty(Map.prototype, "toString", d)` and a getter for `length` on `Uint8Array.prototype` failed with a TypeError: "a test can't add this one". * AOT: a mock of a JSON file never ran over layers `vi.doMock("./data.json", () => ({ default: ["mocked"] }))` in a test file, and `import data from "./data.json" with { type: "json" }` in what it imports: over layers the factory never ran and the importer read the real data, without an error. A run compiled as one program was right. * AOT: the enums of compile-call.def are generated as five Rust modules compile_call_enums.rs had reached the 1,000 lines that the schema's generator allows a file that people read. It stays, as five `pub use`. The same 3,125 generated items, the same schema hash, the same C++ outputs. * AOT: three planted faults for what lies in a frame Three words at the end of `enum Sabotage`, and the places in the frame-allocation passes and AOTEnvironments.cpp that read them. No ordinary build changes. * AOT: an executable's TypeError for a name that a built-in only inherits gave false advice `holder[name] = f` with `Uint8Array.prototype` and "set": the message advised "Spell out the object and the name where it does". No spelling opens that in an executable. An earlier commit of this pull request brought it. * AOT: the mock of a built-in module outlived its test file a.test.js: `vi.mock("node:fs", ..)`. b.test.js: no mock. src/uses.js, which both import, reads `existsSync` by the default, the namespace and a name. In b.test.js plain gives false, false, false; compiled over layers it gave "mocked" three times, without an error. * AOT: behind `export *`, a mock that took the original had no names barrel2.js: `export * from "./barrel.js"`. `vi.mock("./barrel.js", async original => ({ ...(await original()), x: "over x" }))`. user.js: `import * as ns from "./barrel2.js"`. Written reads were right; `Object.keys(ns)` was empty for plain's x and y, `ns[key]` was undefined and `"x" in ns` false, without an error. * AOT: build time: what runs each turn of a loop is looked for inside the loop "Does every turn of the loop pass this block" was answered by a walk from the loop's header that nothing kept inside the loop: it left by the exit and went through the rest of the function, for every append in a loop. No generated code changes. * AOT: `vi.spyOn(EventTarget, "prototype", "get")` ended the whole test run `vi.spyOn(EventTarget, "prototype", "get").mockReturnValue("spied"); typeof EventTarget.prototype`: plain gives "string"; compiled, the run ended with "AOT: built-in cell 48 (key 4024, EventTarget.prototype) is not in this realm". `vi.spyOn(WebSocket, "OPEN", "get")` read 1 for "spied", without an error. A spy on a member that compiled code takes for a constant is now refused with a TypeError. Bun's half calls `JSC::AOT::isMemberTakenForConstant()`, declared here. * AOT: one visitor names every member of ExpandIteration's arm state The abort under a lowered limit was a member added to `struct ArmState` and forgotten in a hand-written list. One structured binding behind the struct now names all eleven members, so a member added anywhere stops the build at that line. No generated code changes. * AOT: a mock laid over an ES module that has run adds no name to it `await import("./dep.mjs"); mock.module("./dep.mjs", () => ({ own: "mock own", real: "mock real" })); await import("./user.mjs")`, where user.mjs has `import { own, real } from "./dep.mjs"`: plain throws "SyntaxError: Export named 'own' not found in module"; compiled code loaded user.mjs, which read undefined and "mock real". So too with `jest.doMock()`, with `require()` of the importer, and through one `export *` or two. * AOT: a name that a compiled module's table lacks is refused in plain's class and words It was "TypeError: 'nope' was left out of the executable .. This is a bug in Bun." It is now a SyntaxError that begins with plain's sentence: "Export named 'helper' not found in module '<path>'." No run is known to reach it without a planted fault. Found in review of this pull request. * AOT: five counts of the front end become rows of the table of limits Five constants decided how far the front end looks into types, and nothing could lower them. They are rows of limits.def now, with the values they had, and `--aot-limit` lowers them. Default builds do not change. * AOT: a test that ends the mock of a built-in module itself `vi.mock("node:fs", ..)` hoisted, and a test calls `vi.doUnmock("node:fs")`: plain goes on reading the mock through the code under test until the test file ends. The fix for a mock that outlived its test file, earlier in this pull request, handed the module over at once, so the rest of that file read the real one, without an error. It is handed over where the test file ends. * AOT: seven spies on a `prototype` are taken again `vi.spyOn(FormData, "prototype", "get").mockReturnValue("spied"); typeof FormData.prototype` gives "string" in plain and on Bun's main. The refusal of spies on members taken for constants, earlier in this pull request, refused it too, and the `prototype` of Bun.Cookie, Bun.CookieMap, Bun.WebView, DOMException, PerformanceObserver and WebSocket. * AOT: SSA repair gathers the equal edges into a wide join again, and compares them behind the renaming The gathering was switched off earlier in this pull request because it compared arguments before they were renamed. It now runs behind the renaming of uses and holds each edge's own arguments against the first edge's: an edge that differs keeps its own and stays where it is. A release assertion counts the arguments of every edge. With a test in IR. Generated code changes as the gathering first changed it. * AOT: the verifier refuses a store that may create a field below an accessor `PutSlotOrAdd`, or a key of `PutSlots` that may add, on a field that flow analysis marked as an accessor on the prototypes is now an internal error. Those two operations declare no write of the member guards, and that is right only while no such store exists. A rule of the verifier, a diagnostic and two tests in IR. No generated code changes. * AOT: a body asks for the room of its array methods with its own `function pricesOfEven(list) { return list.filter(f).map(g); }`, called from many places of one long function: the helpers' bodies spent the caller's whole allowance for inlined copies, and then every `filter()` and `map()` inside them found it spent. Each copy was two loops, two lists and a call for every element, where the helper by itself is one loop. Speed alone. * AOT: the image lists the modules that a test may mock and that are loaded under another name "../a" and "../a.js" for a.ts, "../a" for a/index.ts. Three flags of a module in program-records.def, a table of the image (`modulesReachedUnderOtherNames`), and `isReachedUnder()` for Bun to ask. An executable's table is empty. * AOT: every image is asked before any is told of a copy from disk Where a realm runs several images, each was asked in turn whether a copy of a file may run from disk, and each that said yes set its flags at once: the flags were set before the overall answer was known. No program reaches the difference. Found in review of this pull request. * AOT: a loader that is out of sight hands out the objects of Bun's own modules import buffer from "node:buffer"; const mine = () => "mine"; JSON.parse("0", () => process.getBuiltinModule)("node:buffer").atob = mine; console.log(buffer.atob("YQ=="), buffer.atob === mine); // "a false" for "mine true" Hidden code fetches the module's object by the module's name, so the object had never escaped. Where a loader has escaped, the objects of all public modules have too. Bun's main is right here. Bun's half goes with this line: it puts back that `buffer.atob` and the global `atob` are one function, without which `Object.assign(buffer.atob, { kept: 7 }); atob.kept` was undefined. * AOT: `require()` of a module whose import had been mocked failed once the mock had ended `jest.doMock("./dep", ..); require("./user.mjs").seen(); vi.doUnmock("./dep"); require("./user.mjs").seen()`: "ReferenceError: Cannot access 'a' before initialization"; plain gives the real values. The commonest form: the mock ends with its test file and the next test file requires the user. The mock stood in place of a module whose body never ran, and a compiled `require()` of an importer that had run did not pay what was owed. * AOT: a door for Bun to say whether a program's texts are held, the engine's half One trailing member of ModuleHost, `holdsTextsOfProgram`, and the condition in `startImport()` that asks it. It is null in today's Bun, in the jsc shell and in testaot, and null means as before: by itself this changes nothing. It is for the order in which two `import()`s settle in an executable: `await Promise.all([import("./a.mjs"), import("./b.mjs")])`, where a.mjs and b.mjs import each other, fails with "ReferenceError: Cannot access 'fromB' before initialization". Bun's main is right there. Bun's half cures it. * AOT: behind the mock of a built-in module its importers read the module again a.test.js: `mock.module("node:util", ..)` ahead of the load, then `import("./src/uses.js")`. b.test.js imports the same file and has no mock. In b.test.js `all.format` and `format` were undefined ("TypeError: all.format is not a function") and `Object.keys(all).length` was 2 for 47. The namespace object listed the mock's names in the next test file, without an error, for 29 of 30 built-in modules tried. * AOT: `$<name>` in a replacement text reads any name off the groups class G { get n() { return "named"; } } const re = { flags: "", exec(s) { .. r.groups = new G(); return r; } }; RegExp.prototype[Symbol.replace].call(re, "a", "[$<n>]"); // [named] An executable threw "TypeError: 'n' was left out of the executable". Where the getter stores `this.reads = 1`, it printed `0[named]` for `1[named]`, without an error. A row of the table of built-ins, and the arm of flow analysis that serves it. * AOT: "this assignment always throws" is a build error only if every run reaches the write `if (Array.prototype.at) { ...; process.exit(0) } Array.prototype.at = polyfill` did not build: TS100014. The check asked only whether the write's block dominates every return of a module body that is always evaluated. A call that may end the process, or an explicit throw, ahead of the write now makes it the warning "throws if it runs", and the write is refused at run time as before. * AOT: the compiler's own check stopped the build where an object without a prototype is handed to a parameter declared as a list The meet of what flow analysis knows with what a former entry check claimed has two sound types, and neither lies inside the other; in one order the proofs were lost. What stores behind a former entry check hold is now said at the stores. With a statistic. Three stored programs are in Bun. * AOT: a mock in the middle of a chain of re-exports answers leaf.js declares x. barrel.js: `export { x } from "./leaf.js"`. barrel2.js: `export { x } from "./barrel.js"`. `vi.mock("./barrel.js", () => ({ x: "over x" }))`, the one in the middle. `import { x } from "./barrel2.js"`, or `ns.x` and `ns[key]` off its namespace object: plain and Bun's main give "over x"; this branch gave "ReferenceError: Cannot access 'x' before initialization", and with a factory that takes the original, the real x without an error. This is the run time's half, for computed reads and keys; the written reads are cured in Bun. * AOT: the schema's generator no longer refuses a long file The check on the lines and bytes of a generated file goes. Nothing but the check read its two limits. All 67 outputs are the same bytes. * AOT: a door for the original of a mocked package, the engine's half One more trailing member of ModuleHost and the run time behind it, with native tests. By itself it changes nothing. It is for node_modules/reads/index.js `const dep = require("dep"); exports.seen = () => dep.value();` and a test with `import { seen } from "reads"; vi.mock("dep", () => ({ value: () => "mocked" }))`: plain gives "real", because a module that Vitest externalizes gets the original of what `vi.mock()` mocked; compiled code gives "mocked", without an error. Bun's half cures it. * AOT: the answer of a loop's first question has one reader A type test put on the edge that enters a loop sends what fails there past the header, to the arm where the header's own question fails. All that the header defines was made again on that way, but not the header's type test itself: an arm that returns the answer, or branches on it again, read a value that had not run. With a test in IR. * AOT: five more calls into the compiler's run time that a run without compiled code made ask first Outside aot/: bytecode/RepatchInlines.h, llint/LLIntSlowPaths.cpp, runtime/JSObjectInlines.h (two places), interpreter/StackVisitor.cpp: +7 -5 in all, every line inside `#if ENABLE(AOT)`. Each now asks `hasCompiledCode()` first. No answer of a plain run changes. * AOT: values copied onto an object of the realm escape Object.defineProperties(Event.prototype, Object.getOwnPropertyDescriptors({ laid: { deep: function () { return 1; } } })); typeof new CustomEvent("x").laid.deep; // "undefined" for "function", exit 0 With `const shared = { n: 0 }` laid so as `counter`, `new CustomEvent("x").counter.n = 7; shared.n` ended the executable: exit 134, "StoreOfOtherType of field n .. which compiled code reads without a test". * AOT: a test that a branch narrows by stays in its loop, wherever the branch stands function f(a, b, c) { if (c === a) { c ^= 1; } for (let i = 0; i < 1 && (c === a || b !== c); i++) { if (a === c) { console.log(a === c); } } return c; } The compiler aborted at default flags: "Stopped while compiling f in PlaceTypeTests". Bun's main builds it. `c === a` in the head of the loop and `a === c` in its body are one value with two branches, and the hoisting of loop-invariant reads took it in front of the loop. With a test in IR. * AOT: a `require()` answers the original until an asynchronous mock factory has settled early.cjs: `module.exports = require('cjs-pkg')`. The test file imports early.cjs and says `vi.mock('cjs-pkg', async importOriginal => ({ ...(await importOriginal()), hello: () => 'mocked' }))`. A `require()` cannot wait for a factory: in plain it answers the original until the factory has settled. The run time's half; the rest is in Bun. * AOT: a receiver that leaves may be any object A second rule of flow analysis behind "an escaped built-in that climbs lets out what stands above". No program is known that answers otherwise than plain today. With a planted fault, a statistic and tests in IR. * AOT: no copy of a hot loop's reads for a list that may hold undefined const q = rows.map(r => r.qty); // one row has no qty for (let i = 0; i < q.length; i++) total += q[i] ?? 1; for (let i = 0; i < q.length; i++) if (q[i] == null) count++; // "5 0" for "5 1" A wrong answer at default flags, without an error. The pass copies a hot loop's reads for storage that holds any value, behind a failed test for storage of doubles. The copy left into the exit of the loop for doubles, into code where `q[i] == null` had been answered "no" by the storage and its read was gone. This only takes cases away from the pass. The tests in IR that say "there is a copy" are restated. * AOT: "Stopped while compiling" says that an earlier step may be at fault The step that is named may only be the first to read what another one left. The text now adds: "An earlier step may be at fault: --aot-check=verify-each-pass names it." * AOT: three more places look at the exception first The end of an async generator's step asked `!promise || scope.exception()`, so the second half was never asked where there was no promise. A namespace object asked to delete declared a scope with a check still pending. They show only under JavaScriptCore's exception-scope validator, as on Bun's ASAN lane. * AOT: what a mock replaced behind the stars of its file comes back when it ends leaf.cjs: `exports.x = "real x"`. barrel.js: `export * from "./leaf.cjs"; export const own = "own"`. user.js reads x off barrel.js. a.test.js: `mock.module("./barrel.js", () => ({ x: "mock x" }))`. In the next test file user.js still read "mock x", without an error; plain reads "real x". So too with a CommonJS package for leaf.cjs. * Revert "AOT: a JSON.stringify in a run without compiled code no longer watches levels" 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. Bun's main finds the cycle early: 300 calls in 0.1 s. Every answer was right. runtime/JSONObject.cpp is again as it stood before that commit of this pull request, byte for byte. * AOT: three more things ahead of a locked write make "this assignment always throws" a warning The first of them: a call that is handed a built-in that may end the process as its receiver or an argument: `process.exit.call(..)`, `.apply(..)`, `Reflect.apply(process.exit, ..)`, `[0].forEach(process.exit)`. Behind the commit that made TS100014 an error only if every run reaches the write. * AOT: a planted fault for the copy of a hot loop's reads One word in compile-call.def and the one condition in AOTArrayCapacities.cpp that asks for it. No generated code changes. Thirteen stored programs and the word's test are in Bun. * AOT: getters that a bundler's `__export` lays on an object of the realm escape function __export(target, all) { for (var name in all) Object.defineProperty(target, name, { get: all[name], enumerable: true }); } __export(Event.prototype, { laid: () => "mine" }); new CustomEvent("x").laid; // "'laid' was not compiled into this executable" for "mine" Where that idiom is recognized the getters are found by name, on a way past the rule that lets what is laid on a built-in escape. One line. * AOT: switch off the clamp of loop bounds On two random programs of testb3 its code parted from level 0. The tests that rest on it keep their expectations under a TODO. * AOT: the factory of a mock that imports the module it mocks, and fails, was called twice `mock.module("./m.js", () => { calls++; imports.push(import("./m.js")); throw ..; })`, then user.js, which imports m.js, is loaded. That import comes to the mock behind the end of the run, when the mock waits to be run again, so compiled code called the factory a second time; `bun test` calls it once. In a build with assertions it stopped at "module->status() == Status::Evaluated", as on Bun's ASAN lane. Outside aot/: runtime/CyclicModuleRecord.cpp +7, inside `#if ENABLE(AOT)` behind `vm.hasCompiledCode()`; runtime/AbstractModuleRecord.h +1: an inline `clearTopLevelCapability()`, which only that block calls. * AOT: the way to a mock goes on through a table that lacks the name barrel2.js has x by a star from barrel.js, which says `export { x } from "pkg"`, and "pkg" is mocked: "ReferenceError". barrel.js's table has no entry for x, because a table lists what somebody reads off the object of its module. Bun's main is right here. Behind "a mock in the middle of a chain of re-exports answers"; the front end's half is in Bun. * AOT: a planted fault for what stores behind former entry checks hold One word in compile-call.def, and the place in AOTEntryChecks.cpp that reads it. No ordinary build changes. * AOT: a mock of a built-in module that a test ends itself goes on for whoever was linked `bun test --aot` compiled as one program, at default flags, gave another answer than `bun test`, without an error. Under aot/runtime alone; no generated code changes. Two tests are in Bun. * AOT: the run time reads no overloads 234,241 bytes of read-only tables that only the compiler read had come to be kept in executables: an earlier commit of this pull request asked the table of overloads a question at run time. The run time no longer reads them. * AOT: the accessors of a library module that the test runner hands out are entered `const util = jest.requireActual("node:util"); Object.assign(util, { ...util })`, or `for (const key of Object.keys(util)) util[key] = ..`: "'formatWithOptions' was not compiled into this executable because nothing that the compiler can follow calls it". Plain gives no error. The member is an accessor of the module's export object, and its setter is called only by a store under a key that the build can't read. * AOT: a helper that mocks a package which the test file comes to through other files demo.test.ts: `import "./helper.ts"; import demo from "./demo.ts"`. helper.ts: `vi.mock("confirm", async importOriginal => ({ ...(await importOriginal()), default: wrapped }))`. demo.ts imports prompts.ts, which says `export { default as confirm } from "confirm"`. Over layers: "ReferenceError: Cannot access 'default' before initialization". Plain, and a run compiled as one program, are right. Bun's main has it too. * AOT: every image is told of a file that a plugin's `onLoad()` filter names Such a file was left to the host in its own image and the other images were not told, so in the image above its importer was served compiled where a run of one image leaves it to the loader. * AOT: what a copy writes onto an object of the realm escapes Object.assign(Event.prototype, { get laid() { return { deep: function () { return 1; } }; } }); typeof new CustomEvent("x").laid.deep; // "undefined" for "function", exit 0 With `get laid() { return shared; }`, `new CustomEvent("x").laid.n = 7; shared.n` ended the executable: exit 134, "StoreOfOtherType of field n". A function returned so: "was not compiled into this executable". * AOT: the run-time counters ask whether a VM still has instances A VM's free lists are counted for its first instance, and `instances.first()` was read without asking whether the list is empty. Where it is, the first of the instances that wait to be destroyed is taken. With a native test. * AOT: `vi.doMock()` leaves alone what a test file has loaded, through two re-exports too leaf.js: `export const x = "real x", y = "real y"`. barrel.js: `export { x, y } from "./leaf.js"`. barrel2.js: `export { x, y } from "./barrel.js"`. user.js imports barrel2.js, reads, calls `vi.doMock("./barrel.js", () => ({ x: "over x", y: "mock y" }))` and reads again: plain gives "real x" both times. One line. * AOT: the road of an asynchronous mock factory checks its exceptions Every `bun test --aot` run in which an import waits for an asynchronous mock factory (`vi.doMock("./m.js", async () => { await ..; return {..} })`, then `await import("./late.js")`) aborted under JavaScriptCore's exception-scope validator: "ERROR: Unchecked JS exception". An earlier commit of oven-sh/WebKit#974 brought…
* 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 oven-sh#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.
* AOT: the planted faults that drop an operation reach passes over the whole program, and a second one drops stores
Under the planted fault, the check behind a pass over the whole program is shown a copy of the first body that has such an
operation, without it; the program and every later verification are as they were. A second word, PassesDropStores: a store by name
goes. Only under `--aot-check=verify-each-pass` with a planted fault. Witnesses are in Bun.
* AOT: no warning about an import path where the build has read every name
`import(new URL("./x.mjs", import.meta.url))`, the same with .href, `import(import.meta.resolve("./x.mjs"))`,
`const n = ["a"][0]; require(`./lib/${n}.js`)`, `require(["./chi", "ld.cjs"].join(""))`,
`require(path.join(__dirname, "plugins", name + ".cjs"))` over a literal list, `import("node:" + ["process"][0])`: each warned
TS100064 "this import path isn't a constant. If it resolves to code that isn't in the executable, the import throws at run time",
though the build had read every name.
* AOT: the original that a mock factory takes of a package is the package's own compiled code
a.test.js: `vi.doMock("pkg", async importOriginal => { const original = await importOriginal(); globalThis.original = original;
original.inc(); return { ...original, a: "mock" }; })`; z.test.js mocks nothing and imports "pkg". `pkg.fn === original.fn`,
`pkg.count`: plain true, 1, and the body runs once. As one program: false, 0, and the body ran twice. Over two layers: true, 1, but
by the copy from disk, interpreted.
* B3: in versionLoops a value that another block uses was taken for a Phi of the loop's header
A wrong answer without an error. The cloner turns every Phi of the region into Identity(Get(variable)), and every value that
another block uses into a variable too; variableOfHeaderPhi() told a header's Phi by that look. A plain Identity(x) left in the
header, x a hoistable value of the loop that another block uses, looked the same: x's variable was read in front of the fast loop,
where nothing had set it, and that read stood in x's place in the whole fast loop. A variable with a single definition is no Phi of
the header. Found by testb3's random programs.
Shared with the JIT: b3/B3VersionLoops.cpp +3 -2. The phase runs only where `policy().loops.versions` is set; it is false by
default, and the JIT does not set it.
* AOT: the verifier's net is narrow behind three passes over the whole program
SplitFreshResults answered for every operation that goes; now for CallDirect alone. MoveGeneratorFramesToHeap: for
YieldBySavingFrame alone. The row of RemoveUnreachableFunctions is deleted: the pass takes whole functions away and nothing out of a
function that stays. Only what `--aot-check=verify-each-pass` judges. A red control for each is in Bun.
* AOT: six counts of the checker's queries are rows of the table of limits
MaxConstructorParentsFollowed 64, MaxHelpersFollowedToPrototypeLink 8, MaxFunctionsGivenThis 32, MaxCallsReadForParameter 16,
MaxTermsReadForProduct 16, MaxDeclaredSignatureDepth 3: bare numbers in Bun's sources until now, with the values they had. Goes
with Bun's half.
* AOT: the image says which async functions hold no await of their own
The host asks it of the factory of a mock: inside a require() a factory that awaits is not called. For a function that the engine
compiles, the parser answers. A compiled function has no executable, so the front end's flag goes into a sorted table of the
program, and awaitsByItself() looks the function's source up in it. Goes with Bun's half, which calls it.
* AOT: the verifier's net is narrow behind InlineHeads
Its row answered for every operation that goes; now for CallDirect alone: the pass puts the head of a known function in place of a
call of it. Only what `--aot-check=verify-each-pass` judges. A red control is in Bun.
* AOT: whoever holds a module that was linked to a mock keeps that mock
held.js: `import { who } from "./dep"`; the test: `vi.doMock("./dep", () => ({ who: "one" })); const held = await import("./held.js");
vi.doUnmock("./dep")`, or a second vi.doMock(); `held.read()`. Plain: `one`, for as long as held.js is held. Compiled: on a CommonJS
file `undefined` at once, neither the mock nor the file; on an ES module, a CommonJS file and a package, the real module or the
second mock behind the next load by name. All without an error.
* AOT: under the address sanitizer, a reference held across a call that moves its table is stopped at once
Under the address sanitizer, a function that was run for the parts of an IR record reads the record once more, lists grow by one
element, and the table of functions moves at every append, so that a reference held across a call that moves its table is stopped
at once. With tests.
* AOT: an import waits for a mock factory that has started
One line in Modules::importUncompiled(). Its test, mock-original-fails.test.ts, is in Bun and is red without it.
* AOT: build time: two quadratic costs in PlaceTypeTests, and one where a loop holds many comparisons
A function with thousands of `x !== x` and of type tests, such as 20,586 loops `while (a !== a) a = [a];`: the graph was made anew
behind each cut, and a failed test was looked up in a list. And `for (..) { if (k > 0) ..; if (k > 1) ..; .. }`: for every relational
comparison all blocks of the loop were walked before it was asked whether the comparison tests one of the loop's counters:
comparisons times blocks. No output changes.
* AOT: build time: the walk that feeds a built-in that walks its argument has a statement of its own
The solve of flow analysis grew worse than a square on a chain of objects handed to a built-in that walks it, such as
JSON.stringify or console.log. The solver makes a statement that has run often wait while any other can run, and the walk that
feeds such a statement was part of that same statement, so at every level of the objects nothing else was due.
* AOT: a request to terminate a Worker waits behind an entry file that has run
`const w = new Worker('require("worker_threads").parentPort.postMessage([1, "two"])', { eval: true });
w.on("message", m => { resolve(m); w.terminate(); })`, in the company of a large project, in a build with assertions: "ASSERTION
FAILED: module->evaluationError() == exception->value()", runtime/CyclicModuleRecord.cpp, evaluate(). About one run in seven.
* AOT: `vi.spyOn(Bun, "origin", "get")` was refused
`vi.spyOn(Bun, "origin" | "isStandaloneExecutable" | "version_with_sha" | "ModuleGraph", "get")` answered "TypeError: Cannot spy on
Bun.origin because built-in objects are read-only in compiled code, and a test can't replace this one: it is read without being
looked up"; `bun test` makes the spy. As one program at default flags too, as soon as a file of the project uses Bun at all. A member
that changes on its own is locked by no image.
* AOT: a store under a key that the build doesn't know enters a setter that deletes a field or calls a method
`import "node:util"; class C { z = 0 } const KEY = JSON.parse(..); function run(given) { const o = given ?? new C();
Object.defineProperty(o, "p", { set(v) { delete this.z } }); o[KEY] = 1.5; return o.z } run(); run([])`: `bun run` prints undefined,
undefined; the executable ended with exit code 134: "DeleteField on an object of layout 0 (C), which the compiler assumed never
happens". And with `set(v) { this.bump() }`: "`bump` was not compiled into this executable because nothing that the compiler can
follow calls it".
* AOT: a sliced or joined string typed as a union of string literals, as a Map or Set key, crashed or missed the key
`type T = "red" | "green" | "blue"; const map = new Map<T, number>([["red", 1], ["green", 2], ["blue", 3]]);
function use(x: T) { return map.get(x); }
for (let i = 0; i < 4; i++) console.log(use(i === 2 ? (text.slice(1, 6) as T) : "red"));` with a text that the build doesn't know:
plain prints 1 1 2 1; the executable crashed at the third line. With `(part + "em") as Unit` the key was missed. Map.has and Set.has
alike. At default flags.
* AOT: the verifier's net answers for what ResolveBuiltins resolves
Its row answered for every operation that goes. Only what `--aot-check=verify-each-pass` judges.
…port() in test runs (oven-sh#982) * AOT: a counter of which call of a mock meets which state AOT::countMockCall() returns at once unless the path for counts is set. It changes no answer. Goes with Bun's half, which calls it. * AOT: `vi.stubGlobal` defines its name on the global object however its function travels lib.js: `export const read = () => typeof someGlobal`. A test: `run(vi.stubGlobal)` with `export const run = f => f("someGlobal", "stubbed")` in another file; `[vi.stubGlobal][0]("someGlobal", "stubbed")`; `const held = { stub: vi.stubGlobal }; held.stub(..)`. Plain: `string`; compiled: `undefined`, without an error. Also through a static field, an instance field, a Map, a generator, Object.values, a spread, a closure of another file. Goes with one word in Bun's table of built-ins. * AOT: under `--aot-check=verify-each-pass`, a body that is split tells the check With --aot-check=verify-each-pass, the pass that splits a function behind its entry checks tells the check that the function's body was replaced. The check compared the old body with the function that tests the arguments, and a build with assertions stopped there. * AOT: under `--aot-check=verify-each-pass`, a function that a pass leaves without values is looked at The check compared nothing where the body behind the pass had no typed value: meant for bodies that are not typed yet, it also held for a body without any value, so a pass that empties a function was seen by nobody. A red control is in Bun. * AOT: five points of a review of oven-sh#978 - Under `--aot-check=verify-each-pass`, the check of a pass that answers "unchanged" held the numbers of blocks against each other and not the numbers of values: a value in no block went unseen. With two planted faults; their witnesses are in Bun. - In updateIntegersInMemory() a user that is listed already is the last one listed, so the search through the list goes. - In isSettledByMock() two operands without effects change places, the cheaper one first. - The counter of refusals tells a refusal by longer parts of the run time's own sentences: a program may well say "compiled code" itself. - A native test compares the bits of two numbers of values. No generated code changes. Outside aot/: runtime/AbstractModuleRecord.cpp +1 -1, in a function that a run without compiled code never calls. * AOT: an unsigned number wrapped in holdText() where a text's turn had passed Found in review of oven-sh#978. A text whose turn has passed adds no levels. Its test, chained-imports.test.ts, is in Bun: it observes the order in which chained import() calls settle. * AOT: `--explain=statistics` counts the Map and Set lookups emitted in line, and `--explain=missed-optimizations` names some Three rows: InlineHashLookups, TagLookupsWithConstantKeys, TagLookupsWithOtherKeys. The sites of the third are named: "missed: lookup-key-not-constant at <file>:<line>:<column>". Goes with Bun's half. * AOT: a call on the first of two objects of a constructor function went to the prototype that the second was made with Two objects of one constructor function in one function, the function's `prototype` replaced between them and put back: `const q = new Q(); Object.defineProperty(Q, "prototype", { value: second }); q[key] = 1; const r = new Q(); ..put back..; q.describe()`: plain "first", compiled "second", without an error, at default flags. The step of ThreadJumps that sent an unlikely way past the next test of the class whose object it made is taken out. * AOT: a view behind `memory.toResizableBuffer()` kept its old length `const bytes = new Uint8Array(memory.buffer); memory.toResizableBuffer();` and then `bytes.length`, `bytes[0]`: `bun run` prints `0 undefined`; compiled `65536 9`. A loop over bytes made 65,536 turns for 4; a loop of six turns crashed. The row of the table of built-ins that toFixedLengthBuffer and toResizableBuffer share did not say that it writes the storage of typed arrays. * AOT: the store of a sort in place names the elements of every form of storage The library sorts a short list where it lies, and takes storage of int32 for contiguous storage on purpose. The store named B3's heap of contiguous elements alone, which B3 holds disjoint from that of int32 elements, so a read `a[i]` under the other name could have been carried across it. * AOT: a CommonJS file that an import() call names runs in its turn `const [a, b] = await Promise.all([import("./a.mjs"), import("./b.cjs")])`: in node and in `bun build --compile` a.mjs runs first; compiled ahead of time, b.cjs did. Where b.cjs has `require("./a.mjs")` and a.mjs calls `import("./b.cjs")`: the keys of b are `default,fromB`; compiled `default` alone, and b.fromB undefined. Two lists of known differences in Bun get shorter with it. * B3: a decoded narrow cell may be promised dereferenceable The validator's rule for a promise of dereferenceable bytes also takes Add(ArgumentReg, Shl(ZExt32(y), constant)) with y no constant. Found in review of oven-sh#978. Shared with the JIT: b3/B3KnownProperties.h +8 -2. Under B3's default policy no promise is ever stored, so the rule is never asked in the JIT. * B3: a copy that threadJumps makes for one way in forgets what was promised about it threadJumps copies a tail for each way in and puts the Phi's input for that way in the Phi's place; the copy kept the promises that were made of the Phi. A pure value of the copy is loop invariant then, and was hoisted with its load above the test that chose the way. A graph written by hand: 70 at level 0, a crash at level 2. Shared with the JIT: b3/B3RegionCloner.cpp +18 -2, B3RegionCloner.h +2, B3ThreadJumps.cpp +1 -1. The cloner is used by threadJumps, rotateLoops, versionLoops and unrollLoops alone; each runs only under a field of the policy that is false by default and that the JIT does not set. * AOT: a direct eval has a family of its own among the run-time operations main.ts `console.log(eval(process.argv[2]))` built with --disallow-code-generation-from-strings: `bun check --aot` said that the executable keeps the feature "Interpreter", which it used to leave out. A matter of size, not of what the flag forbids. Behind "a trimmed executable keeps the run-time operations that its image lists". Goes with Bun's src/trim/features/engine.def. * AOT: the observer of the states of a mock, third edition What the observer reports, not what a mock does: a built-in module under a mock said `none`; now `in place` where the mock replaces it and `laid over` where the loader held it. A new state: `ended while another waits`. Goes with Bun's half. * AOT: build time: the walk that feeds a built-in that walks its argument asks what is new Behind "the walk that feeds a built-in that walks its argument has a statement of its own". Each round the walk's statement asked the same reads of a set one member larger, so the solver let go of every node asked for before and took the new ones up: each part four times as dear at a doubling, on a build of 4,000 chained literals. No answer of flow analysis changes. * AOT: under `--aot-check=verify-each-pass`, a pass that moves a function's body without saying so stops the build With --aot-check=verify-each-pass, a pass over the program that moves a function's body into another function without saying so stops the build, which names the pass and the function. Before, the check compared the old body with whatever took its place. With a sabotage and a pair of tests. * AOT: over layers, the next test file read a mock that had ended with the first a.test.js: `mock.module("pkg", ..); const { seen } = await import("./src/uses.js")`. b.test.js: `import { seen } from "./src/uses.js"; seen()`. uses.js: `import { f } from "pkg"; export const seen = () => f()`. Plain: `real` in b.test.js; compiled, over layers: `mocked`, without an error. It came with "whoever holds a module that was linked to a mock keeps that mock" (oven-sh#979): five lines of that commit, in AOTModules.cpp, are taken back. * AOT: a flat string has a type of its own in the emitters StringEmitter::implOfFlatString() loads the text of a string's cell with a load that is marked immutable and promised dereferenceable. It took any pointer to a cell, and the emitter of Map and Set lookups handed it a sliced or joined string because the type held only literal strings ("a sliced or joined string typed as a union of string literals, as a Map or Set key, crashed or missed the key"). It now takes a FlatStringCell, which only three functions make, so that the mistake does not compile. * AOT: two layouts that are made equally often are ranked alike Where more layouts have the names asked of an object than there is room to test for, they are ranked, last by how often each is made: a sum of products over the whole program. Two layouts made equally often could come out one rounding error apart, and the last digit chose which of them is tested for: five lines in a library file moved twelve functions of unrelated packages. The key is cut short of its last digits. * AOT: a read of a string whose result has no room for "outside" is refused, not compiled unchecked Six branches read a string with no test of the position where the result's representation has no room for the answer to a position outside it (NaN or undefined): charCodeAt, codePointAt, at and the reads of a range. Nothing makes such a type today, so they were dead code; the build now stops there instead. * AOT: what a refused spy or mock says in a test run `TypeError: Cannot spy on 'answer' because module exports can't be replaced in a compiled executable` and `mock(module, fn) cannot replace a module that was compiled ahead of time: "<path>"` are what a user meets where the build missed a spy or a mock. In `bun test --aot` there is no executable, and exports can be replaced; the first named no module, and neither said what to do. The engine words both now, by the kind of run. Goes with Bun's half. * AOT: no direct call where an argument cannot be what a predicted callee takes An untyped file holds `box.run.call(AbortSignal, [controller.signal])`, `box` defined by nobody; a typed file of the same build has `class Holder3 { run(given: number[]) {} }` and one call `run([5, 6, 7])`. The build stopped: "verification failed after ResolveCalls; StoredTypeMismatch .. (AssumeType)", at default flags. * AOT: two assumptions are written down where they would break Compiled code makes the size of a table's index as `2 << shift` in 32 bits; the shift is 31 above 201,326,592 entries. The run time now stops where it grows a table past what the walk in line can address. No cost in compiled code. And one in SinkAllocations. * AOT: a getter that escapes and adds, deletes or freezes a member of `this` ended the executable `class Row { port = 80 } function peek(base) { const target = base ?? new Row(); Object.defineProperty(target, "value", { get() { this.seen = 1; return 1 } }); target.value; return typeof target.seen } console.log(peek(), peek(Object.defineProperties({}, JSON.parse("{}"))))`: plain `number number`; the executable ended with exit code 134: "AddProperty of seen on an object of layout 0 (Row), which the compiler assumed never happens". Also where the getter deletes a member, freezes `this`, or fills a memo by `??=`. The twin of "a store under a key that the build doesn't know enters a setter that deletes a field or calls a method". * AOT: a planted fault that drops an operation is used up only by a drop that is reported Behind a pass over the whole program the planted fault was used up at the first body where a drop was made, also where the check has no word for it, and no later body was tried: a red control could show nothing. Found in review. Only under `--aot-check=verify-each-pass` with a planted fault. * AOT: over layers, the mock of one test file leaked into the next dep.js lies in a lower layer than user.js and has run. a.test.js: `vi.doMock("./dep.js", () => ({ flag: "one" })); (await import("./user.js")).read()`: one. b.test.js, the next file, mocks nothing and imports user.js again: plain `real`; compiled `one`, without an error. Within a file too: behind vi.doUnmock() or a second vi.doMock(), an import() of user.js anew went on reading the first mock. `bun test` takes whoever was linked to a mock out of its loader when the mock ends, and a load that comes by makes it anew; a user above a mocked file is now linked anew where `bun test` loads it anew. * AOT: under `--aot-check=verify-each-pass`, a store behind a check that nothing passes stopped a build The verifier does not hold a value against the declared type of the place it is stored into where the value comes from a check that nothing passes: the code behind such a check runs in the generic version alone. `held = value` behind `value instanceof Kind` in a function called with .call() stopped a build under --aot-check=verify-each-pass. * AOT: `--aot-validate` refused valid programs with "internal compiler error": UpcastNotSubset after PlaceTypeTests PlaceTypeTests copies a loop into a fast and a slow one and lays a test of the type on the way back into the fast one; what the test leaves of an array's type had lost the array's proofs, and an upcast behind it was not typed anew, so its source no longer lay inside the type it casts to. The verifier's rule is right; the IR was inconsistent. Bun's main has it too. Its test is in Bun. * AOT: two points of a review of oven-sh#979 - FailedTests sizes its set once, where there is a failed edge or switch at all. - The native test of the net for held references, in builds with the address sanitizer, took any crash and any failed check of its child for "stopped"; it now asks for "AddressSanitizer: heap-use-after-free" in the output. * AOT: a whole number that the engine boxed as a double was stored as an int32 `const row = [9.5, 0.5, 2, 3].slice(0); row.push("label"); function look(x) { const list = [1]; if (typeof x === "number" && (x | 0) === x) list.push(x); return list[0] + list[list.length - 1]; } look(row[2])`: `bun run` prints 3, compiled 0, without an error. The same for `list.push(x, x)`, `[1, x]`, `[x, x, x]`; list.join() was right, every read in line wrong. Bun's main has it too. * AOT: two IR tests in place of stored programs The table-driven test of TryPutElements gets the two cases it lacked: storage for doubles is handed texts or objects. A new test of the effects of a memory's kept buffer. Tests alone; eight stored programs go out of Bun with them. * AOT: `bun test --aot` as one program settles import() calls as the executable of the same files does A test run took another road than the executable for import() calls, so the two settled them in different orders: over four tables of module graphs, 6,870 cells parted; now none. No cell of an executable changes. Where the executable is wrong today, the test run is now wrong in the same way. Goes with Bun's half: never one without the other. * AOT: a package that got the original read a preload's mock after it had ended The preload: `vi.mock("dep", () => ({ value: () => "preload" }))`, `import "./own.js"` (a file of the project that imports "dep") and `import { seen } from "reads"` (a package that imports "dep": under vi.mock() it gets the original). a.test.js: `vi.doUnmock("dep"); await import("dep")`. b.test.js: `seen()`, the package's first read. Plain: `real`; compiled: `preload`, without an error. It came with "whoever holds a module that was linked to a mock keeps that mock" (oven-sh#979). The set of importers that got the original was cleared at every test file's beginning, while the ended mock of a preload outlives the file; it is never cleared now. * AOT: the number of uses from which a property name gets a stub of its own is a row of the table of limits From 512 reads, stores or definitions of one name on, program-wide, the name gets the shared by-name code once more with its number bound. The 512 stood outside the table, so it could not be lowered, and no small program and no fuzz lane ever ran that code. Now MinUsesForOwnStub, with the value it had. Goes with Bun's half. * AOT: the program's first generator ended the executable where a prototype had got an indexed property `class B extends Array {}` made once through `new Proxy(B, {})`; `Object.defineProperty(V3.prototype, "0", {})`, then the same on its parent's prototype; then the program's first generator object: "panic(main thread): abort() called", exit code 134. Plain is right. Generators of a function that nobody has asked for its `prototype` share a stand-in for it; compiled code compares with a structure that is recorded when the program starts, and the stand-in was made at the first generator. It is made of the recorded structure now. * AOT: over layers, a barrel kept the names of a mock into the next test file dep.js: `export const flag = "real"; export const plain = "real";`. barrel.js: `export * from "./dep.js"`. user.js: `import * as barrel from "./barrel.js"`. The first test file: `vi.doMock("./dep.js", () => ({ flag: "one", plain: "one", added: "added" }))`, then import() of user.js. In the next test file, which mocks nothing, `barrel.added` read "added" for undefined, and Object.keys(barrel) `added+flag+plain` for `flag+plain`. With `export * from "./dep.cjs"`, `barrel.flag` read `one` for `real`. Without an error; as one program it was right. Behind "over layers, the mock of one test file leaked into the next". * AOT: the mocks that a helper laid are laid again for the next test file that imports it helper.js: `vi.mock("./tax.js", () => ({ rate: "mock" }))`; a.test.js and b.test.js import it; z.test.js doesn't. Plain: a mock, b mock, z real. Compiled: a mock, b real, z real, without an error, at default flags, as one program and over layers, with all five functions that mock. `bun test` undoes a test file's mocks at its end, evicts the modules that laid them, and runs them again for the next file that imports them. A compiled body runs once, so the calls are recorded and made again. * AOT: `prev2 = prev; prev = cur` in a loop gave prev2 the new prev `prev2 = prev; prev = cur; cur = cur + 3` in a `while`, over texts, in a cold or long function: a33|a33|a333 for a3|a33|a333, without an error. Bun's main has it too. Where the test in front of a loop fails on a back edge, PlaceTypeTests defines the header's parameters again, one behind the other in one block, and the repair of SSA took a use behind a definition for a use of that definition: a parallel copy done in sequence. Every parameter that is handed on as another is now read before the first is defined again. * AOT: a read that throws inside `try` in a loop over `[[], {}]` ended the executable `try { for (const e of [[], {}]) try { e.x.y; } catch {} } ..`: exit code 133. Bun's main has it too. Where type propagation cuts a block behind a value that never comes, the failing side went, with the read that throws; the probe in front of it throws nothing, and control ran into Unreachable. A probe behind which the code is cut becomes the full read, which throws into the handler. * AOT: under `--aot-check=verify-each-pass`, a handler that only threw on is said to go "verification failed after Select; .. (PropGet) .. may throw or run code, and the handler of its block is gone": a read of error.name in a `catch` whose handler is a `finally` that only pushes onto a list of the function's own. The pass is right: first the dead list goes with its push, then the handler only throws on. It now says so. A build without the flag compiles what it compiled. * AOT: `--aot-validate` refused a valid program: a function given by one caller, a Proxy over a function by another `const o = { m: f }; return o.m();` given a function by one caller and a Proxy over a function by another: "StoredTypeMismatch" at a NewObject, "after ResolveBuiltins; function 'use'". The verifier's rule is right: the failing body is the version of the callee for callers that pass some function, and its parameter's type named a class where the Proxy goes by its name. * AOT: README: whoever moves an allocation asks about the guards Two sentences: a pass that moves an allocation asks creationMayBeOrderedWithGuards() and guardsOrderedWithCreation(), as SinkAllocations does; a NewObject without a prototype operand takes the constructor function's prototype when its site first allocates, and the operation's effects do not say so. * AOT: a module stays mocked where the next mock already waits A mock that has ended stays in place while the next one waits. When a load then let go of it, the module went out of the set of mocked modules, though the next mock had put it there: a mock waited on a module that did not count as mocked, until its factory had run. No line of the mock sequences changes. * AOT: a validating build tests at run time that a flat string is flat where it is opened Behind "a flat string has a type of its own in the emitters": the one place where FlatStringCell trusts its caller is checked in a validating build, which names the operation that is being generated. With a planted fault, RopesPassForFlatStrings; its witnesses are in Bun. * AOT: one value under two local names, tested by truth, lost its null `function f(a, b = a) { if (!a) { if (b === a) seen.push(a ?? "default"); b ^= 1; } else a == b; return b?.[0]?.[0]; }` called with -1, 1, "", "1", null: plain `,default`, compiled `,null`, without an error. With `seen.push(b === a)`: true,false for true,true. Another form ended the executable with exit code 133. Bun's main has it too. PlaceTypeTests laid a cast to an operand's type that claimed more than was tested: of 288 variations 95 were wrong. * AOT: every source under aot/ compiles alone 47 of 967 sources did not compile by themselves: they leaned on whatever stood in front of them in a bundle of 8 or 16, so a new file that shifts the bundles could stop the build in a file that nobody had touched. Includes and a few names, in 36 files. Found in review of oven-sh#979. No generated code changes. * AOT: behind vi.resetModules() a failed import fails again in the same words Behind `vi.resetModules()` the second and every later `import("./wants")` of a file that imports what isn't there failed as `BuildMessage: ENOENT reading "not-installed"`; plain says `ResolveMessage: Cannot find package 'not-installed' imported from ..` each time. In `bun test --aot`, at default flags, as one program and over layers. The loader forgets a kept failure with the importer's entry, while the compiled record stays as it is; the failure is kept for such a request now. Outside aot/: runtime/JSModuleLoader.h +1, inside `#if ENABLE(AOT)`: `hasResolutionFailure()`, an inline const question about a table that the loader has already. Only aot/runtime/AOTModules.cpp asks it. * AOT: build time: one question for all receivers first Behind "a getter that escapes and adds, deletes or freezes a member of `this` ended the executable", which made a build quadratic: at every read whose answer holds an unknown value the receivers that have not escaped were told apart, and each group's own chain was asked: receivers times reads. 6,000 objects made at top level, half of them handed to code from disk: 22,599 M instructions for 11,154 M. Now one question for all of them first: does any accessor of the program lie on or above any of them under the key. * AOT: asking an error of a class of one's own for its own names runs Error.prepareStackTrace Where the engine is asked for an own name of an error, or for all of them, it makes the error's stack, and the program's Error.prepareStackTrace runs in the middle of the call. For an instance typed `Error` the compiler knew; for one typed by the layout of its own class (`class Failure extends Error`) the step was proven free of code, so a read of what the hook writes was merged over the call: `const before = calls; Object.freeze(failure); before + " " + calls` printed "0 0" for "0 1", without an error. Eleven operations. * AOT: an import() of a CommonJS file that is still running takes its names when it has ended b.cjs: `exports.early = 1; const a = require("./a.mjs"); exports.late = ..`. a.mjs: `import("./b.cjs")`. `const b = await import("./b.cjs")`: in node and in `bun build --compile` the keys are `default,early,late`; compiled ahead of time `default,early`, and b.late undefined. Where b.cjs throws behind the require(): in node the require() throws and the import() is rejected; compiled, the error was lost. * AOT: four more rankings go by a key that is cut short of its last digits Behind "two layouts that are made equally often are ranked alike": the order of the arms of a planned slot, which optional fields of a layout lie in line, which conditions are kept past the limit on tested types, and the percentiles of density all ranked sums of products over the whole program, so that a rounding error could choose. Each key is cut once; ties go as before. On 15 public programs one function changes. * AOT: four mends in the run time's modules - A test file with a hoisted `vi.mock("node:fs", ..)` never finished loading. It came with "`bun test --aot` as one program settles import() calls as the executable of the same files does". - Over two layers a mock factory that had failed was called once too often where the mocked module was taken out of the cache. It came with "over layers, the mock of one test file leaked into the next". - With the exception-scope validator on, keepFailureOfRequest() ended the run: it has a scope of its own now. It came with "behind vi.resetModules() a failed import fails again in the same words". - In a test run, a load of a compiled file by a computed path out of code from a string is refused in words that are true of a test run, and the run's closing lists count such loads. An executable's words are as they were. They share AOTModules.cpp. Goes with Bun's half. * AOT: the README says that no pass merges or removes a call of a host function Beside the advice for a pass that moves an allocation: no pass merges or removes a call of a host function or getter. The rows of the built-ins are relied on for that and are not checked against it: what a call changes may be read through another object, or be ambient. No code changes. * AOT: over layers, a package under vi.mock reaches a file that an earlier test file loaded user.js: `import { flag } from "pkg"`. a.test.js imports the user. b.test.js has `vi.mock("pkg", () => ({ flag: "mock" }))` and imports the user by a statement. c.test.js imports the user. Plain: real, mock, real. Compiled over layers: real, real, real. With vi.mock and vi.doMock, an ES and a CommonJS package; mock.module was right, and so was one program. `bun test` lets go of the importers that earlier test files left when a Vitest mock meets a loaded module that it leaves alone. The compiled counterpart was called only where a mock is laid over a module. Goes with Bun's half. * AOT: JSON.stringify of a ring of records through lists threw a RangeError for a TypeError `interface Ring { size: number; below: Ring[] }`, eight or more of them, each in the list of the one before and the first in the list of the last; `try { JSON.stringify(ring) } catch (error) { error.name }` in a function that runs twice. Plain: `TypeError: JSON.stringify cannot serialize cyclic structures.` Compiled: `RangeError: Maximum call stack size exceeded.` * AOT: an expectation is proven only inside its operand's type A verifying build stopped with "verification failed after SimplifyControlFlow; .. TypeOutOfDate .. (Expect)" on `if (Array.isArray(value)) record.m = value;` in a function that only `undefined` and an arguments object reach. An expectation counts as proven by the tests in front of it; what they leave was worked out without the operand's own type. * AOT: the verifier asks nothing of an assumption that nobody reads A verifying build stopped with "verification failed after SimplifyControlFlow; .. StoredTypeMismatch .. (AssumeType)" on a call through `.apply` with a list made at the call site, resolved behind a test of the callee to a function whose parameter is proven a list of small integers. The arm is dead, and nobody reads the assumption: no code is made for it, and none by it. * AOT: a list whose constructor or prototype was changed through an unknown object is no plain Array What map, filter, slice, concat, flat and splice answer on a list was proven a plain Array although the list's `constructor` (with Symbol.species) or its prototype had been changed: `const target = foreign.none ?? own; target.constructor = { [Symbol.species]: .. }`, or Object.setPrototypeOf, Reflect.setPrototypeOf or `__proto__ =` on such a target, where the list has escaped and the changing code is reached late. `Array.isArray(it.map(x => x))`: plain false, compiled true. `it.slice().constructor.name`: plain Sub, compiled Array. Without an error. Goes with Bun's half. * AOT: four mends in the run time's mocks and modules - Over layers: `vi.doMock(m, f1); const user = await import("./user.js"); vi.doUnmock(m)`, or a second mock; then `user.read()`. Plain reads f1's values for as long as the object is held. Compiled over layers it read `undefined`, the real module, or the second mock. - A preload has `vi.mock("./dep.js", () => ({ who: "preload" }))` and `import "./helper.js"`, which imports dep.js; a test file has `vi.mock("./dep.js", () => ({ who: "file" }))`. Plain: the file and what it loads read `file`, and helper.js goes on reading `preload`. Compiled: `file` everywhere. - The original of a built-in module under a mock. `mock.module("undici", async importOriginal => ({ ...(await importOriginal()) }))` behind a require("undici") failed with ENOENT in the first test file of a run; so did bun:sqlite, bun:jsc, bun:ffi, ws, node-fetch and three more. Under a bare vi.mock("bun:sqlite") behind an import of it, require("bun:sqlite") answered the class Database instead of the module's exports. - Behind "a request to terminate a Worker waits behind an entry file that has run": by reading, twelve more checks of that kind stand behind the body of a CommonJS file, whose record never stands on the stack of the evaluation. A request to terminate that comes back while the record has been evaluated without an error of its own is now filed on the record at the one door into our evaluation. They share AOTModules.cpp. Goes with Bun's half. * AOT: build time: a long row of classes, each with a method What was asked of each class by a walk to the top of its row is answered for all layouts at once, a step for each layout, at eleven sites. 3,125, 6,250, 12,500 and 25,000 classes on one thread: 8.2, 25.3, 87.6 and 324.8 G instructions before; 5.6, 14.6, 44.9 and 153.9 G now. What is left is no walk: a type is the sorted list of its layouts, and a row of n classes has n lists of n/2. Goes with Bun's half. * B3: an ahead-of-time build stops where fixSSA would invent a value Where no Set reaches a read of a variable, or an edge into a live Phi, fixSSA puts a zero there, so a phase that loses a Set gave a wrong answer without an error. Under `optimization.checksVariablesAreSet`, which only the ahead-of-time policy sets, the build now stops there with VariableReadBeforeSet. The JIT's policy has the flag off. Beside it: `validation.takesIdentitiesInHeadersForPhis`, off by default, brings an old fault of versionLoops back for a test; and clearer names in B3FoldPathConstants.cpp. * AOT: the one question for all receivers is asked of a set that the statement is woken for It came with "build time: one question for all receivers first". Under `--aot-check=flow-fixpoint` a build stopped with "FlowEndedTooEarly: the flow analysis was at rest, and 2 of its rules still add something when they run once more over the solved sets". The question was asked of the receivers that have not escaped by now, which leaves no watch: when a receiver escaped later, the statement was not woken. It is asked of all the known receivers now; who has not escaped is asked behind it, as before. * AOT: a spy on call, apply or bind of a library function is refused aloud `const s = spyOn(Array.from, "call"); Array.from.call(Array, [1]); s.mock.calls.length`: plain 1, compiled 0, and the test passed; with a mockImplementation the real function answered. Compiled code takes call, apply and bind of a built-in function for what Function.prototype holds. A function of the engine or the host refuses them as properties of its own; for a function of the library that is written in JavaScript spyOn() never asked. It is refused now in the words of the others. In `bun test --aot` alone. Goes with Bun's half. * AOT: one value under two local names, tested by truth, lost its null: the other order of the comparison It completes "one value under two local names, tested by truth, lost its null", which cured `b === a` alone. `function f(a, b = a) { if (!a) { if (a === b) seen.push(typeof (a ?? "d")); b ^= 1; } else a == b; return b?.[0]?.[0]; }` called with -1, 1, "", "1", null: plain `string,string`, compiled `string,number`, without an error. Bun's main has it too. PlaceTypeTests laid a third cast whose claim was taken while the proof still had the type made from the stand-in; like the other two it now claims the type of the value that it replaces. Goes with Bun's half. * AOT: build time: the walk for `module.exports` asks under that name alone `import { getPlatformProxy } from "wrangler"` and one test under `bun test --aot` took 369 s to build, 305 s of it in the rules of the flow analysis. For `require.cache[path].exports = other`, a store under a key that is no name went over every member of every built-in holder among the receivers, only to ask whether one is the exports of a module object. It asks under the name "exports" alone now, and not at all where the receivers may be an unknown object. 369 s become 271 s; the image's bytes are the same. * AOT: a stop in compiled code ends the process also where SIGTRAP has a listener With `process.on("SIGTRAP", ..)` listeners (the npm package signal-exit lays one) a stop in compiled code ran on (x64) or hung (arm64). Under the image's assembler policy breakpoint() now emits an illegal instruction (ud2, udf), so the process dies by SIGILL with the crash report; what fills the gaps between functions is hlt on x64. The JIT's policy and bytes are unchanged, which testmasm pins. Goes with Bun's half. * AOT: a count of the parameters that callers may let go of A new statistic, ParametersLetGo, counted in ChooseCallingConventions: how many pairs of a function and a parameter the callers may let go of. An IR test asks for it. No generated code changes. * AOT: seven mends in the run time's mocks - `mock.module("node:os", () => made)` or `vi.doMock("node:path", ..)` with a counting getter on the factory's object, over layers: four lines read "mock 2" for plain's "mock 1": one read of the getter too many, without an error. It came with "over layers, the mock of one test file leaked into the next". - q.cjs has `const k = require("./k.js")` and was loaded by the earlier test file; behind `vi.doMock("./k.js")`, `require("./q.cjs").top()` read the real k for the mock's, without an error. It came with "four mends in the run time's modules"; three lines of that commit are taken back. What they had cured stands open again: `import * as early from "./early.js"` at the top of a test file, then `vi.doMock("./dep.js", ..)` and `await import("./late.js")`: early, which imported dep.js ahead of the mock, reads the mock's values; only in one realm over layers, and only where an earlier test file had loaded dep.js. - barrel.js has `export * from "./dep.js"`; an earlier test file takes the barrel's namespace object; a later one says `vi.doMock("./dep.js", () => ({ flag: "one", plain: "one", added: "added" }))` and loads another user of the barrel. Plain: `one,added,added+flag+plain`. Compiled: `one,,flag+plain`. As one program and over layers. - The same line over the layers that a run takes by itself, where the end of an earlier mock had the barrel linked anew too early. - node_modules/reads, an ES module: `export const seen = async () => (await import("dep")).value();`; the test has `vi.mock("dep", () => ({ value: () => "mocked" }))`. Plain: `real`, because Vitest leaves packages as they are. Compiled: `mocked`. - lib.ts has `export * as space from "./space"`; the module has run; `mock.module("./lib.ts", () => ({ space: { inner: "mock inner" } }))`. Plain: `space.inner` reads `mock inner`. Compiled: `real inner`, for a statement importer, a namespace object, import(), a captured value and a barrel. - A module whose body threw stayed failed behind vi.resetModules(), behind vi.doUnmock() and in the next test file; plain loads it again. config.js: `if (!process.env.KEY) throw ..`; with the key set behind a reset: plain `k`, compiled `threw KEY is not set`. They share AOTModules.cpp. Goes with Bun's half. * AOT: the checks of slot types are not compiled for size Under the checking modes alone (BUN_JSC_validateAOTRuntime, an image built with --aot-validate). Seven functions that run for every object that is still alive lay under a pragma that compiles a file's functions for size, which takes no inlining to speak of. They are compiled as other code is; what is printed before the process stops stays under the pragma. +384 bytes of text. Nothing changes at default flags. * AOT: what a call returned is quoted where it is called and is no function `expect(1)()`: plain `expect(1) is not a function. (In 'expect(1)()', 'expect(1)' is an instance of Expect)`, compiled `Expect is not a function`. So too `make(1)()`, `o.n.k()(2, 3)`, `make(make(1))()`, `make?.(1)()`, `make(...[1])()` and `make({ a: 1 })()`. Goes with Bun's half. * AOT: an accessor that a copy brings to a locked built-in is warned of `Object.defineProperties(Object.prototype, Object.getOwnPropertyDescriptors({ get extra() { return 41; } }))` and `Object.assign(Array.prototype, { get at() { return f; } })` throw a TypeError at run time, as documented, but the build gave no TS100014. With a data property the warning came. An accessor names its key as a property does now. * AOT: a build can write down what the flow analysis hands on `BUN_JSC_dumpAOTIR=FlowFacts=<path>` makes a build write what the flow analysis hands to ApplyFlowResults, a line for each fact, each thing named by its place in the source, so that two builds can be compared. A tool for testing: nothing changes where the word is unset. Goes with Bun's half. * AOT: Bun read under a key that nobody knows keeps every member `const d = ["$"].join(""); typeof Bun[d].braces`: plain and `bun build --compile` say `function`, compiled ahead of time `undefined`, without an error. `Object.getOwnPropertyNames(Bun[d])`: 7 names for 2. The same for Bun.sql and Bun.SQL. The flow analysis let a key that nobody knows name no member of Bun, on purpose; the read then answered a bare stand-in function, and a read off the stand-in is silent. Goes with Bun's half. * AOT: testaot says both types, and prints the body around a failed verification Where a type differs from what its rule gives, the message ends ": there stands X, the rule gives Y". With TESTAOT_SHOWS_FAILING_BODIES=1 and --single-process testaot prints the function in front of the run that failed and behind it. Two hooks in the verifier, null in the product, asked only behind a verification that has already failed. * AOT: a child of a native test that crashed says how it was started The launcher prints the whole command line of a child that crashed or hung. A test alone. * AOT: an import cycle is entered where the first import() call enters it - In executables, and in test runs as one program: `const first = import("./a.mjs"); await null; import("./b.mjs")`, a and b importing each other, a reading a name of b at once: compiled code threw a ReferenceError. The later call's graph was complete in fewer jobs, so the cycle was entered at b; node, `bun run` and `bun build --compile` enter it at a. Of 24,696 graphs on which the three agree, 2,438 were wrong and 252 are: all 1,067 errors and all 8 wrong values are cured, none is newly wrong; what is left are orders, wrong on Bun's main too. - In a test, `await import("./main.mjs")` whose body awaits `import("./top.mjs")`, top importing m2 (which imports b.cjs, which requires a.cjs) and a.cjs: the executable and every reference print `b start, a`; the test run printed `a, b start`, without an error. It came with "`bun test --aot` as one program settles import() calls as the executable of the same files does". - A test file with a hoisted `vi.mock("node:fs", ..)` never finished loading beside a test file that names `vi.resetModules()`. It came with the same commit; the first of "four mends in the run time's modules" had cured one form alone. - main.cjs `require("./mid.mjs")`; mid.mjs `import { loaded } from "./reader.mjs"`; reader.mjs `import defer * as deferred from "./deferred.mjs"; export const loaded = deferred.loaded`; deferred.mjs `export const loaded = import("./reader.mjs").then(..)`: plain and `bun build --compile` print `loaded`; compiled ahead of time the promise never settled while the program's CommonJS entry was still running. Bun's main has it too. They share AOTModules.cpp. Goes with Bun's half.
* AOT: the checks of slot types go by a plan for each layout
Under the checking modes alone. Holding every slot of every object that is still alive against its type was 20 to 23% of all user
instructions of the TypeScript-compiler benchmark under checked proofs: for each field anew, whether it is a brand or an accessor, where it
lies, its type and its range; and over half of the visits were to fields that the object doesn't hold. All of that is the same for every
object of a layout, so a plan is made at the first object of a layout. The plan says only whether a slot is outside its type; the old walk
then names it, so a report reads as before. Nothing changes at default flags.
* AOT: the file of what the flow analysis hands on says why
Behind "a build can write down what the flow analysis hands on": the line of a layout ends with one column more, why the analysis could not
rule more out. A tool for testing: nothing changes where the word is unset.
* AOT: a call of Math.random() is not removed or run twice
`setRandomSeed()` from `bun:jsc` makes the sequence of Math.random()
reproducible, so a program can count the calls. The compiler believed that
nobody could: the call was declared to write only the runtime's own state.
function roll(use) {
const r = Math.random();
return use ? r : -1;
}
Where `use` is false the call was removed, and every later number differed from
`bun run`. The same happened to a bare `Math.random();`, to an argument that the
callee does not read and to a property that nobody reads.
The call could also run twice. A typed function that fails a type test before
it has done anything visible starts again in its generic version:
function f(holder: Holder) {
const r = Math.random();
const inner = holder.inner;
return inner.x + inner.y + r;
}
With an `inner` that is not laid out as a Point, `r` was the second number.
Math.random() now also reads and writes external state, as Date.now() does.
Two tests had the old effects written into them and are restated.
* AOT: the refusal of a load by a dynamic path names the flag too
"Cannot load '<path>': this executable can't load a module from a dynamic path. Pass the file to bun compile as another entry point" now
goes on ", or build with --aot-allow-code-from-disk", as the other refusal of a load does. Words alone. Goes with Bun's half.
* AOT: native code that runs out of stack in an executable is reported
Native code that ran out of stack in an executable built with --aot ended the process with SIGSEGV, nothing on stderr, no crash report, and
a raw terminal left raw. Bun's crash handler registers an alternate stack; the engine then installed its own actions for SIGSEGV and SIGBUS
over it without SA_ONSTACK, and only the thread that started had an alternate stack. A thread that has none gets one, and the two actions
take it. Goes with Bun's half.
* AOT: seven mends in the run time's modules and mocks
- `mock.module("./m.js", () => { calls++; imports.push(import("./m.js")); throw ..; })`, then two `import()`s of m.js one behind the other.
Plain asks the factory each time (`failed 2`, `failed 3`); compiled as one program, the second was answered with the first's error and
nothing was called, without a sign.
- A mock that works, laid over the one that failed, then `import()` by a name that the build can't read: plain `mock`; compiled, rejected
with the first error.
- a.test: `mock.module("node:os", () => ({ EOL: "mock EOL" }))`, then a graph that ends in node:os and passes through barrel.mjs. b.test:
`mock.module("./barrel.mjs", () => ({ own: "mock own" }))`, then `import { own } from "./outer.mjs"`. Plain: `mock own`. Over two layers:
`SyntaxError: Export named 'own' not found in module outer.mjs`.
- main: `try { require("./file.cjs") } catch {}`; file.cjs:
`import("./file.cjs").then(.., error => console.log(error.message)); throw new RangeError("thrown by the file")`. `bun run` takes the file
out of require.cache at the throw, so the import() runs the file again and is rejected with what the second run throws. Compiled:
rejected with the first error, one run.
- A plugin of the run answers for `virtual:aliased` in onResolve; `await import("virtual:aliased")` twice behind an import statement of the
same name. `bun test` asks the plugin at every call; compiled, at the statement alone.
- a.test.js: `vi.doMock("./dep.cjs", () => ({ who: "one" })); const held = await import("./held.js")`; held.js:
`import { who } from "./dep.cjs"`; b.test.js: `import { read } from "./held.js"`. Plain and one program: `real`. Over layers: `undefined`,
though dep.cjs's body ran.
- main.cjs: `const a = require("./a.mjs")`; a.mjs: `import("./b.cjs"); export const fromA = ..`; b.cjs: `require("./a.mjs").fromA`. node and
`bun build --compile`: "a ends | main has a | b starts | b sees a". Compiled ahead of time, as `bun run`:
"a ends | b starts | b sees undefined | main has a". It goes as node and `--compile` now.
They share AOTModules.cpp. Goes with Bun's half.
* AOT: testaot can ask the laws of the algebra of types of every type of a program
`RUN: laws-of-types`: 26 laws of intersectWith, castTo, unionWith, without, isSubsetOf and overlaps over every pair, and for three laws every
triple, of the JavaScript types that a program holds: a line for each shape of pair that breaks one, and a sum for each broken law. With
TESTAOT_LISTS_BROKEN_LAWS_OF_TYPES=<file> the same behind every command of every test. A tool of the tests alone; no test pins its sums yet,
because they go by the table of built-ins that the engine is built with.
* AOT: a step of the build counts who reads a field that assumes more than a type says
aot/codegen/generator/reads-that-assume.ts counts, file by file, who names the heap of a string's value and who calls the four helpers for
flat strings, and holds the counts to aot/codegen/AOTReadsThatAssume.txt: 22 places, each with the reason why it is right. A place that the
list lacks stops the build before the first file compiles. `AOT_CHECK_READS_THAT_ASSUME()` in aot/AOT.cmake runs it here; Bun's build has
its own step. No generated code changes.
* AOT: the build warns of a store under a name that native code locks on a host object
`process.versions.node = "1.2.3"` and its like throw at run time, and the build gave no warning. The table of built-ins now says which names
native code locks, and the check for writes to locked built-ins reads it. It feeds a diagnostic alone: no answer of a program and no
generated code changes. Goes with Bun's half.
* AOT: the end of a mock gives back what the mock replaced
Behind the sixth of "seven mends in the run time's mocks". a.js and b.js both have `export * as inner from "./inner"` and both are mocked;
then `vi.doUnmock("./b.js")`. Plain gives back what b's mock replaced, `mock of a`; compiled code read `real`. A mock that replaces a
namespace that is handed on now keeps what stood there, and its end gives that back. A parameter that nothing read is gone. Goes with Bun's
half.
* AOT: a link that fails behind a computed name throws at the load, in plain's words
`for (const file of ["one.js", "broken.js", "two.js"]) await import("./" + file)`; broken.js: `import { nope } from "./dep.js"`. `bun run` and
`bun build --compile`: `SyntaxError: Export named 'nope' not found in module '..dep.js'.` Compiled ahead of time:
`ResolveMessage: Cannot find module '/$bunfs/root/broken.js' imported from '/$bunfs/root/broken.js'`. Where broken.js only imports the file
with the fault, the build was refused where `--compile` builds. Goes with Bun's half.
* AOT: the statistics tell the lines of packages, of declarations and of Bun apart
Four more statistics beside SourceBytes and SourceLines, for `bun build --compile --aot`'s new closing words, "compiled 57,464 lines in
18.64 s (3,082 lines/s)". Counters alone. Goes with Bun's half.
* AOT: the detail of a stale type names an operand only where there is one
It came with "testaot says both types, and prints the body around a failed verification". Behind a TypeOutOfDate the detail read the first
operand after asking only how many there are; an optional operand that is left out (NewObject's prototype) is an empty id, and testaot died
by SIGSEGV where it should have named the stale type. The block runs only where testaot has set the printer of types: no product build
enters it.
* AOT: a store that is listed at its allocation goes by the proof of the pass
A verifying build stopped with "verification failed after SplitUnions; .. (PropSet) in b0 may throw or run code, and is gone". The removal
is right: `v24 = NewObject ..; PropSet v24, v12 key="inner"; FinishObject v24` becomes `NewObject -, v12, ..`, only where the store's plan is
a plain store to a slot of an object that the function has just made. The check asked too much. Goes with Bun's half.
* AOT: a test: a number stored into a new extension has the owner's barrier
The barrier for a new extension is decided as if a cell were stored, whatever the value's type. A build that decided by the value's type
would have passed every other test. A pair of tests alone, as shipped and with the planted fault StoresNeedNoBarrier.
* AOT: require() asks what the imports under a type have made of a data file
`import typed from "./data.json" with { type: "json" }` and `require("./data.json")` in one program gave two objects in compiled code: a store
through one was not seen through the other, without an error. Under `bun run` require() hands out the namespace of what the import made,
whose default is that one object; `bun build --compile` has one object too. Goes with Bun's half.
* AOT: a test pins the laws of the algebra of types over the types that it names
Behind "testaot can ask the laws of the algebra of types of every type of a program": `RUN: laws-of-types` takes for its corpus the types
that the test names, so its sums no longer go by the table of built-ins that the engine is built with. tests/types/laws.ir: 21 types, 20 of
26 laws hold; the sums of the other six may only shrink. Tests alone.
* AOT: a delete by key looks at the key, and an assignment asks for the target's prototype
Two wrong answers of executables at default flags, without an error; Bun's main has both.
- `Reflect.deleteProperty(Object.freeze({ m: y }), "m")` answered true for false. Nobody read the field, so it was removed, and the delete
found nothing to refuse. A delete by key now looks under its key, as a test does.
- `Object.assign({ __proto__: { set a(v) { o.b = "S"; } } }, o).b` gave "x" for "S". The expansion reads every field of the source first,
and the setter that the target inherits ran too late. Such a call stays a call.
Goes with Bun's half.
* AOT: the file of what the flow analysis hands on has no blind spot
Behind "a build can write down what the flow analysis hands on": a check that simply stays, and a call whose callees nobody knows, had no
line, so two builds that differ in hundreds of them read alike. Every check and every call of a value in a function that is entered has a
line now. A tool for testing: nothing changes where the word is unset. Goes with Bun's half.
* AOT: Bun read under a key that nobody knows keeps what code of the library makes, not every member
It narrows "Bun read under a key that nobody knows keeps every member". Where that rule fired, all 115 members of Bun counted as used,
Bun.FFI and Bun.Transpiler among them, and `--trim` kept the run time whole: one large application grew from 428.9 to 475.8 MB. Only the 16
members that code of the library makes ever got the bare stand-in whose members read `undefined`; they alone are kept now. What native code
makes is there whoever reads it. `typeof Bun[d].braces` stays `function`. Goes with Bun's half.
* AOT: testaot asks for its process id the portable way
It came with "testaot can ask the laws of the algebra of types of every type of a program": `<unistd.h>` and getpid(), for the name of the
file that TESTAOT_LISTS_BROKEN_LAWS_OF_TYPES asks for. The Windows lanes have no such header: "fatal error: 'unistd.h' file not found".
getCurrentProcessID() of <wtf/ProcessID.h> instead. A test tool alone.
* AOT: a long text with accents is encoded by the library
`new TextEncoder().encode(text)` and `.encodeInto()` of a Latin-1 text with accents took 10 to 14 times as long in an executable as under
`bun run`: one scalar loop counted the text and a second wrote it a byte at a time. Over 16 characters simdutf does both now, as plain Bun's
encoder does; up to 16 characters the old loops stay. Goes with Bun's half.
* AOT: a call for a module of a graph under way settles behind the call for the graph
main: `import("./a.mjs").then(x); import("./b.mjs").then(y)`; a.mjs: `import "./c.mjs"`; b.mjs: `await import("./c.mjs")`. node, `bun run`
and `bun build --compile`: b begins, c runs, a runs, x, b ends, y. Compiled ahead of time: b begins, c, a, b ends, x, y, without an error.
The modules ran the same; the order in which the calls settle didn't. Goes with Bun's half.
* AOT: a refusal that the run time cannot decide decides no exact test
At the IR alone: no JavaScript was found that shows it. `v1 = TypeTest v0 testedType=builtin(ImportMeta)`; on its refusal
`v2 = AssumeType v0 provenType=number|builtin(ImportMeta); v3 = TypeTest v2 testedType=builtin(ImportMeta)`: the verifier admits it, and
SimplifyControlFlow decided v3 false and assumed a number. With an IR test.
* AOT: what only an image of the library alone reached is deleted
The engine's half of the build option `--compiledLibrary`, which Bun no longer has: two options of the compile call, the flag of an image
(its bit stays free; no other flag's bit moves), and what only they reached. It needs a Bun without the option.
* AOT: a planted fault that drops the card of a variable
A word for the tests, and an IR test that is red with the word and green as shipped. No product code. Goes with Bun's half: each is red
without the other.
* AOT: a test run lists the compiled files that ran from source, and why
In `bun test --aot` a file that the image holds ran from its text in the interpreter, without a word, behind `import("./a.js?" + n)`,
`vi.resetModules()`, `delete require.cache[..]`, and where a `Bun.ModuleGraph` imported it: such tests don't test the code that an
executable runs. The run time notes each such file with its reason, for one list. No answer of a program changes. Goes with Bun's half.
* AOT: the stubs align with the same padding as the rest of the code
On arm64 the padding that aligns the stubs was still a breakpoint (the assembler's align() emits brk), so with `process.on("SIGTRAP")`
listeners a wild jump into it hung. The stubs align with udf now, as the rest of the code does. x64's bytes are unchanged. Goes with Bun's
half, whose arm64 case is red without this.
* AOT: an image that has noted no load has no moments
By reading: no program was found that fails. Two functions of the run time's modules read the list of the moments of loads without asking
whether the image has noted a load, and three callers came without the guard that the others have. Both ask it themselves now: an image
without moments answers with the module's own place, and "not loaded in this test file".
* AOT: the recheck of guards reads only what its functions reach
Build time under the program verifier alone: no byte of any output depends on it. Where the first verification reports a guard that isn't
available, the summaries of effects were computed over the whole program from nothing, twice, to verify a few functions again. Now they are
computed over what the bodies of those functions name; every other function counts as unknown code.
* AOT: the iterator of a map is told by its cell
The table of built-ins gives %MapIterator% its cell type, so the algebra of types tells it from Map and Array (the meet is never, both
ways). Two tests, red without it: seven iterators pass, made by the shell, by compiled code, of a class below Map, with the prototype taken
away; fourteen others, the prototype object and an heir of it among them, don't. Goes with Bun's half.
* AOT: a guard that is not available names what may have ended it
Behind GuardNotAvailable the verifier's detail says which guard it is and, of the operations in front of the use that may end what it
proves, the nearest six. It needs no printer of types, so a build under `--aot-check=verify-each-pass` says it too. A diagnostic alone.
* AOT: a cast may stand between the header's parameter and the step of a for-in
`bun build --compile --aot main.js` ended with exit 134, 'panic: abort() called', 'Stopped while compiling a function (main.js:4) in
ExpandOwnKeys'. Bun's main has it.
`function f1(L) { for (const q in L) { if (L[+q] === 2) return 1; } return 0; }`, declared in a callback that is kept and called later, and
called in a try in a loop over `const L = [2]`. A test's body is such a callback. The pass took the position that the step takes for a
parameter of the loop's header, which no rule of the IR says: a cast may stand between them.
* AOT: the iterator of a set is told by its cell
As "the iterator of a map is told by its cell", for %SetIterator%. Its lines in the same two tests: seven iterators of sets pass, whoever
made them; the iterators of maps and sixteen others don't. Goes with Bun's half.
* AOT: an object held from ahead of the mock never reads the mock
`import * as early from "./early.js"` at the head of a test file, then `vi.doMock("./dep.js", ..)`: early.js read the mock's values of dep.js
where `bun test` reads the file's, without an error: in one realm, in a run that is built in layers, with dep.js loaded by an earlier test
file. Of the two places where a mock can answer a read from outside, only the first respected that the module is read past its mock. Goes
with Bun's half.
* AOT: one forward walk builds a body anew in place of the compaction's five helpers
aot/ir/AOTForwardWalk.{h,cpp}: a walk that reads a body once, in an order in which a block stands behind the blocks that dominate it, and
builds the body anew while it reads. compactFunction() calls it, and the five static helpers that it replaces go. The order of the blocks is
today's. No output changes.
* AOT: an import of a module that compiled code began settles when it has run
main.mjs: `require("./mid.mjs")`; mid.mjs: `import "./a.mjs"; require("./b.mjs")`; b.mjs: `import("./mid.mjs").then(settled, settled)`.
`bun run` and `bun build --compile`: settled. Compiled ahead of time the promise never settled: exit 0, and nothing said. The same with
`await import("./mid.mjs")` in the entry, with `await` in an async function of b, and with two calls. Goes with Bun's half.
* AOT: the jobs behind a delivered text run ahead of the next file's body
Under `bun test --aot`, `import("./a.js").then(() => setTimeout(() => { import("./b.js"); /* thirty awaits, then a line */ }, 0))` ran b's
body ahead of the thirty jobs. `bun test` runs the jobs first, because a file's text comes in a later turn. It came with "an import cycle is
entered where the first import() call enters it"; Bun's main is wrong there too. Goes with Bun's half, which calls a new entry of the run
time.
* AOT: a call or a new in the place of what is constructed is quoted as Bun prints it
`const p = Object.create(K.prototype); new p()`. Plain: "K is not a constructor (evaluating 'new (Object.create(K.prototype))')". Compiled:
"K is not a constructor": the quote was missing, or wrong: 'new make', 'new new K', 'new q' for `new p.q()`. 71 of 158 forms differed. The
text of an error alone. Goes with Bun's half.
* AOT: a closure that a callee hands back keeps what it captured
`function lender(way, n) { let a = n * 2; const r = way(() => a++); a += 10; return r; }`, where `way` is one of fifteen functions at that
site and the one that runs is `function returnsIt(f) { return f; }`. The closure called later: plain prints `12 13`; compiled code printed a
wrong number or crashed. Goes with Bun's half.
* AOT: the recheck of guards scans for borrowed parameters only what it reads
Behind "the recheck of guards reads only what its functions reach": build time under the program verifier alone. Each of the recheck's two
readings still made the table of borrowed parameters from nothing, by a scan of every body of the program. Now the scan takes the bodies
below the functions under recheck, and the second reading takes the first one's table. `--aot-check=verify-each-pass` and a validating
build gain the second too.
* AOT: an import of a file of an image below walks its mocks once
`vi.doMock("./k.js", async () => { .. await .. }); vi.doMock("./m.js", async () => { .. }); await import("./a.js")`, where a.js imports k and
m, in a test file behind another test file whose mock's factory has run. `bun test`: k's factory begins, k's ends, m's begins, m's ends.
`bun test --aot` over two or three layers: k begins, m begins, m ends, k ends, without an error. For a file of an image below, import()
walked the mocks twice, and nobody read the second walk's answer.
* AOT: a copy of a parameter that holds -2 is not taken for 0
A wrong answer of an executable at default flags, without an error. It came with oven-sh#982; Bun's main is right.
const T = [];
function f(a, b) {
let c = b;
if (!b) {
if (c !== b) T.push(1);
}
b == c;
b?.[0];
if (a == b) {
b -= 1;
c--;
if (typeof c === "object" && c !== null && "id" in c) T.push(2);
}
T.push(b, c);
}
f(1, 0); f(-2, -2); f(1, ""); f(null, undefined);
console.log(T.map(String).join(" "));
`bun run` prints `0 0 -3 -3 NaN NaN`; compiled code printed `0 0 -3 -1 NaN NaN`. While type tests are placed, a condition's first proof
stands for all of its proofs, and a cast took its claim from such a stand-in: the type inside `if (!b)`, `undefined|0`. A claim is taken
only from a value that stood before the run of the pass. Two IR tests, one with a planted fault. Goes with Bun's half.
* AOT: what is asked of the objects of every escaped class is asked once
Build time, quadratic in a made-up shape and next to nothing on a real program. `class A0 {} class B0 extends A0 {} globalThis["a" + 0] = B0`,
n times: every class that is out of sight has the objects of every such class among its own, and one step of the flow analysis read every
member of every layout's set: n layouts by n objects. Now a layout keeps its own objects and two bits, and what is asked of every member is
asked of each shared set once. The answers are joins, so the order can't move them. Goes with Bun's half.
* AOT: a test that runs the frames pass alone gives the verifier the tables that the pass published
The test tool's road for one pass hands the verifier no results of the flow analysis, so under `check=VerifyEachPass` it reported
CalleeRetainsFramePointer for a callee that AllocateInFrames had just proven to keep nothing. Where the verifier's inputs hold no flow they
get the pass's own table. In a build the inputs always hold it: no output changes. With an IR test.
* AOT: two system calls at the start of an executable, and only its own handler is changed
Behind "native code that runs out of stack in an executable is reported", which cost every start four `rt_sigaction` calls and one
`sigaltstack`: a compiled hello world made more system calls than a plain executable. The main thread is no longer asked whether it has an
alternate stack, because the host gave it one before the engine started; and only SIGSEGV is looked at, but on Darwin, where running into a
guard page raises SIGBUS.
`SA_SIGINFO` says nothing about whose an action is: a library that sets its own SIGSEGV handler had `SA_ONSTACK` forced on it. Now only a
handler that lies in the executable's own text is changed. Goes with Bun's half.
* AOT: the mocks of one handed-on namespace stand in a stack
It replaces the way of "the end of a mock gives back what the mock replaced". m1.js and m2.js each have `export * as ns from "./h.js"`; a test
lays mock A over m1, then mock B over m2.
- `vi.doUnmock("./m1.js")` while B stands: compiled code read `real | real`; `bun test` reads `B | B`.
- Then `vi.doUnmock("./m2.js")`: the end gave back what B had replaced, A's value, though A had ended. Every later test file of the realm
read A, and nothing cleared it.
The mocks that replace one namespace stand in a stack that is kept with the module whose object is handed on. The latest answers; an end
takes its own entry out wherever it sits; when nothing is left the module's own object is handed on. There plain `bun test` leaves the first
mock's object in place: a difference that is meant. Goes with Bun's half.
* AOT: a declared iteration may step the iterator itself
It mends "the iterator of a map is told by its cell" and "the iterator of a set is told by its cell", under `--aot-validate` alone: no answer
was wrong at default flags. Every validating executable ended by SIGABRT at `Array.from(map.values())` and `map.values().toArray()`:
"AOT native call: .. internal state changed, the entry writes , not InternalFields". An entry that says that it iterates its operand writes
nothing by itself; the write is `next`'s, and native code steps the iterator.
* AOT: fix the macOS build
AOTForwardWalk.cpp uses entryBlock without including AOTCFG.h, which declares
it. AOTModules.cpp writes ArrayAllocationProfile inside namespace JSC::AOT,
where the name can also mean JSC::AOT::ArrayAllocationProfile, so the call to
constructArray() found no overload.
Both compiled only because of what stood earlier in their unified-source
bundles. Adding a file moved the bundles on macOS.
…and one walk (oven-sh#997) The middle and the back end of the ahead-of-time compiler are written anew. The flow passes and the B3 back end go; in their place stand a scan, a link, a plan, one walk over each function, a sweep and a code generator of its own. `ir/`, `codegen/`, `stubs/`, `runtime/`, `object/`, `image/` and `builtins/` are kept, and changed where they met the old middle. What changes outside `aot/`, by folder, file and line, and what a program that is not compiled ahead of time sees of it, is listed in the description of the pull request: 131 files and 539 hunks, of which 157 are on a road that such a program takes. WebAssembly ahead of time moves from OMG to BBQ; BBQ's code for a plain module was compared with main's over 4.3 million functions on x64 and arm64 and is the same. `Source/bmalloc` does not change. The 53 commits of the branch, each with what it fixes and what was run for it, stay readable on the pull request.
…runs with mocks, builds that grew (oven-sh#999) AOT: fixes behind oven-sh#997: silent wrong answers, a crash in test runs with mocks, builds that grew Fixes alone, on 42067fc. Outside aot/ five files change: see "Outside aot/". Fixed: - Old, silent, as users build: `receiver.f(swap())` called the function that the argument had just stored. With `class A { f(x) {..} } class B { f(x) {..} } class H {}`, `const held = new H(); held.f = x => x + 1000;` and `function swap() { held.f = another; return 1; }`, a loop over `[a, b, held][turn % 3].f(swap())` called what `swap()` had just stored under `held.f`, where `bun <file>` calls the function that lay there before the arguments ran: 308 for 208. The read of the method was left to the call, behind the arguments, where every other receiver's read counted as constant; a field that is appended from outside the constructor counted so too. It is asked of the planned layout as well (aot/walk/AOTWalkUnionTableCalls.cpp: +2 -1). The call through the table stays where nothing stands between the read and the call. 13 stored programs in Bun's tree. - Old, silent, as users build, the ordinary spelling among them: a later source of `Object.assign()` or of a spread was read as it was made, not as it is by then: `Object.assign({..}, defaults, this.options)` behind a getter; a source that a variable of the file, a captured variable, a field of a holder or a helper's parameter also holds. And `Object.assign(t, s, t)` with `t` a literal in sight laid the old fields over the new. A source is made again in sight only where it is a value of this walk that nobody else holds, and an operand behind the sources that names the target is the target as it is by then (aot/walk/AOTWalkCopiesOfObjects.cpp: +7). 26 stored programs in Bun's tree. - Old, silent, as users build: the order of an object's keys could be wrong behind a store during which code of the program runs (aot/runtime/AOTPropertyCachesOperations.cpp: +10 -1). It only narrows what a store remembers. 4 stored programs in Bun's tree. - Old: a test run that mocks a module and ends the mock could crash (aot/runtime/AOTModules.cpp, AOTModules.h, AOTModuleRecord.h, AOTTestRunFactories.cpp, runtime/AbstractModuleRecord.cpp, AbstractModuleRecord.h: +35 -9). Known: in such a test run a barrel of CommonJS stars with many importers loads slower with it: +0.7%, +7.9%, +19.6% at 200, 800, 1,600 importers, measured on the working tree. Not fixed here. - Old, silent, test runs with module mocks, an everyday spelling: `vi.doMock("./mid.js", async original => ({ ...(await original()), extra: 1 }))` where `mid.js` holds `export * from "./tools.cjs"; export * from "./early.js"` and `early.js` holds `export * from "./tools.cjs"`: the original had no names (`both=undefined,kept=undefined` for `both=file,kept=1`); `vi.importActual()` alike. The record of a CommonJS file and the record of its face name one binding (aot/runtime/AOTModules.cpp, one function: +3 -2). 37 files of stored test runs in Bun's tree. - Old, silent, test runs with module mocks: the factory of a mock that requires its own CommonJS file, where the build lets that file leave the program (a line of the run names its key in `require.cache`): `bun test` runs the file once and the mock lies over the real module; compiled, a TypeError (aot/runtime/AOTModuleEvaluation.cpp: +7). 15 files of stored test runs in Bun's tree. - Old: a build could hang on a program with a very wide switch, object or module (aot/facts/AOTFactsRegExpTexts.cpp: +11 -2). 4 stored programs in Bun's tree. - Old: a build could take much memory and time on a program that makes many long lists as it starts: the run that a build makes of the start of a program priced an operation at one, whatever it made. A place that is filled books its bytes in the room that stands, and sixteen places cost an operation; no new bound and no new number (aot/facts/AOTFactsRegExpTexts.cpp: +22). 2 IR tests, 1 test file in Bun's tree. It does not end the kind. - Old, loud: right typed programs were refused with TS100031: an `int32` out of `slice()`, `filter()` or `sort()`, and a cast that is only expected, asked where the walk no longer names what it casts (aot/walk/AOTWalkInt32Promises.cpp: +52 -2). 10 stored programs in Bun's tree. - Old, loud, the engine's half: a load under a name that nobody knows threw where `bun <file>` answers, though the program writes a library module's name as the key of a literal, of a store, of a class member or as a joined key (aot/facts/AOTFactsOfProgram.cpp: +34 -1). The other half is Bun's. - Slower, no answer wrong: in an executable built with `--external` or `--aot-allow-code-from-disk`, no module's value stayed in sight, so what one file of the library imports from another was no constant any more. What the internal files of the library hand out stays in sight (aot/facts/AOTLinkReach.cpp, AOTLink.cpp, AOTLinkFlow.h, AOTLinkTables.h, aot/image/AOTModuleTables.cpp, aot/runtime/AOTBuiltinModules.cpp, AOTModuleData.h, AOTModuleTable.cpp, AOTModuleTable.h: +93 -5). 3 new IR tests, 1 changed, 131 lines of native tests, 1 test file in Bun's tree. No time was measured on this tree. Outside aot/: - runtime/AbstractModuleRecord.cpp (+1 -1), AbstractModuleRecord.h (+3): for the crash in test runs, above. Both stand inside `#if ENABLE(AOT)`. The first is one more condition in a test that is reached for a record of compiled code alone; the second is one protected inline function. No member is added. A program that is not compiled runs neither. - wasm/WasmBBQJIT64.cpp (+1 -1), toward upstream's text: the baseline JIT of WebAssembly asks for atomics as upstream asks (`isARM64_LSE()`) where no image is built. On Linux both questions read one bit; no test. - JSTests/stress/intl-datetimeformat-language-change-mid-construction.js (+3), JSTests/BUN-TEST-DIFFERENCES.md (+2 -1): upstream's test on timing, which failed once on the arm64 lto-hardened lane at 3c42aae and passed when its job was run again, is marked `serial!`. On a build of this commit with the sources of Bun's side of these fixes, Linux x64, release, no LTO: it compiles and links; the engine's 2,567 IR tests pass (2,562 at 42067fc); the block of thread-local storage is 23,504 bytes, as before. Nothing else ran on it before this push: the stored programs and test runs of these fixes run beside CI. Each fix was seen by its author to turn its own programs on a build of the working tree; whether they turn on this text is what those runs are for. AOT: three native tests that need Bun's table of entries wait for it Tests alone. The last commit brought three native tests under exports-that-stay-in-compiled-code/ that name two modules of Bun's library. `testaot` as this repository builds it has the table of the stand-in host, where their IR text does not parse, so the four test jobs with the compiler failed: 4,307 passed, 3 failed, these three. aot/tests/AOTExportsThatStayInCompiledCodeTests.cpp goes back to 42067fc's text (-131). The fix that they test stays, with its three IR tests and its test file in Bun's tree. AOT: mocks in test runs: one more road to a crash; a file that throws below its own factory runs again On e8741fa. One file changes, aot/runtime/AOTModuleEvaluation.cpp (+3 -4). Nothing outside aot/. Fixed: - Old: a test run that mocks a module and ends the mock could crash: one more road (+1 -4). - New with a864536, silent, test runs with module mocks. That commit let the factory of a mock require its own CommonJS file where the build lets that file leave the program. Where the file throws on its way, what its run had begun was kept, and the next `require()` of it got the exports as far as they were filled, with no error. With `exports.early = "half"; if (..) throw new Error("part fails"); exports.late = "whole";` two calls in the factory answered `threw part fails`, then `half undefined`, and the file ran once; `bun test` answers `threw part fails` twice and runs the file twice; 42067fc answers with a TypeError each time. Seen in three test runs on a build of a864536's text. The run that threw is forgotten here too, as on the other road that runs an original beside its mock (aot/runtime/AOTTestRunOriginals.cpp) (+2). Three stored test runs (9 files) for Bun's tree, run by its own harness with the three that a864536's fix brought: 6 pass on the build below, 3 on a build of a864536's text, none on a build of 42067fc's. Corrected: - a864536's message headed the fault of that factory "Old, silent". It is a TypeError where `bun test` has none: loud. - a864536's message says of the fix for a build that takes much memory and time on many long lists "1 test file in Bun's tree". That file is not in Bun's tree: the message of c4d68cd4 in oven-sh/bun says why. On a build of a text that holds this commit's change and one other that is not in it, with Bun's side of this branch, Linux x64, release, no LTO: it compiles and links; the engine's 2,567 IR tests pass; the test runs above. This very tree was not built as one: its one changed file was compiled in that build, all its other files in the build for e8741fa's text. AOT: a field read in a loop as what the build saw stored; mocks in test runs: one more road to a crash On a48610b. Three files, +24 -2. One of them lies outside aot/: see "Outside aot/". Fixed: - Old, silent, as users build. A field into which the program is only seen to store objects that it fills under computed keys: `class Shelf { constructor(names) { const bins = {}; for (let i = 0; i < names.length; i++) bins[names[i]] = i; this.bins = bins; } }`. Code made from a text stores a list there, `(0, eval)("(holder, value) => { holder.bins = value; }")(shelf, [1, 2, 3])`, and a loop of nine turns asks `Array.isArray(shelf.bins)`: false in every turn, where `bun <file>` says true. So too with `new Function`, with `Reflect.set` out of a text, and with a function stored and `typeof` asked (aot/plan/AOTSlotPlan.cpp: +5). 5 stored programs for Bun's tree. Each built alone: all five print what `bun <file>` prints on the build below; none does on a build without these five lines, nor on a build of 42067fc's text. - Old: a test run that mocks a module and ends the mock could crash: one more road. - No behavior: the three lines of a48610b that forget a run that threw are spelled with a `return` of their own (aot/runtime/AOTModuleEvaluation.cpp: +3 -2). Corrected: - a864536's message says that an `int32` out of `slice()`, `filter()` or `sort()` was refused with TS100031. Of `sort()` that is not so: the stored program for it builds at 42067fc. Outside aot/: - runtime/AbstractModuleRecord.cpp (+16), two places, both inside `#if ENABLE(AOT)`, for the crash in test runs, above. Each asks first whether the VM holds compiled code and then whether the record is one of compiled code; what follows is reached for a record that nobody compiled, in a run that holds compiled code. A program that is not compiled runs neither. On a build of this commit with Bun's side of this branch, Linux x64, release, no LTO: it compiles and links; the engine's 2,567 IR tests pass; the five programs above. Seven test runs print on it what they print on a build of a864536's text. The five test runs of the factory that requires its own file print what `bun test` prints.
…'s folder, mocks in test runs (oven-sh#1000) Fixes alone, on oven-sh#999. Seven files, +97 -3, all under aot/. Nothing outside aot/. Fixed: - Old, an abort as users build. `t.mjs` holds `export * from "node:path"; export { sep } from "./lacks.cjs"`, lacks.cjs does not export `sep`, and another module imports `sep` from t.mjs: `bun <file>` prints "/"; the executable ended with exit 134. So too where the star leads to a CommonJS file: `export * from "./stars.cjs"; export { twice } from "./lacks.cjs"` with `exports.twice = function (x) { return x + x; }`, and `twice("a")` in the importer: `aa`; exit 134 (aot/facts/AOTLinkAccesses.cpp: +2). 1 IR test; 53 files of stored programs in Bun's tree: eleven programs: all print what `bun <file>` prints on the build below, two on a build of 819116c's text. Not whole: kin of these programs are not fixed here. - Old, the engine's part: `await import(flag ? "./other.sqlite" : "./data.sqlite", { with: { type: "sqlite" } })`, and the same under a name with a hole. An executable of `--compile` opens the database of the folder in which it runs; with --aot it opened the database of the folder in which it was built, under its whole path: silent where the sources still stand, "unable to open database file" where they do not. A load with attributes under a name that begins "./" or "../" first asks the host with the text as it is written; a load without attributes takes no new step (aot/runtime/AOTModules.cpp, AOTModules.h: +19 -2). The other parts and the test file are Bun's. - Old, silent, test runs: who was linked under a mock that a second `mock.module()` patches read undefined behind its end, where `bun test` goes on reading what the second one gave (aot/runtime/AOTTestRunMockViews.cpp: +5). 16 files of stored test runs in Bun's tree. - New with a864536, a build, seen in the IR alone: where the build lists the library modules whose names a program writes, a field without a key ended the build at an assertion. The environment of a closure has such fields (aot/facts/AOTFactsOfProgram.cpp: +1 -1). 1 IR test. No JavaScript is known that shows it. Known, new with this change, not fixed here; both by run, on a build of it with 0e58059e of oven-sh/bun: - Silent, an odd program: one .sqlite file imported twice under a name made at run time, as `{ type: "text" }` and then as `{ type: "sqlite" }`: `bun <file>` and an executable of `--compile` hand out the first form again (`string string`; the other way round `object object`); compiled, each type gets a form of its own: `string object`, `object string`. - Silent, small: a plugin that is added at run time, `build.onResolve({ filter: /\.sqlite$/ }, args => ..)`, is handed the working folder as `args.importer` for a database that is imported under a name made at run time; `bun <file>` and `--compile` hand it the file. Before this change that import was refused: "Cannot find module". On a build of this commit but for two lines of comment in one IR test, with Bun's side of these fixes, Linux x64, release, no LTO: it compiles and links; the engine's 2,569 IR tests pass (2,567 at 819116c), and with the table of the stand-in host 4,322 pass, none fails, 19 are left out, as before. The two new IR tests fail on the test program of 819116c's text. On that build and on a build of 819116c with dddebacb of oven-sh/bun: the 38 stored programs that come with these fixes: 38 | 6 pass; three families of stored test runs by Bun's own driver: 12 of 12 | 1 of 12; Bun's four new test files: 4 | 1.
Merge upstream 5a61071 with full fork and upstream history. Port distinct require namespaces and loader-parent reachability to the new runtime and thin-summary owners; retain fork GC and memory accounting fixes. Pair hosted qualification with Bun 1d6aff3e without publishing or relabeling historical artifacts.
Use all four migrated IR fixtures from the exact Bun candidate and verify their bytes instead of downloading obsolete value-flow commands. Require four passing cases and explicitly run the require-namespace isolation and captured-export native regressions.
|
Dependency limit exceeded — report not shown. This pull request scan exceeded the 10,000-dependency limit applied to this scan, so the results are incomplete and may be inaccurate. To avoid reporting false positives, Socket has not posted a report. Upgrade your plan to raise the dependency limit and get complete reports, or view the partial scan in the dashboard. Socket is always free for open source. If this is a non-commercial open source project, contact us to request a free Team account. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem and change
Sync the OpenClaw engine fork through upstream AOT rewrite
5a61071f79134bf41844e7ea31a8449147d85dd4, retaining the fork fixes and complete commit history. Bun's rewritten compiler at upstream2596ac5drequires this schema; historical S archives cannot qualify it.Merge commit required. Do not squash or rebase. Signed merge
2f8b517de6b6b1fc3261bceb39588bf0e13c140eretains parents8c0ebbe34c570ae20ee14f433a62e83bb6ccf0c6and5a61071f79134bf41844e7ea31a8449147d85dd4. Current signed head:651eeaca6e27.Port distinct require/import namespaces to the new runtime, retaining marker isolation, shared live exports, GC roots and mock/cache invalidation. Move dynamic CommonJS parent reachability to the scanner, thin-summary and loader-replacement owners. Hook registration alone must not escape the parent; legacy resolver/require replacements must retain it. Preserve upstream's removal of old ValueFlow/CallModules passes and blanket private-evaluator roots.
Signed
1ed3a4477af3also ports the rewritten typed-array allocation fast path to the fork's existing heap contract: typed-array subspace, complete packed weak-link/mode initialization, list registration and exact logical payload accounting after both allocations succeed. Existing Instance VM plus Heap offsets preserve the Instance ABI. The native fixture checks prior head/counter state, packed links, unrounded payload bytes and unchanged slow exits. Signeda5f127f226d5requires that exact native case.Candidate and validation
Qualification now pins signed Bun
0dd6a8f4fc, tree90e7bca7e472288efe7f5d58b2913d55863bca96. This records an unqualified candidate. Baseline/lane identities, the 85-file selection, size limits, archive/provenance gates and adapter patch inputs/digests remain unchanged. All four migrated IR regressions, three non-trim AOT mock suites, five native namespace cases, 17 module-program cases and the exact typed-array stub case remain required; missing/skipped sources fail qualification.Bun run 42579667012 on candidate
0dd6a8f4fcpasses the full Bun/engine build, runtime engine identity, packaging and Rust trimming-policy tests. Its IR gate still passes three of four: closed-image/bounded-host setup alone cannot isolate a direct function export, which upstream deliberately exposes to the host. The independently reviewed next Bun repairaa024e5a1dwraps the hidden closure in a plain exports object and updates both callbacks to readparent.exports.hidden; existing assertions remain intact. Its native trial against engine651eeaca6e27passes the full build, exact engine-identity startup, packaging and Rust trimming-policy tests and all four engine IR regressions. The broader runtime suite reaches its existing 20-minute deadline with failures; qualification fails. Source-proven Bun startup, SQLite, data-URL and fs-test repairs plus HTTP cleanup diagnosis are in progress. The verified Bun report artifact13659027540is 4,381,277 bytes, SHA-256cf1487a1b4009f6e9c805bec4e151fc440d345d279d573e630b5dbd1a22f7d0f. This engine branch still records candidate0dd6a8f4fc; no qualification is claimed for either candidate.Signed Bun repairs through
8a8ab89ef1fix startupNODE_OPTIONSdiagnostics, SQLite publisher-export consumption, data-URL MIME selection, fs-test registration, response-socket cleanup after mutable finish listeners, and absent objects in prototype traversal. Native run 42588256994 passes build, exact engine identity, packaging, Rust trimming-policy tests and all four engine IR regressions against651eeaca6e27. The full selection now completes: 691 passing / 21 failing records, including two passing dependency-setup records. NODE_OPTIONS, SQLite column types, loader, fs, IPC, main HTTP and fast-path regression files pass. Qualification remains red on the recorded ICU/size/trimming and reflection/fixture failures. Verified report artifact13661302173: 4,423,747 bytes, SHA-256b2ebfb65d84edd0ffe1f840a0dd2aa61f762e7d55d0b5e7b974e26448e42c0d5.Focused replay 42590096312 uses the immutable old aa024 executable, separate from probe
ffa6e04cbf. Intentional fatal-init/UV cases now pass in 142/88 ms with original assertions/deadlines after process-local mapped-core suppression; CommonJS plain/AOT parity also passes after retiring the removed layered-test debug route. The overall replay stays red on constructor stack-column parity and an imported namespace-mutation fixture. These findings drive reviewed signedd78fa8ba39(use the fork'snewtoken position) and0c10d4cc6e(require TypeError and unchanged imported namespace), without relaxing engine contracts or baselines.Current signed integration head
56739277f7includes standard-stream exception propagation, response-owned connection lookup, aliased-response cleanup, native-owner Proxy callback state, and Node-verified cork-count expectations. Corrected signedb47a817a21applies runtime extension precedence to in-place AOT imports and restores stock compiled/bundled policy; it supersedes the earlier executable-only no-package fallback. Six Rust load rows now exercise both modes. Source checks and formatting pass on this head. Full native run 42603897638 completed with explicit frozen base2596ac5d391d47804c93a42bdbe615804aa62387and engine651eeaca6e2792378928033b1ae7ac66d6f91eab. Build, trimming-policy Rust checks, written-position/module-mode checks and AOT CLI gates pass. The completerun-aot.test.tsfile passes 72/72. The ledger records 695 pass / 17 fail plus four separately passing IR inputs across 714 selected paths; direct networking passes 154 tests with eight skips. Remaining failures cover source/system-ICU version, size/locale/loader retention, type optimization and HTTP/prototype surface assertions. Full green and shipping qualification remain incomplete. Verified report13668566451: 4,423,412 bytes, SHA-256aedb0b12aa0c0000aa3f60c75e5fb8304191f4078f5a8b2510bb702e913974f7.Native gate 42599698918 builds and packages exact
a6644f3357030f40982c976c3a522a8b0a3af7e5; 256 trimming unit tests, 12 invariants and all six written-position tests pass. The load-format gate then exposes a bad test assumption: its helper defaults to in-place mode, so the earlier CommonJS expectation change cannot follow from the executable-only fallback. CLI and the broader selection did not run. The corrected two-mode test and in-place producer repair are reviewed and signed inb47a817a21. Earlier gate 42597791945 separately stopped at an unused test import, fixed ina6644f3357.Admitted runtime replay 42603067250 uses immutable native
a6644f3357with the corrected framing helper: all 37 focused HTTP regressions and both standard-stream cases pass. Its three CJS failures use the older executable and are superseded by56739277f7's complete 72-test CLI pass. Exact response/body/callback assertions remain intact. Verified replay artifact13666283475: 7,087 bytes, SHA-256c0dba5ca1ac77068cac6ecc1bccfa74e0044de7deb80c6785cf4e0a5648cf2d4. Upstream CJS #48354 and standard-stream #48352 are ready for review; stock-native Buildkite validation remains pending (#139279, #139274).Proxy socket red replay 42598563871 proves both new TCP regressions fail on immutable native
8a8ab89ef1b6f75b6daea63c84cc1156b8e4ec94: raw writes lose bytes/close the connection, and uncorking queued response writes throws a private-field error. Reviewed signedf7c57fcac8e8f2ec528e22c7e350b157d23a8ab2keeps private callback state on the existing native handle's canonical socket owner, while preserving public receiver observations and accounting. Verified red-report artifact13665045811: 4,882 bytes, SHA-2568cbfcf6293ab6d47e3520967536be88455812f50c8664686a4a2e17908f6e12b. The later admitted replay above passes both cases, including the corrected raw-prefix framing and all 37 HTTP siblings.Matched stock comparison 42601831115 succeeds with exact fixtures, one frozen stock type/dependency tree, LLVM 23.1.1, Rust nightly-2026-09-15 and system static ICU 74.2. Stock Bun
2596ac5d391d47804c93a42bdbe615804aa62387plus stock WebKit5a61071f79134bf41844e7ea31a8449147d85dd4removes 110/98 features; candidatea6644f3357plus engine651eeaca6e27removes 109/93. All four compiled programs produce exact expected outputs and clean exits. The simple-server delta isOperationsOnPrivateNames; the unknown-values delta additionally retainsOperationsOnArgumentsObjects,OperationsOnDates,OperationsOnEncodedTextandOperationsOnStringBuilders. The same 109/93 shortfall persists at56739277f7. Separate operation-origin diagnosis 42607192709 is running with frozen Bun56739277f7834a63cdd2ba8fd496672c5ab8f645, controller2009bdc5775deb66913637fd1892ac1d4e8ee798, and diagnostic-only engineadc737694a84cf1d0ac1c72c4c73adf9a023b107. Both probe patches passed independent review and are signed; the admitted engine candidate remains651eeaca6e27. Producer diagnosis is in progress; all original floors remain unchanged. Verified comparison artifact13667171797: 181,108 bytes, SHA-25605fd59aed00f135df43ba00c721f877d95fefbf7b17fcad70acf0ab32bca6770. This is source-pair proof, not shipping ICU 79.1 qualification; the separately recorded coarse-ICU loader/size/locale gaps remain.Completed native qualification 42591603139 builds earlier signed
01857e53babc3a5e21b90dd716ff20e33b1f11bewith engine651eeaca6e2792378928033b1ae7ac66d6f91eab: build, packaging, trimming-policy tests, focused Rust positions, and production CLI constructor-stack/namespace checks pass. The full selection completes with 691 passing / 21 failing records. Its remaining CLI failures isolate bare imported.cjsstrictness; its three source-map expectations still point atErrorinstead of the repairednewposition. The source-map expectations are repaired in6bdde38d9a46623a1d7aae59b8195d960b39ceb7; the initial CJS fallback was insufficient and is superseded by the reviewed in-place repairb47a817a21, with native proof pending. Verified report artifact13663820238: 4,424,942 bytes, SHA-2567a30e9f015ac31d070a69c321255853ceccec1d7768d67cd21d2662bf4a61163. Partial native run 42595417947 built earliera2f039d9dd6d75a1ee05caa136acb8b698a58b6d: build, packaging, trimming-policy tests, focused Rust/CLI checks, and the pinned Node control pass; its default selection has 28 passing / 1 failing record (system ICU floor). The frozen-base dispatch input was omitted, so those 29 records do not qualify the standard-stream/HTTP owner repairs. That selection error is corrected in full run 42597791945. Verified partial-report artifact13664131945: 4,352,034 bytes, SHA-25600ca30fbf84d5584918b83111c26a46c650c370d9eaa98c551436caef094db1d.Focused red replay 42594192257 uses immutable executable artifact
13661110892from native8a8ab89ef1b6f75b6daea63c84cc1156b8e4ec94: both new repeated-request Proxy/inherited-response tests fail withERR_HTTP_SOCKET_ASSIGNED; the existing alias-state test fails too. Node declarations 26.2.0 andhttp.d.tsare present. Verified diagnostic artifact13663135550: 18,953 bytes, SHA-256ce4a25070bac4de207bb56932417586e817c6d66bbf1a8bfd7dcbd6897f70cdb.Original single-file control 42594866158 passes plain/compiled HTTP behavior on the same
8abinary and retains the same five genericansweraccesses as the migrated fixture. Source comparison shows the retired report preferred non-generic copies, while supported explain summaries select the worst copy; the metrics are not equivalent. Guard diagnostic 42595783244 also passes runtime parity, but reports four rejoining access guards (one namedIncomingMessage), not both expected typed-entry guards. This does not establish the former fast-copy slot-optimization assertion; no compiler metadata patch or blanket expectation relaxation has been made. Its verified artifact13663381963: 915 bytes, SHA-2568673687ece380331b8c9a81d4334d75d3209e815df46bdb38fa2d871ebcddb2c.The generic test-only core suppression port is ready for upstream review in oven-sh/bun#48339, with fork-native evidence disclosed; stock-upstream Buildkite remains blocked. Shipping-archive transport, remaining runtime/trim/reflection qualification, publication approval, and consumer repin remain outstanding.
Signed local engine commit
b17472779f04da2779d553ae29c1f598bd36edc0prepares the candidate pin to Bunaa024e5a1d5466098357784d7f6fb159581e1478, treed5ef4d4e1575e9afd50c9b13e222eb2226f1dac5. The three-value update passes exact Git-object/adapter-hash checks and fresh independent review scoped-clean P0–P2. It is retained unpushed while canonical release-asset transport remains blocked. Remote PR head and current engine proof remain651eeaca6e27, candidate0dd6a8f4fc; neither candidate is qualified.Fresh reviews are scoped-clean P0–P2 for the full fork delta, namespace/visitor ports, typed-array allocation, harness, Bun repairs/fixtures and this candidate update. Static checks verify the exact candidate tree/manifest and all three adapter input hashes against its Git objects.
Engine proof and remaining work
The following evidence belongs to signed
a5f127f226d58fb28332a01c889964c6d408c08b:89415eea95b15df656b12a5c27cc89f665af4835bcf7b448eea97c2954ce4d10.8b48ac2586; the active Bun branch repairs that failure. Independently verified Linux proof 13654679995: 347,970 bytes, SHA-25600f1b5d2602a949d8e6712b7b24ac41c02504f553fe20d15dd46c4679b50ba03.Current-head engine checks have two pre-compilation transport failures: Linux exhausts five HTTP 504 retries downloading canonical zstd, and Windows correctly rejects a 92-byte HTML response to its checksum-pinned ICU download. The macOS artifact builds pass on
651eeaca6e27; ARM64 qualification passes on the same source. Independently verified artifact 13658881366 is 59,550 bytes, SHA-25665a4d4266b29e092d6b50000e31a9e7df0410eacfde10c5c368f1bfa4585ae24: exact typed-array AOT case, five namespace/17 module-program cases, all 1,901 stress/1,639 module results, three API regressions, five accounting/sampling modes and seven stack modes pass. Raw results and AOT output were checked against the gate. The single fresh-runner retry (attempt 2) also fails before compilation: Linux ICU and Windows ASAN downloads return the same HTML-body digestbef140a1a96994029153dca8c00b1750b9a5a764fb9db2dc68d7bb40e8a29e8a. Checksums reject those responses. Automatic retries stop; canonical archive qualification requires working runner-to-release-asset transport. Canonical recipes, source pins and expected checksums are unchanged. Previous proof retains its recorded source identity. Canonical-ICU Bun runtime/size/shipping and current consumer qualification remain incomplete. Source/system-ICU receipts retainshippingArchiveQualified: false; earlier FA/S proof does not qualify this candidate.Draft. No fork/native code was executed on the maintainer workstation. This branch is admitted only with
publish=false. No release, tag, archive publication, consumer repin or deployment is included. Tracking campaign: openclaw/bun#221.