Repository navigation
AOT: bun test --aot on large suites, module loading fixes, fuzzer fixes - #47316
Merged
Merged
Conversation
…ut of lists and Workers' programs; WebKit preview 7f21d5ed [skip size check] - WebKit: autobuild-preview-pr-927-7f21d5ed. - Modules: bind_modules() keeps one set of stored __esModule marks for both kinds of module and reads a changed mark from the object. limit_exports() limits the export tables of faces as it does those of files. A name taken from a JSON import asks for the files between, so files that a package lists under sideEffects run. What `delete` removes from a namespace is bound on no object. - CommonJS: under "type": "module" a default import holds what the link captured, and takes its short way only where nothing stores to module.exports behind the end of the body. A file whose namespace may be looked at and whose text does not say all its names has its object. - Names of modules out of lists and tables: a walk of a list (forEach, map, filter, find ..) leaves the list as written only where its function is written in the call, has no rest parameter, does not find itself through `arguments`, a direct eval or its own name, and holds its third parameter as every other holder of the list does. `instanceof` hands the list on. A table that writes its own toString or valueOf is not followed. - Programs under a second tsconfig take what the application's load found: import.meta.main, which file stands for which, erased sites, folded constants. An imported require that is given a path from its own folder resolves from that folder. Which file of the application a file is goes by its path and its loader. - Classes: an unused class goes whole only where its heritage is proven; otherwise it leaves its heritage and one look at it, a computed key leaves ToPropertyKey, and a static member named "prototype" keeps the class. The transpiler records which classes assign their fields under useDefineForClassFields: false and the second parse reads that, with the build's own table of defines. Methods stored on the prototype of a constructor function are members of its layout. - Syntax: the parser says where no `;` can follow a bare `declare`, so `declare` is a name on the left of for-of, for-in and for await and in front of every kind of line break; a line break behind `global` is any of the language's. - Types: only callable built-ins join a function type; the type of a well-known symbol is symbol. - Workers: whether the files and texts that Workers start from are one program with the rest is decided behind the front end, ahead of the first run of the engine. The constructor tells the engine when other threads are ending. - macOS: operator new of every piece of a trimmed executable goes to mimalloc, and the ranges of its pieces are known to stack traces. - bun check: a ring of functions without annotations is resolved in linear time. - The build: a third list of the engine's sources, compiled for minimum size. - Tests: stored programs, generated tables and fuzz families for each of the above and for the engine's changes; a NaN of every pattern of bits through N-API, bun:ffi and the serializer into fields of every kind; definitions by descriptor; host objects that come back by some road; declarations that nothing uses; contextual keywords as names. Files that read how often compiled code calls the run time run on Windows. The large build of workers-from-texts-that-the-build-knows takes its turn among the others again. Program folders have short names; two tests join the fixed step; tests that pass on both Windows lanes leave the awaited list and two units join it; the engine's native tests run on Windows. - Docs: rows of docs/compiler/compatibility.mdx follow the above.
…off an ES module, and tests for the engine's changes; WebKit preview 617ed49e [skip size check]
- WebKit: autobuild-preview-pr-927-617ed49e.
- Modules: what kind of module a file is goes by whether a top-level await is written in it, not by whether one can be reached. A CommonJS file whose getter reads, off an ES module, a name that the ES module hands on from another CommonJS file keeps its object unless the copy and the name cannot be told apart.
- The loader's fetch hook asks VM::synchronousModuleQueue().
- Strings: the natives of node:path take the width of a substring's characters from its base's storage as it is now; a string that is used as a property key takes the storage of an equal key, of either width.
- Output: a build prints neither the hint about untyped code nor the counts of property accesses; the counts end the text report of --explain.
- macOS: --aot-symbols takes a file whose table of indirect symbols has an odd number of entries.
- bun check: the calls that were resolved meanwhile are one list.
- Windows: in place, the path that an import with { type: "file" } gives has the separators of the host.
- Tests: stored programs for each of the above and for the engine's changes: module variables of files that are still running, patterns without elements, Reflect.construct, super behind a delete, closures among many entry points, counted imports under a synchronous load, a callback that stores the answer of a call that never returns into a captured variable. Own members of compiled objects that native code looks for: RegExp test and search, the first line of a stack, Bun.inspect, concat, printed class names, labels of heap snapshots. Sums over typed arrays. A family for traps that are stored on the handler after the proxy is made. Four units run the engine's flow tests under 32 other schedules of the rules. The reference run of the packages takes the reference's environment. Five more units run on every commit on Windows. The three programs that terminate a Worker inside a loop count the walks that came to their end, in place of a clock. The tests of --trim are skipped where the Bun under test cannot trim, and the one on the allocator also runs with glibc. highway-strings.test.ts lists what is wrong and calls expect() once a test: the same cases in half a second. One-loop rows for numbers that leave their slot. A way out of native code that the system refuses is left out of both runs of the N-API test. A test of lint.yml's kind runs check-unified-sources.py. Sabotages for calls that throw without a site and for bounds that are read twice. Pins of generated code that fell are restated. The pins and rows of objects of constructor functions that are not built are as they were before that step of SinkAllocations.
- Docs: a row of docs/compiler/compatibility.mdx for useDefineForClassFields under a nested tsconfig.json, and its table of platforms says what CI runs on each; a paragraph of docs/compiler/internals.mdx on calls without a site.
…30-64483dcf [skip size check]
…, the program named at start on Windows; WebKit preview-pr-930-3fdfc9b1 [skip size check]
…named without its extension; typed rows of a query; WebKit preview-pr-930-bf1133d2 [skip size check]
…each executable behind its runs; each CI step names the build it tests; the Mach-O writer hands out owned parts of a mapping; bun check keeps the members of a named union; WebKit preview-pr-930-0cb47ca7 [skip size check]
…un without debug information that the build step uploads beside bun-profile; test/aot/fuzz and its test as in 5319f52 [skip size check]
…unction made by bind() of a proxy and what is read through a proxy are followed; tests for modules of both kinds in a ring; the runner gives the compiled programs a job for each processor, deals the dearest first and runs the quick test files in a bucket of their own; the watcher of memory has a thread of its own; WebKit preview-pr-930-c2d259b5 [skip size check]
…ld that asks nothing of the library reads no file of it; process.send and Bun.spawn bring the channel in only where they are called; children and parent of a module object that goes out of sight; a moved range keeps what follows it in its last page; the runner deals the compiled programs by all their files, and the dear ones share one library; WebKit preview-pr-930-02becc12 [skip size check]
… all modules run in the cargo lane; every lane builds the light programs alone; under ASAN the compiled programs run on a Bun without debug information; a following round is asked for only where it keeps more; a frame names a function made under a symbol key by its own name; the generators of the fuzzer are sources of run.ts; WebKit preview-pr-930-430afb3a [skip size check]
… traps; the printer is loaded for a printed value only where a class that needs it may have an instance; start-up functions are handed over by need; internal/fs/streams no longer loads node:fs/promises; a list that grows by push is checked in linear time; the tests build a trimmed and a whole executable in one build [skip size check]
…at it imports; the shards are dealt whole builds; long test files run beside one another under ASAN and on Windows; the texts of the library read the members of Object, Buffer, Reflect and Bun in one order; a list that grows by push is checked in linear time whatever lies between the pushes [skip size check]
…o ask for; the exports of a module of the library are handed to nobody new as the receiver of a member call; pushes with branches between them are checked in linear time; tables that only a build reads are made at first use; one compile writes the whole and the trimmed executable in two test files [skip size check]
…that cost the plain files their form [skip size check]
…described by their paths; a star or dynamic import of a library module brings what its getters load; a call of what is no function throws wherever it stands; a name out of a list that was changed in place counts as unknown; the long test files run two at a time on the Linux lanes; every test of the compiler runs on Alpine, the other lanes run those that ask the system, and the lane with a sanitizer leaves out those that compile one text again; an executable built on a PE template chooses its cuts anew; dependencies that no code of a crate uses are gone [skip size check]
…hem; three programs run half their turns; a test file that says it runs alone stays out of the bucket [skip size check]
…piler run on a copy of Bun without its debug information, and what fails is tried again on the whole file; bun check --timing prints its queries, and the test of long rings counts them [skip size check]
"classes with a method of one name that each call it on an instance of any of them" has had its growth in instructions and in two counts in budgets.linux-x64.json. Its growth in memory stands beside them now: macOS judges it, where Linux leaves it unjudged below 96 MiB. [skip size check]
src/exe_format/macho.rs: the loop that hashes and writes pages calls `SHA256::hash(page, hash)`, as on main. [skip size check]
…ative plugins run on every lane - readMembersThatMakingStandardErrorReads(): the two lambdas are static functions with a ThrowScope of their own, and each call site checks or releases. - scripts/runner.node.ts: the three test files that compile a plugin with the system's compiler and load it are among the tests that ask the system, so they run on every lane. - test/aot/files-written-while-bundling.test.ts says why its plugins wait. [skip size check]
On macOS, an executable made by `bun build --compile` started a detached thread at boot that loaded the system trust store when a flag in the module graph said the program might ask for it. Under `--aot` the flag was set by reachability; otherwise it was set by searching the output for the text `getCACertificates`. This removes the thread, the flag, both places that set it, and the test for the flag. The system trust store is loaded on first use again, on the thread that asks for it. [skip size check]
They checked that Security.framework was loaded before the program asked for the system certificates, which no longer happens. Also updates a comment in node:tls that described the prefetch. [skip size check]
…IR tests [skip size check] Pins WebKit to the preview of oven-sh/WebKit#934. - Merges main. - Wrong answers and compiler stops found by fuzzing, by the run-time checks and by CI are fixed, each with its reproducer under test/aot. - Under --minify-identifiers a class with standard class decorators keeps its written name. - The compile object of Bun.build() takes aotValidate, aotCheck and aotTesting, hidden like aotDisable and aotHeat, with the meaning of the command line's flags. - --explain names where an escape begins. - The React compiler's lowering reads every segment of a joined string. - A compiled hello world starts with fewer instructions, page faults and system calls. - mimalloc moves to 6da53c35 and takes no patches. - The directives of an .ir test are checked strictly: a misspelled or misplaced one is an error. - The test runner leaves the engine's shell, its test program and its tests packed in a shard that starts none of them, and unpacks with tar on Windows. - The checks that read the sources are removed, with their two steps in lint.yml and the aot_check build rule. - scripts/vs-shell.ps1 keeps variables of several lines away from the Visual Studio loader.
Collaborator
|
Updated 11:40 AM PT - Oct 31st, 2026
⏳ @Jarred-Sumner, your commit 0a39dbd is still building in
Add |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
This was referenced Nov 1, 2026
Open
robobun
added a commit
that referenced
this pull request
Nov 1, 2026
…d path Since #47316, __filename in an ahead-of-time executable says where the file lay when the program was built. module._compile() asked whether that path names a compiled module. The answer was no, so ES module text under __filename took the name of the file. It now asks with the path under which the executable knows the file (Bun::pathInExecutable). The program test/aot/parallel/commonjs/module-compile-es-module-text/main.cjs fails on main with this branch merged and without this change.
This was referenced Nov 1, 2026
Open
This was referenced Nov 2, 2026
dylan-conway
pushed a commit
that referenced
this pull request
Nov 2, 2026
…46969) ### Problem - `mordant-baseline.toml` accepts more findings than main has for fourteen (lint, file) lines. The `mordant` job accepts findings up to the baseline count. A new finding in one of those files then passes with no report. - Commits on main removed 21 findings in nine crates. The counts did not follow. ### Fix - Delete ten lines and lower four counts to what `bun run rust:mordant:baseline` records on main at 271b65e. The baseline goes from 494 findings to 473. The Notes list each line. - Six more stale lines are left out. #46244 removes five of them. #46528 rewrites the sixth, `unused_pub:src/sys/lib.rs` (55, main has 53). - Verified: `bun run rust:mordant` over the three CI targets exits 0 with this baseline. No source file changes. ### Background - mordant is the lint pack that the `mordant` job in `.github/workflows/rust-lints.yml` runs through `scripts/rust-mordant.ts`. `mordant-baseline.toml` holds, for each crate, the count of findings that the job accepts for each lint and file. - The job fails only when a file has more findings than its count. It reports nothing when a file has fewer, so a count stays too high until someone lowers it. - A count is the largest one that any of the three targets (Linux, Windows, macOS) records. The `unused_pub` counts come from one run over all three targets together. <details><summary>Notes</summary> | Line | Before | After | | --- | --- | --- | | `bound_checked_only_in_debug:src/css/properties/custom.rs` | 2 | deleted | | `unused_pub:src/glob/lib.rs` (the only line of `[bun_glob]`) | 1 | deleted | | `generic_body_not_generic:src/js_parser/visit/visit_expr.rs` | 1 | deleted | | `unused_pub:src/jsc/host_fn.rs` | 2 | deleted | | `unused_pub:src/jsc/lib.rs` | 1 | deleted | | `generic_body_not_generic:src/resolver/fs.rs` | 1 | deleted | | `field_valid_only_when:src/runtime/shell/subproc.rs` | 1 | deleted | | `field_valid_only_when:src/runtime/timer/mod.rs` | 2 | deleted | | `generic_body_not_generic:src/runtime/webcore/Blob.rs` | 2 | deleted | | `unused_pub:src/windows_sys/externs.rs` (the only line of `[bun_windows_sys]`) | 1 | deleted | | `safe_fn_returns_shared_mut:src/bundler/transpiler.rs` | 2 | 1 | | `safe_fn_returns_shared_mut:src/http/HTTPThread.rs` | 3 | 2 | | `generic_body_not_generic:src/jsc/ConsoleObject.rs` | 6 | 5 | | `generic_body_not_generic:src/runtime/socket/socket_body.rs` | 8 | 4 | Main has 197 lines, 494 findings and 38 sections. This branch has 187 lines, 473 findings and 36 sections. How the counts were measured, with mordant at the pinned revision 564c8da5 and `nightly-2026-09-01`: - `bun run rust:mordant:baseline` on main at 271b65e writes a baseline of 184 lines and 464 findings. It differs from the committed one in these fourteen lines, in the five lines of #46244, and in `unused_pub:src/sys/lib.rs` (55 to 53). - `bun run rust:mordant` with the baseline of this branch exits 0 and writes no `target/mordant/over-baseline.txt`. It is one `cargo mordant --workspace --keep-going` run over `x86_64-unknown-linux-gnu`, `x86_64-pc-windows-msvc` and `aarch64-apple-darwin`. Two lines with a known cause: - `[bun_glob]`: #47559 added the line on 2026-10-31, because `bun_glob::walk_bun_glob` (#47513) had no caller. After that, #47316 put `#[cfg_attr(mordant, allow(unused_pub))]` on the function, and #44833 added a caller in `src/js_parser/lower/import_meta_glob.rs`. - `custom.rs`: #47025 removed the two `debug_assert!(*d <= 0x7F)` checks that the lint reported. Changes from the first version of this PR (nine lines, measured at 449877f): - `generic_body_not_generic:src/runtime/jsc_hooks.rs` stays at 1. The baseline that main regenerates has that line again. - #47238 deleted the `IOReader.rs` and `IOWriter.rs` lines on main, so this PR does not change them. - Eight lines are new: `custom.rs`, `resolver/fs.rs`, `timer/mod.rs`, `Blob.rs`, `[bun_glob]`, `[bun_windows_sys]`, `transpiler.rs` and `HTTPThread.rs`. </details> <!-- robobun:evidence:begin --> --- **no test proof** · iteration 0 · no src or test change; test-proof not applicable <!-- robobun:evidence:end -->
dylan-conway
pushed a commit
that referenced
this pull request
Nov 3, 2026
Both conflicts are in test files that the last two commits here changed so that two of main's tests pass. #47316 has since rewritten both files, so main's versions are taken, also for build-scaling-cases.ts. With that, this branch differs from main only under src/.
dylan-conway
added a commit
that referenced
this pull request
Nov 3, 2026
… package manager (#47414) ### What does this PR do? 50 separate changes, one commit each: 16 in the core crates (`bun_core`, `bun_collections`, the TOML and XML parsers, the crash handler), 16 in the resolver and the code next to it (the HTTP client's setup, source maps, the standalone module graph), and 18 in the package manager. Two more commits answer the `mordant` lint job: 51 takes back a part of change 5, and 52 moves a `match` that change 33 left in two places into a method. Nothing is left of commits 53 and 54: they changed three test files so that two of `main`'s tests pass, #47316 has since rewritten those files on `main`, and the merge of `main` takes `main`'s versions. So this PR changes nothing outside `src/`. Commit 56 answers a review comment: `read_directory` and its siblings return a shared reference. The changes follow #47291, #47317, #47338, #47382 and #47393 and work the same way: signatures name the type and the lifetime that the value already has, hand-written pointer code becomes plain ownership and borrows that the compiler checks, and what was never needed is deleted. Together: **126 fewer `unsafe` blocks and 8 fewer `unsafe fn`s**, +877/−1651 over 73 files. No safe function became `unsafe`, and no wrapper type was added. **[Where the compiled code changes](#where-the-compiled-code-changes) lists every place where the program does different work.** | # | Change | `unsafe` | Diff | |---|---|---|---| | 1 | toml: the parser keeps tables and arrays as `StoreRef` | −6 blocks | +43/−64, 1 file | | 2 | bun_core: remove `boxed_zeroed_unchecked`, `ZStr::boxed` and `OptionsEnvArg for Box<ZStr>` | −3 blocks, −1 `unsafe fn` | +3/−50, 4 files | | 3 | xml: `Unit::from_u16` and `Unit::from_bytes` instead of casts | −3 blocks | +20/−22, 1 file | | 4 | crash_handler: `UNSUPPORTED_UV_FUNCTION` holds the name's bytes | −2 blocks | +6/−13, 1 file | | 5 | xml_index: `StructuralIndex::seek` and `at` read the window with `get` | −2 blocks | +14/−10, 1 file | | 6 | collections: `HiveSlot` borrows its hive | −2 blocks, −1 `unsafe fn` | +28/−60, 1 file | | 7 | bun_core: `vec::extend_from_fn` is `Vec::extend` | −1 block | +5/−28, 2 files | | 8 | fmt: the shared temp buffer is an `Option<Box>` | −3 blocks | +65/−119, 1 file | | 9 | SmolStr: `Inlined::all_chars` views the `u128` with `bytes_of_mut` | −1 block | +2/−11, 1 file | | 10 | bun_core: `impl io::Write for Writer` calls the inherent methods | −2 blocks | +2/−6, 1 file | | 11 | atomic_cell: `unreachable!()` for the widths that `Atom` excludes | −1 block | +2/−3, 1 file | | 12 | highway: `memmem` subtracts addresses | −1 block | +2/−2, 1 file | | 13 | Check constant strings at compile time instead of `from_utf8_unchecked` | −2 blocks | +17/−40, 4 files | | 14 | Call `gettid`, `getpid`, `alarm`, `_exit` and `raise` as `safe fn` | −7 blocks, +3 `unsafe extern` blocks | +25/−24, 5 files | | 15 | bun_core: `fd_path_raw_w` takes `&mut [u16]` | −5 blocks, −1 `unsafe fn` | +17/−30, 2 files | | 16 | output: `STDOUT_STREAM` and `STDERR_STREAM` are atomics | −3 blocks | +21/−13, 1 file | | 17 | resolver: `BrowserMapPath::map` is `&'static BrowserMap` | −4 blocks | +5/−12, 1 file | | 18 | resolver: keep the slot that `MaybeUninit::write` returns | −2 blocks | +4/−8, 1 file | | 19 | resolver: drop three lifetime casts that change nothing | −3 blocks | +7/−14, 2 files | | 20 | resolver: `RealFS::read_directory` and `EntryLookup` keep their `'static` | −3 blocks | +21/−55, 4 files | | 21 | http: `h3::ClientContext::get_or_create` reads the loop itself | −2 blocks | +6/−14, 2 files | | 22 | boringssl: `ssl_ctx_setup` takes `&OwnedSslCtx` | −2 blocks, −1 `unsafe fn` | +11/−22, 2 files | | 23 | sourcemap: `impl_pod!` for `SyncEntry` and `ObjectBrace` | −3 blocks | +9/−20, 1 file | | 24 | sourcemap: `WindowReader<'a>` borrows the stream | −1 block | +32/−40, 1 file | | 25 | resolver: four `unsafe` blocks that plain borrows replace | −4 blocks | +18/−38, 4 files | | 26 | resolver: `package_json` travels as `&'static PackageJSON` | −3 blocks | +30/−77, 6 files | | 27 | resolver: `load_tsconfig` keeps its boxes | −4 blocks | +16/−37, 1 file | | 28 | resolver: `BIN_FOLDERS` is initialised at compile time | −2 blocks | +17/−39, 2 files | | 29 | standalone_graph: in `inject`, `zname` borrows `zname_owned` | −2 blocks | +3/−12, 1 file | | 30 | sourcemap: `ParsedSourceMap` holds the standalone graph's sources by type | −1 block | +22/−62, 4 files | | 31 | http: `InitOpts::abs_ca_file_name` is a `&'static ZStr` | −1 block | +14/−36, 3 files | | 32 | exe_format: `_mh_execute_header` is a `safe static` | −1 block | +3/−3, 1 file | | 33 | install: `init` reborrows the `&'static mut DirEntry` it matched on | −2 blocks | +2/−9, 1 file | | 34 | install: `save_lockfile` uses `progress.root` instead of a stashed node pointer | −2 blocks | +2/−15, 1 file | | 35 | install: drop four raw-pointer and lifetime detours that borrowck does not need | −4 blocks | +16/−44, 3 files | | 36 | install: `find_workspace_root_above` borrows `walk.path` for the source path | −1 block | +2/−9, 1 file | | 37 | install: `AutoInstaller::path_for_resolution` takes `&mut PathBuffer` | −1 block | +3/−15, 3 files | | 38 | install: `NetworkTask` reads its manager through the `ParentRef` it stores | −1 block | +7/−22, 1 file | | 39 | install: remove three one-line `unsafe` blocks that have a safe spelling | −3 blocks | +10/−19, 3 files | | 40 | install: clear `root_lifecycle_scripts` with a `scopeguard` on `manager` | −1 block | +3/−10, 1 file | | 41 | semver: delete `query::Group.input` | −1 block | +22/−37, 3 files | | 42 | install: `UpdateRequest.e_string` keeps the `StoreRef` it is made from | −5 blocks | +16/−41, 3 files | | 43 | install: `TarballStream::init` returns a `Box` and `populate_result` is a safe fn | −4 blocks, −2 `unsafe fn` | +149/−202, 4 files | | 44 | install: the Windows hardlink task is passed and reclaimed as a `Box` | −5 blocks, −1 `unsafe fn` | +22/−32, 1 file | | 45 | install: the Windows `Hardlinker` keeps the cwd slice it gets | −1 block | +4/−25, 1 file | | 46 | install: `ROOT_PACKAGE_JSON_PATH` becomes `PackageManager::root_package_json_path` | −8 blocks | +40/−36, 8 files | | 47 | install: `Diff::generate` takes `to_update: bool` instead of the update requests | −1 block | +24/−28, 4 files | | 48 | install: `Package::parse_from_real_manager` takes `&mut PackageManager` | −1 block, −1 `unsafe fn` | +13/−35, 2 files | | 49 | install: in `init`, `original_cwd` borrows the boxed copy of the cwd | −1 block | +3/−9, 1 file | | 50 | install: remove two pieces of dead code that needed `unsafe` | −2 blocks | +29/−61, 2 files | | 51 | xml_index: `seek_refill` calls `at` for every index, as it did | 0 | +4/−8, 1 file | | 52 | resolver: `EntriesOption::try_entries_mut` returns the listing or the read error | 0 | +14/−8, 2 files | | 53 | test: `custom-accessor-reached-computed-key.ir` allows one more member in eleven types | 0 | nothing, since the merge | | 54 | test: record the growth in memory of one shape of build-scaling, as #47329 does | 0 | nothing, since the merge | | 55 | the merge of `main` | | | | 56 | resolver: `read_directory` and its siblings return `&'static EntriesOption` | 0 | +21/−21, 5 files | The commits can be reviewed one at a time. Worth knowing before reading on: - **Nothing that users can see changes**, so the PR adds no test. One leak ends, on a path that hardly ever runs: a `tsconfig.json` chain of more than 64 `extends` frees the configs it has read (change 27). - **Five changes end an aliasing or type bug that had no visible effect.** - Change 30: the standalone graph stored a pointer to one struct as an integer, and the reader cast it to another `repr(Rust)` struct with the same fields. - Change 40: a guard wrote through a raw pointer to the `PackageManager` that was stale by the time it ran. - Change 47: with `--frozen-lockfile` and for `bun pm`, `parse_with_json` made a `&Lockfile` while a `&mut` to the same lockfile was live. - Change 49: `original_cwd` pointed into a buffer that was overwritten while it was still in use. Nothing read the overwritten bytes. - Change 20: a `&mut Entry` could overlap a `&'static Entry`. The store it was made for wrote the value that was already there, so the store is gone. - **Change 14 adds three `unsafe extern "C"` blocks.** They hold `safe fn` declarations of `gettid`, `alarm`, `_exit` and `raise`, next to their callers, as the neighbouring code does for other libc functions. - **New functions, all without `unsafe`:** `Unit::from_u16` and `Unit::from_bytes` (change 3), `with_shared_temp_buffer` (8), `WindowReader::new` (24), the `const fn BoundedArray::new` (28) and `EntriesOption::try_entries_mut` (52). - **Seven changes are compiled only for Windows or macOS, in whole or in part:** 15, 44 and 45 (Windows), 32 (macOS), and single lines of 16, 29 and 43. - **Three structs get smaller, and no file format changes:** `semver::query::Group`, and with it every `Dependency`, by 16 bytes (41), `resolver::Result` and `MatchResult` by 8 (26), and `UpdateRequest` by 8 (42). All of them hold pointers and exist only in memory. - `bun.lockb` stores a `Dependency` as the 26 bytes of `dependency::to_external`: name, name hash, behavior, tag and the literal of the version. The range is not stored. `to_dependency` parses it from the literal again on load. - `write_array` and `read_array` take only `NoUninit` and `AnyBitPattern` types, which `Dependency` and `Group` are not. - `padding_checker.rs` and `bun.lockb.rs` assert at compile time the size and alignment of the structs that are written as they are in memory: `Resolution`, `Meta`, `Bin`, `Scripts`, `PatchedDep`, `semver::Version`, `String` and `ExternalString` among others, and `NpmPackage` and `PackageVersion` of the manifest cache. This PR touches neither those files nor those types. ### Where the compiled code changes I compared the release assembly (x86-64 Linux, `--emit=asm`) of 21 crates, before and after, function by function, with names demangled and labels normalised: the 18 crates that this PR touches, and `bun_jsc`, `bun_io` and `bun_shell_parser`, which use what changes in `bun_core`, `bun_collections` and the resolver. | Crate | Functions | Differ | Other constants or names only | Same length | Shorter | Longer | Instructions | Branches | Compares | Calls | Panic call sites | |---|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:| | `bun_collections`, `bun_highway`, `bun_paths`, `bun_exe_format`, `bun_lint`, `bun_io` | 4,265 | 0 | | | | | | | | | | | `bun_core` | 1,148 | 13 | 1 | 2 | 9 | 1 | −237 | −32 | −34 | −20 | −3 | | `bun_parsers` | 412 | 5 | 1 | 0 | 0 | 4 | +30 | +4 | +4 | +2 | +2 | | `bun_semver` | 70 | 12 | 6 | 2 | 4 | 0 | −18 | 0 | 0 | 0 | 0 | | `bun_crash_handler` | 120 | 2 | 0 | 0 | 2 | 0 | −8 | −2 | −1 | −2 | 0 | | `bun_boringssl` | 30 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | | `bun_sourcemap` | 104 | 9 | 0 | 4 | 1 | 4 | +14 | 0 | 0 | 0 | 0 | | `bun_install_types` | 34 | 5 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | | `bun_resolver` | 443 | 29 | 10 | 4 | 13 | 2 | −93 | −2 | −2 | +5 | −1 | | `bun_http` | 380 | 5 | 0 | 1 | 1 | 3 | 0 | 0 | 0 | +1 | +1 | | `bun_standalone_graph` | 208 | 3 | 0 | 0 | 2 | 1 | −12 | 0 | 0 | −1 | 0 | | `bun_install` | 2,819 | 667 | 561 | 45 | 34 | 27 | −165 | −39 | −50 | −16 | −13 | | `bun_bundler` | 1,983 | 25 | 12 | 2 | 5 | 6 | 0 | 0 | −1 | 0 | 0 | | `bun_shell_parser` | 121 | 1 | 0 | 0 | 0 | 1 | +2 | 0 | 0 | 0 | 0 | | `bun_jsc` | 1,707 | 10 | 6 | 1 | 3 | 0 | −6 | 0 | 0 | 0 | 0 | | `bun_runtime` | 10,459 | 98 | 78 | 4 | 4 | 12 | +72 | +10 | +9 | +6 | +2 | The table is from before the merge of `main` and before commit 56. Commit 56 changes no machine code: `.text`, `.rodata`, `.data`, `.data.rel.ro` and `.eh_frame` of a release build for x86-64 Linux are byte for byte the same with and without it. The last five columns are net, and count the functions that exist on one side only: 21 only before and 19 only after, which were removed, renamed or inlined, or are generic code with another type argument. **Most of what differs is the size of four structs.** `semver::query::Group` loses 16 bytes (change 41), so a `Dependency` goes from 240 to 224. `UpdateRequest` goes from 280 to 256 (41 and 42). `resolver::Result` goes from 296 to 288, and `MatchResult` loses 8 too (26). `PackageManager` gains the 16 bytes of its new field (46), which moves the fields behind it. That gives other offsets and strides (the fourth column), and where a stride changes, the compiler also multiplies differently (`imul $296` is now `lea` and `shl`) and shapes loops differently. Of the 106 functions of `bun_install` that differ in more than constants, 90 differ by at most 5 instructions and have no compare, branch or panic site more. So do all 13 of `bun_bundler` and all 4 of `bun_jsc`. **Checks.** One path makes a check that it did not make before: `seek_refill` of the XML index, once per refill of its window, which takes in 8,192 units of input. These are all the functions in which the listing shows more conditional branches, compares or panic call sites than before, and why: | Function | Listing | Why | |---|---|---| | `compute_color_depth` (`bun_core`) | 385 to 408, +6 branches, +6 compares | `index_of` got shorter (change 12), and the compiler now inlines it here. Runs once per process. | | `EncodedSlice::fmt`, `FormatUTF16::fmt` (`bun_core`) | 382 to 365 and 389 to 380, +1 panic site each | They called `SharedTempBufferBorrow::new` and its `drop`, which each looked the thread local up. What those two did is now inline (change 8), with the panic for a destroyed thread local that both contained. 5 branches fewer each. | | `WrappedWriter::write_16_bit` (`bun_runtime`) | 100 to 162, +9 branches, +9 compares, +6 calls, +3 panic sites | The same: the two functions that it called, of 35 and 38 instructions, are inline. It asks for the address of the thread local once instead of twice. | | `StructuralIndex::seek_refill`, for `u8` and `u16` (`bun_parsers`) | 35 to 36, +1 branch, +1 compare | After `fill_to` has returned, `at` compares the index with the length of the window again, where it read the element unchecked (changes 5 and 51). | | `load_node_modules` (`bun_resolver`) | 2,828 to 2,848, +3 branches, +3 compares | The compiler duplicates three tests at exits: twice the one in front of a `dealloc`, once the one of the debug log's indent. One per path, as before. | | `finalize_result`, `load_as_file_or_directory` (`bun_resolver`) | 735 to 712 (+1 branch, −3 compares, +1 call) and 564 to 559 (−1 branch, +1 compare) | `Option<&PackageJSON>` is one word where `Option<*const _>` was a tag and a pointer (change 26). In `finalize_result` the compiler duplicates one call of `BSSStringList::append`. | | `load_tsconfig` (`bun_resolver`) | 481 to 502, +1 compare, +5 calls, −1 panic site | The drops of the boxed configs (change 27): three call sites of their drop code and two of `dealloc`. An `unwrap` goes. | | `h3::ClientContext::get_or_create` (`bun_http`) | 74 to 84, +2 branches, +2 compares, +1 panic site | It reads the loop itself (change 21). The same two tests leave its caller `HTTPClient::start_::<true>` (940 to 928), and they now run only when the engine is created. | | `add_catalog::prepare`, one instance of `reject_unknown_update_requests`, `update_package_json_and_install_with_manager_with_updates` (`bun_install`) | 939 to 937 (+2, +2), 512 to 516 (+2, +2), 3,702 to 3,722 (+2 branches, −2 compares, +5 calls, +1 panic site) | Their source is not touched. They walk, copy and drop `UpdateRequest`s, whose size and field order change. The third has the same 25 allocations, and four more copies of the `dealloc` in its drop code. | | `package_json_editor::edit` (`bun_install`) | 3,005 to 3,003, +2 branches, −2 compares | `Option<StoreRef>` is one word where `Option<*mut _>` was two (change 42). | | Change | What differs | Effect | |---|---|---| | 1 | `toml::Parser::parse_keyval` goes from 275 to 282 and `TOML::parse` from 601 to 602: moves. | Same tree. | | 2 | `ZStr::boxed` and the two methods of `OptionsEnvArg for Box<ZStr>` go, 109 instructions. | Nothing called them. | | 3 | The two `structural_index_chunk` are still inline in their callers, which do not change. A copy of each (10 instructions) is now also emitted on its own, and nothing in the crate calls it. That is +20 of the +30 instructions of `bun_parsers`, and its 2 calls and 2 panic sites. | None. | | 4, 13, 14 | The crash handler goes from 825 to 819: two `strlen` calls and their branches go. `CrashHandler__unsupportedUVFunction` goes from 48 to 46, without the test for null. Because it is known not to return, `napi::uv_posix::unsupported` (6) is inlined in the six `uv_*` stubs that call it, without its `unreachable!`. One closure of the crash handler and one of `__runtime_embed_load` have another number in their names. | Only on the way to a crash. | | 7 | `extend_from_fn::<Dependency>` (69) and `::<u32>` (40) are `Vec::extend_trusted` (57 and 33). | One bounds check fewer per call. | | 8 | Apart from the three functions above: `format_utf16_type::<Formatter>` (150) is `with_shared_temp_buffer` with its closure (134). `SharedTempBufferBorrow::new` (35), its `drop` (38) and the slot's `drop` (9) go. The thread local's destructor goes from 10 to 8, and `bun_runtime` gets a copy of it (6). | Same text. After a nested use the inner buffer is freed, not the outer. | | 9 | **`SmolStr::from_char` (3 instructions) was inline in the shell's brace lexer, which is in other crates, and is a function that they call.** rustc offers a body to other crates by itself only while it makes no call, and this one now calls `unwrap` until LLVM folds that away. `braces::Token::to_text` goes from 37 to 39 and ends three arms in a jump to it. The two `tokenize` make one call more each (581 to 581 and 669 to 676). | One call per `{`, `}` or `,` token in this comparison, which does not link. The build that ships inlines across crates. | | 12 | In `index_of` (44 to 39) the second test of `haystack.len() < needle.len()` folds away. `SplitIterator::next` goes from 93 to 91 and `StringJoiner::push_node` from 82 to 81. `replace` keeps its 122. | Same index. | | 16 | The four `Source::configure_*thread*` functions are 5 shorter each, with 2 compares fewer. The compiler had reduced each static to a flag and rebuilt the descriptor from it with a compare. Now it loads the descriptor. `stdio::init` keeps its 211. | Same descriptors. | | 17 to 20, 25 | `check_package_path` goes from 775 to 725 and `check_relative_path` from 766 to 747 (both with change 26). `add_entry_with_store` goes from 572 to 570, without the store of `dir`. `read_file_contents` keeps its 709. | Same results. | | 22 | `ssl_ctx_setup` (18) and `HTTPContext::<true>::init_with_opts` (49) keep their length. | Same calls. | | 23, 24 | `InternalSourceMap::find` goes from 176 to 177, `append_vlq_to` from 154 to 161 and `Builder::finalize` from 209 to 217: moves to and from stack slots. `seed_window` goes from 77 to 72. No compare or branch more. | The two loops compute the stream once, not once per window. | | 26 | Apart from the sizes: `handle_esm_resolution` goes from 487 to 481, `resolve_and_auto_install` from 1,305 to 1,300, `get_loader_and_virtual_source` from 368 to 365, and the `read_dir_info_package_json` thunk from 26 to 22. | Same package.json. | | 28 | `bin_dirs` goes from 18 to 11 and `dir_info_uncached` from 1,099 to 1,089, without the loads and stores of the flag. | Same list. | | 29, 30 | `LazySourceMap::load` goes from 230 to 219. `Lookup::get_source_code` goes from 180 to 189, with the same tests in another order: it looks at the new field before it asks for a provider. `SourceContentPtr::from_provider` (6) goes. `to_executable` is 2 shorter, `target_executable` 1 longer. | Same sources. | | 31 | `http_thread_on_init_error` goes from 107 to 99. **The empty default of the field is the address of a static NUL byte, where it was a constant. Each of the 11 functions that build a default `InitOpts` needs one `lea` more for it:** `preconnect` and `send_sync_for_result` in `bun_http`, and 9 in `bun_runtime`, `FetchTasklet::queue` among them. | One instruction more per call, so per `fetch()`. | | 33, 46, 49, 50 | `init` goes from 2,339 to 2,275 (with 31), with 8 branches, 5 calls and 2 panic sites fewer. `allocate_package_manager` is 4 longer and the drop of `PackageManager` 1. `create_fake_temporary_node_executable` goes from 541 to 518. The readers of the path load it from the manager: between 4 shorter and 3 longer. | One copy of the cwd fewer. | | 36 | `find_workspace_root_above` goes from 1,286 to 1,276. | One copy and one bounds check fewer per package.json above the cwd. | | 39 | `StringBuilder` loses a pointer that release builds computed and stored and never read. `allocate` goes from 62 to 60. Three functions that have the builder inline, and that also copy `Dependency`s (41), have fewer branches and panic sites: `Package::clone` (1,130 to 1,105, −6 and −2), `fetch_necessary_package_metadata_after_yarn_or_pnpm_migration` (1,055 to 1,042, −2 and −1) and `get_or_put_resolved_package_with_find_result` (3,118 to 3,123, −6 and −2). | Same strings. | | 40 | The guard's `drop` (21) has another name. `install_with_manager` goes from 3,871 to 3,864 (with 7, 35 and 47). | Runs at the same point. | | 41 | `parse_with` goes from 858 to 848, `Group::clone` from 38 to 34, `default` and `from` lose 2 each. `parse_dependency` goes from 1,842 to 1,844, with one panic site fewer. | Same ranges. | | 42 | `edit_after_resolve_slow` goes from 3,471 to 3,447 and `move_to_front` from 144 to 123. | Same package.json. | | 43 | `tarball_stream::drain_callback`, which has `populate_result` inline, goes from 1,872 to 1,859, with 5 branches fewer. | Same files. | | 47, 48 | `Diff::generate_inner` goes from 5,192 to 5,193, with 4 branches and 9 compares fewer. `parse_with_json` goes from 4,287 to 4,294, with 5 branches fewer. | Same diff. | Changes 5, 21 and 27 are in the table of checks. Changes 3, 6, 10, 11, 34, 35, 37 and 38 ask for the same operations in the same order. Where they show up at all, it is in registers: `save_lockfile` goes from 294 to 292, and `run_tasks_erased` keeps its 6,254. Changes 15, 32, 44 and 45 are not compiled on Linux. The comparison covers x86-64 Linux only, one crate at a time. The build that ships also inlines across crates when it links. ### The changes in detail #### 1. toml: the parser keeps tables and arrays as `StoreRef` `e_object()` and `e_array()` return a `StoreRef`, which is `Copy` and derefs to the node. The parser called `.as_ptr()` on it, passed `*mut E::Object` / `*mut E::Array` around and dereferenced the pointer in six `unsafe` blocks. It keeps the `StoreRef` instead, as the YAML and XML parsers do. The `meta` map is still keyed by the node's address. #### 2. bun_core: remove `boxed_zeroed_unchecked`, `ZStr::boxed` and `OptionsEnvArg for Box<ZStr>` Nothing calls `boxed_zeroed_unchecked`. `ZStr::boxed` is called only by `impl OptionsEnvArg for Box<ZStr>`, and `append_options_env` is used only with `&'static ZStr`, `String` and `OptionWord`: no `Box<ZStr>` exists anywhere else. One `unsafe fn` and three `unsafe` blocks go with them. #### 3. xml: `Unit::from_u16` and `Unit::from_bytes` instead of casts The scanner is generic over `u8` and `u16` code units. Three places that run for one width only turned a value of the concrete type into the generic one, or back, with `transmute_copy` or `from_raw_parts`. `find_ascii` uses `Unit::bytes`, which is the identity for `u8`. The other two are trait methods: the identity in the impl for the width they are reached with, `unreachable!()` in the other one, where `units_of` had `assert!(!U::WIDE)`. Each caller is behind the constant `U::WIDE`. #### 4. crash_handler: `UNSUPPORTED_UV_FUNCTION` holds the name's bytes `CrashHandler__unsupportedUVFunction` stored the `*const c_char` it was given in a thread local, and the two places that print it turned the pointer into bytes again in an `unsafe` block each. The function already measures the string for its own panic message. It does that first and stores the `&'static [u8]`: it never returns, which its signature says with `-> !`, so the string outlives every read. The readers take the slice as it is. The arm for a null `name` goes: the function's contract excludes null, and it passed `name` to `cstr` a few lines further down anyway. #### 5. xml_index: `StructuralIndex::seek` and `at` read the window with `get` Both compared an index with `win.len()` and then read the element with `get_unchecked`. `slice::get` is that comparison and that read. In `seek` the loop condition becomes `while let Some(&p) = win.get(rel)`. `at` calls `fill_to` when `get` misses and tries again: `fill_to` returns only once the index is in the window. `seek_refill` called `at` for every index. It calls `fill_to` and then walks the window as `seek` does, so it reads `base` and the window once per fill. #### 6. collections: `HiveSlot` borrows its hive `HiveSlot<'h, ..>` kept the hive as a `*const HiveArray` that is null for a heap slot, next to a `PhantomData<&'h HiveArray>`, and its `Drop` dereferenced the pointer in an `unsafe` block. That pair is `Option<&'h HiveArray>`: the same word with the same null test, and `'h` was borrow-checked already. All of `HiveArray` is in `UnsafeCell` or `Cell`, and `HiveBox` holds the same reference. `HiveArray::drop_all` was an `unsafe fn` with `Drop` as its only caller. Its body moves there, with the `SAFETY` comment of the call on the `drop_in_place` that needs it. The comment also names what it relies on where a token is forgotten (`assume_init`, `ManuallyDrop`): that the slot has been written. #### 7. bun_core: `vec::extend_from_fn` is `Vec::extend` It reserved, wrote `f(i)` into `spare_capacity_mut()[..n]` and called `set_len` in an `unsafe` block. `(0..n).map(f)` is a `TrustedLen` iterator, for which `Vec::extend` does the same: one `reserve`, raw writes, and the length kept in a local until the end. The check of `spare[..n]`, which the compiler could not remove, goes away. If `f` panics, the elements written so far are in the `Vec`, which is what the doc comment says. #### 8. fmt: the shared temp buffer is an `Option<Box>` The thread local held `Option<NonNull<[u8; 32 KiB]>>` in a wrapper type with hand-written `Drop` and `Deref`, and a guard type took the pointer out, dereferenced it, and on drop put it back or freed it: three `unsafe` blocks for what `Option<Box<_>>` does by itself, in the same word. `with_shared_temp_buffer` takes the `Box` out of the cell (or allocates one), lends it to a closure and puts it back. The three users run their body in that closure. While one runs the cell is empty, so a writer that formats UTF-16 or Latin-1 again still gets a buffer of its own. After such a nested use the inner buffer is the one that is freed, not the outer. #### 9. SmolStr: `Inlined::all_chars` views the `u128` with `bytes_of_mut` It cast `&mut self.0` to `&mut [u8; 15]` through a raw pointer. `bytes_of_mut` gives the 16 bytes of the `u128`, as `slice()` above does with `bytes_of`, and `first_chunk_mut` the first 15. The length is a constant, so the `unwrap` folds away. `Inlined::ptr` had no other caller. #### 10. bun_core: `impl io::Write for Writer` calls the inherent methods `write_all` and `flush` of the trait impl repeated the bodies of the inherent `Writer::write_all` and `Writer::flush`, `unsafe` block included. They call those instead. All four are `#[inline]`. #### 11. atomic_cell: `unreachable!()` for the widths that `Atom` excludes `size_dispatch!` matches on `size_of::<T>()`, a constant in every instantiation, so an optimized build drops the arm for other widths however it is written, and `unreachable_unchecked()` bought nothing. #### 12. highway: `memmem` subtracts addresses The index of the match is the pointer that `highway_memmem` returns minus the start of the haystack. For `u8`, `offset_from` is that subtraction and nothing else, and subtracting the two `addr()`s needs no `unsafe`. It is the same `sub` where overflow checks are off. #### 13. Check constant strings at compile time instead of `from_utf8_unchecked` The crash handler's `GIT_SHA` is the first 7 characters of a `&str` constant: `str::split_at` is a `const fn`, so the detour through bytes and `from_utf8_unchecked` is not needed. `env::GIT_SHA_SHORT`, the first 9 characters, made the same detour through `const_str_slice`, with `core::str::from_utf8`. It uses `split_at` too, and `const_str_slice`, which had no other caller, goes. `__runtime_embed_load` turned the `&[u8]` constants `CODEGEN_PATH` and `BASE_PATH` into `&str` at run time without a check. They become `&str` constants through the `const fn` `core::str::from_utf8`, so that a path that is not UTF-8 fails the build. The doc comment of `strings::str_utf8` names that use instead of `const_str_slice`. #### 14. Call `gettid`, `getpid`, `alarm`, `_exit` and `raise` as `safe fn` Seven `unsafe` blocks in `bun_core` and the crash handler only called a libc function that takes integers or nothing and has no memory-safety precondition. The other OS arms of `thread_id::current_uncached`, and `Global.rs`, `spawn/process.rs` and `ParentDeathWatchdog.rs`, declare such functions as `safe fn` in an `unsafe extern "C"` block next to the caller. These sites do the same, with the signatures that the `libc` crate has, so they call the same symbols (`gettid` still goes through `--wrap=gettid`). For `getpid`, `debug.rs` uses the declaration that `output.rs` has. #### 15. bun_core: `fd_path_raw_w` takes `&mut [u16]` It was an `unsafe fn` that took a pointer and a capacity, and both callers passed `as_mut_ptr()` and `len()` of a slice. With the slice, only the call to `GetFinalPathNameByHandleW` is `unsafe`: the `\\?\` prefix is read by index and removed with `copy_within`. Every index comes after a comparison of `n`, which is the length of the slice, with a larger constant, and each `copy_within` range after one with a constant no smaller than its start, so no bounds check is left. That is why `copy_within` is called in each of the two arms and not once with the offset they produce. #### 16. output: `STDOUT_STREAM` and `STDERR_STREAM` are atomics Each of the two statics holds a file descriptor that `set_init` writes once at startup and that every thread reads when it is configured. They were `RacyCell<File>`, read and written in `unsafe` blocks. An `AtomicI32` (`AtomicU64` on Windows, where an `Fd` is 64 bits) with `Relaxed` loads and stores is the same plain load and store, as for `bun_stdio_tty` below. #### 17. resolver: `BrowserMapPath::map` is `&'static BrowserMap` The one constructor passes `&package_json.browser_map`, and `DirInfo::package_json()` already returns `&'static PackageJSON`. The field shortened that to `'b`, so each of the four matches in `check_path` widened the value back with `Interned::assume`. With the real lifetime on the field, `map.get()` yields a `&'static Box<[u8]>` that is assigned to `remapped` as is. #### 18. resolver: keep the slot that `MaybeUninit::write` returns `dir_info_cached_miss` wrote a queue item with `queue[i].write(..)`, dropped the `&mut` that call returns, and a few lines later got the same slot back with `unsafe { queue[i].assume_init_mut() }`. Only `rfs.entries.get(top)` runs in between and it does not touch `queue`, so the returned reference is kept. #### 19. resolver: drop three lifetime casts that change nothing `BSSStringList::append` and `print` return a slice whose lifetime the caller picks, so `FilenameStoreAppender::append` and the stores' `print` already had the lifetime they return; the `Interned::assume(..).as_bytes()` after each call was a no-op, as `append_slice` right above shows. `add_entry_with_store` dereferenced `stored` twice, once behind a `&*ptr::from_ref(..)` of a slice that already had an unbounded lifetime. It now makes one `&'static Entry` and uses it for the key and for the return value. #### 20. resolver: `RealFS::read_directory` and `EntryLookup` keep their `'static` `RealFS::read_directory` forwards to `read_directory_with_iterator`, which returns `&'static mut EntriesOption`, but declared the result as a borrow of `&mut self`. `FileSystem::read_directory` and `DirEntryAccessor::openat` cast it back to `'static` through a raw pointer. It now returns what it gets, and both callers use the value as it comes. `EntryLookup<'a>` tied the `&Entry` it hands out to the `DirEntry` it was looked up in, while its own safety note rests on the entry being a slot of the never-freed `EntryStore`, the only source of the pointers in `DirEntry::data`. `EntriesOption::lookup` widened a `&DirEntry` to `'static` only to get an `EntryLookup<'static>` out of `get`. The parameter is gone and `entry()` returns `&'static Entry`. One `&mut Entry` could overlap such a reference: `add_entry_with_store` borrowed a recycled entry mutably to store `dir`. All four callers that pass a previous map build the new `DirEntry` with a copy of the old one's `dir`, and an entry's `dir` is only ever a copy of its `DirEntry`'s, so the store wrote the pointer and length that were already there. The store is gone and the entry is borrowed shared; the rest of that block uses `mutex`, `need_stat` and `cache()`, which take `&self`. #### 21. http: `h3::ClientContext::get_or_create` reads the loop itself Both callers passed `unsafe { NonNull::new_unchecked(http_thread().uws_loop) }` and the callee only unwrapped it again with `as_ptr()` for the FFI call. The parameter is gone; the existing FFI block passes `http_thread().uws_loop()`, and only when the engine has to be created. #### 22. boringssl: `ssl_ctx_setup` takes `&OwnedSslCtx` It was an `unsafe fn` over `*mut SSL_CTX` whose only requirement was that the context is live. Its two callers, both in `HTTPContext`, had just stored an `OwnedSslCtx` in `self.secure` and fetched the raw pointer back through an `unwrap`. `OwnedSslCtx` owns a reference, so taking it by reference puts that requirement in the type and the function is safe. The callers pass what `self.secure.insert(..)` returns, and `HTTPContext::ssl_ctx` is gone. #### 23. sourcemap: `impl_pod!` for `SyncEntry` and `ObjectBrace` `Builder::finalize` viewed both tables as bytes with `slice::from_raw_parts`, and `for_each_object_brace` read each `ObjectBrace` with `ptr::read_unaligned`, each behind a comment that the struct is `repr(C)` with no padding. `impl_pod!` has rustc prove that neither struct has padding and that every bit pattern is a value of it, so the views are `slice_as_bytes` and the read is `read_prefix`. The macro does not look at the `repr`: both structs and `LineColumnOffset` are `repr(C)`, and `Ordinal` is `repr(transparent)`. The byte lengths come from those slices, so `copy_from_slice` compares the same values as before, and `read_prefix` is given a `&[u8; 16]`, so its length test is a constant. #### 24. sourcemap: `WindowReader<'a>` borrows the stream `WindowReader` kept the stream as `*const [u8]` and handed it out through `fn bytes<'a>(&self) -> &'a [u8]`, an unbounded lifetime from a raw pointer. Every reader is a local of `find`, `for_each` or `append_vlq_to`, so the field is `&'a [u8]`: copying the reference out leaves `self` free to mutate, which is what the accessor was for. A reader started out as `DANGLING`, with a null stream, and `parse` stored the stream it was given. A reference cannot be null, so `WindowReader::new(stream)` takes the stream, and `parse` reads it from `self`. The two loops compute the stream once instead of once per window, and so also for a map with no windows. That is three header loads that are valid for every blob; `object_braces()` already does them unconditionally. #### 25. resolver: four `unsafe` blocks that plain borrows replace - `Resolver::standalone_module_graph` is `Option<&'static dyn ..>`. Its only two setters, in `VirtualMachine`, assign a `'static` graph, so the transmute in `for_worker` that widened `'_` to `'a` is a copy. `for_worker` stays `unsafe` for `env_loader`. - The close-or-store guard in `RealFS::kind` captures `&mut cache.fd` instead of a `*mut EntryCache`; nothing else touches `cache` while the guard lives. - `FileSystem::init_with_force` returns `INSTANCE.get().cast()` when already loaded, instead of going through a `&mut MaybeUninit` for the same address. - `read_file_with_handle_impl` writes the trailing NUL through `spare_capacity_mut().first_mut()`: the same compare and the same store. #### 26. resolver: `package_json` travels as `&'static PackageJSON` `Result::package_json`, `MatchResult::package_json` and the `read_dir_info_package_json` hook held `Option<*const PackageJSON>`. Every value stored there comes from `DirInfo::package_json()`, `DirInfo::enclosing_package_json` or `FallbackModule::package_json`, which are `&'static PackageJSON` already, so each store cast a reference to a pointer and each read cast it back with `unsafe { &*p }`. The reason given for the pointer was that zeroed initialisation stays valid. Nothing zero-initialises these structs, their `Path` fields hold slices that could not be zero anyway, and an all-zero `Option<&T>` is `None`. The fields and the hook hold `Option<&'static PackageJSON>`, the four functions that store their `package_json` argument take `&'static`, and `Result::deref_package_json` is gone. Both structs shrink by 8 bytes. #### 27. resolver: `load_tsconfig` keeps its boxes Each `Box<TSConfigJSON>` from `parse_tsconfig` was turned into a raw pointer, kept in a `BoundedArray<*mut _, 64>`, read through a `BackRef`, dereferenced twice per merge step and turned back into a box twice. Nothing but this walk holds the configs, so they stay boxes: `current` owns the newest one and the array owns the ones before it, 63 of them, so a chain is still cut off at the 65th config. Two `expect`s and an `unwrap` go with the pointers. When a chain is that long, the configs read so far are freed with the array instead of leaked. #### 28. resolver: `BIN_FOLDERS` is initialised at compile time The static was a `RacyCell<MaybeUninit<BinFolderArray>>` with a `BIN_FOLDERS_LOADED` flag beside it, because `BoundedArray` had no const constructor: both append sites first wrote an empty array under the flag, and every access went through `assume_init_*`. `BoundedArray::new()` is a `const fn` with the body of `default()`, so the static starts as an empty array. The flag, its three loads and both initialising blocks are gone; `bin_dirs()` returns the empty slice of that array where it returned `&[]`. #### 29. standalone_graph: in `inject`, `zname` borrows `zname_owned` The two places that repoint `zname` at a heap buffer stored the box in `zname_owned` and then rebuilt a `&ZStr` from its raw pointer and length, with a comment that the box outlives it. `zname` now borrows what `zname_owned.insert(..)` returns, so the compiler checks that: the old `zname` is last used to build the new buffer, before `zname_owned` is overwritten. The `let _ = (&mut zname_owned, &mut zname);` that keeps Windows from warning about two variables it never changes is compiled for Windows only, since it would borrow `zname_owned` again while `zname` is in use. #### 30. sourcemap: `ParsedSourceMap` holds the standalone graph's sources by type The standalone graph boxed its own `SerializedSourceMapLoaded`, cast the pointer to `*const SourceProviderMap` and stored it as an integer in `underlying_provider`, with `is_standalone_module_graph` set so that nothing would call it as a provider. `Lookup::get_source_code` cast that integer to `SerializedSourceMap::Loaded`, a different `repr(Rust)` struct with the same fields, and dereferenced it. The flag is replaced by `standalone_module_graph: Option<&'static Loaded>`. The graph builds a `Loaded` and leaks it, as it leaked the box before, and `get_source_code` reads through the reference, before it asks for a provider, which such a map no longer has. The duplicate struct, `standalone_module_graph_data` and `SourceContentPtr::data` are gone, and so is `SourceContentPtr::from_provider`, which only forwarded to `from_source_provider` and whose one remaining caller calls that instead. #### 31. http: `InitOpts::abs_ca_file_name` is a `&'static ZStr` The field was a `&'static [u8]` that was NUL-terminated by contract. `on_init_error_noop` rebuilt a `ZStr` from its pointer and length in an `unsafe` block, while the one producer, `PackageManager::init`, stored a slice whose length included the NUL, and its own error handler stripped it again. The producer already holds a `ZBox`, so it parks that in `ABS_CA_FILE_NAME` and passes the `&ZStr`. Both handlers use the field as it is. #### 32. exe_format: `_mh_execute_header` is a `safe static` `find_identity` on macOS copied the header out of the extern static with `unsafe { header_address.read() }`. The linker defines the symbol for every executable, the header is eight integers, and it stays mapped read-only for the life of the process, so reading it has no precondition. The declaration says so with `safe static` and the header is read by value. The block that views the load commands after it stays. #### 33. install: `init` reborrows the `&'static mut DirEntry` it matched on `FileSystem::read_directory` returns `&'static mut EntriesOption`, and its `Entries` arm holds a `&'static mut DirEntry`. `init` and `init_with_runtime_once` turned the matched reference into a raw pointer and back in an `unsafe` block to get a `'static` lifetime that a plain reborrow already has. Both now write `&mut **e`, as `init_with_runtime_once` does further down for `manager.root_dir`. #### 34. install: `save_lockfile` uses `progress.root` instead of a stashed node pointer `Progress::start` returns `&mut self.root`. `save_lockfile` kept that reference as a `*mut Node`, initialised to null, and dereferenced it in two `unsafe` blocks. The first use is now chained onto `start()`, and the second is `this.progress.root.end()`, which is the same node and the spelling the function already uses two lines further down. #### 35. install: drop four raw-pointer and lifetime detours that borrowck does not need - `install_with_manager`: `maybe_root.parse` got `&mut *mgr` through a raw pointer. Its other arguments are a stack `Lockfile`, the detached `log_mut()` and an owned source, so `manager` can be passed directly, as `bun link` does. - `run_tasks`, extract arm: `alias` was `detach_lifetime(tarball.name.slice())` next to `resolution`, a plain borrow of the same `tarball`. While it is live the arm only touches `task.data`, `Copy` fields and `&self` accessors, and makes shared reads of `task.request`. - `run_tasks`, manifest arm: `name` was detached from `task.callback`. Every other use of `task` in the arm is a different field, except `retry_with_abbreviated_manifest(&mut self)`, whose two arguments are now computed first, in the same order. - `populate_manifest_cache`: `(*mgr).end_progress_bar()` after `sleep_until` returned is `manager.end_progress_bar()`; `mgr` is not used afterwards. #### 36. install: `find_workspace_root_above` borrows `walk.path` for the source path Each package.json above the cwd had its path copied into the process-global `ROOT_PACKAGE_JSON_PATH_BUF` in an `unsafe` block, only to name the `Source` that is parsed in the same loop iteration. `Source<'a>` already borrows the loop-local `json_buf`, the body does not touch `walk` again, and log locations own their bytes, so the source now borrows `walk.path`. This removes one copy and one bounds check per package.json. That check could not fail: the copy was only reached after `openat_a` had opened the path, and `openat_a` refuses a path that does not fit a `PathBuffer`. The global buffer is left with a single writer, `init`. #### 37. install: `AutoInstaller::path_for_resolution` takes `&mut PathBuffer` The trait method took `&mut [u8]`. Its only implementation debug-asserted the length and cast the slice's pointer back to `&mut PathBuffer` in an `unsafe` block, and its only caller, the resolver, passes `bufs!(path_in_global_disk_cache)`, which is a `&mut PathBuffer` that was deref-coerced to a slice. The parameter now has the type both ends use, so the cast, the assert and a length that release builds never checked are gone. The call in the resolver is unchanged. This was the last place that reinterpreted a `&mut [u8]` as a `PathBuffer`, so the sentence in the `PathBuffer` doc that gave such casts as the reason for its alignment is gone too. #### 38. install: `NetworkTask` reads its manager through the `ParentRef` it stores `pm_mut()` turned the `package_manager` back-reference into a `&'a mut PackageManager` with an unbounded lifetime. Its two users, `manifest_request` and `for_tarball`, only call `&self` methods and read fields, while their callers hold the manager mutably. They now copy the `ParentRef`, which is `Copy` and derefs to `&PackageManager`, into a local, so `self` stays free to be assigned to. The accessor is deleted. #### 39. install: remove three one-line `unsafe` blocks that have a safe spelling - `lockfile::StringBuilder.ptr` was an `Option<*mut u8>` computed with `as_mut_ptr().add(prev_len)` and only ever asked `is_some()`, in debug assertions. It is now `allocated: bool`. - `Task::Id::for_npm_package` hashed the bytes of a `semver::Version` through `ffi::slice`. `VersionType<u64>` is `NoUninit`, so `bun_core::bytes_of` yields the same bytes. - `__bun_regex_compile` takes a reference and returns an `Option`, and its definition in `bun_jsc` is a safe fn, so the `extern "Rust"` declaration in `bun_install_types` says `safe fn`. #### 40. install: clear `root_lifecycle_scripts` with a `scopeguard` on `manager` `install_with_manager` declared a local `ClearRootLifecycleScripts(*mut PackageManager)` whose `Drop` wrote through the raw pointer. The pointer was taken from `manager`, and `manager` itself was used for the rest of the function, so the pointer was stale by the time the guard ran. The guard now owns the `&mut PackageManager` and the rest of the function reborrows it from the guard. It is declared at the same place, so it runs at the same point. #### 41. semver: delete `query::Group.input` `Group` stored the parsed text as a lifetime-erased `*const [u8]`. The only read was in `Package::parse_dependency`, an `unsafe` deref in the same function that had passed that slice to `parse`; `bun lint` overwrote the field after parsing so that it would not dangle. `parse_dependency` now keeps the slice next to the range, in `workspace_range: Option<(Group, &[u8])>`, and the field is gone, which makes `Group`, and every `DependencyVersion` and `Dependency` with it, 16 bytes smaller. #### 42. install: `UpdateRequest.e_string` keeps the `StoreRef` it is made from The field was `Option<*mut E::String>`. Every writer got a `StoreRef<E::EString>` from `Data::e_string()` or `string_entry()` and called `.as_ptr()` on it, and every reader dereferenced the raw pointer in an `unsafe` block. `StoreRef` is the AST's own `Copy` handle, with `Deref`, `DerefMut` and pointer-identity `PartialEq`, and `edit` already writes through one a few lines below. The field now stores the handle, so the writers drop `.as_ptr()`, the readers in `edit` and `add_catalog` use it directly, and the two comparisons compare handles. #### 43. install: `TarballStream::init` returns a `Box` and `populate_result` is a safe fn - `init` ended in `heap::into_raw(Box::new(..))`, and both callers passed the pointer straight to `heap::take`. It now returns the `Box`. - `populate_result` was an `unsafe fn` taking `*mut Task`, with its whole body in one `unsafe` block. All it needs from the task is the tarball of the `extract` request and the log, and all it leaves there is `data`, `err` and `status`. It now takes `&ExtractTarball` and `&mut Log` and returns `Result<ExtractData, Error>`, with no `unsafe` inside. `finish`, which is `unsafe` anyway and reads the same union field again further down, reads the union and stores the three fields. - The entry path was built with `OSPathSliceZ::from_raw_mut`, although only the Windows escape pass writes to it, and that pass starts by asking for the slice. The checks and the escape pass now work on `&mut norm_buf[..norm_len]`, and the NUL-terminated view is made afterwards with `from_buf`. That leaves `WStr::from_raw_mut`, `WStr::as_mut_slice` and `DerefMut for WStr` without a user, so they are deleted. - The comments in `init` and `finish` name the function that sets the `extract` variant, and describe how the stream is really freed: by dropping `network_task.tarball_stream.take()`. #### 44. install: the Windows hardlink task is passed and reclaimed as a `Box` `HardLinkWindowsInstallTask::init` returned `into_raw(Box::new(..))` for an `unsafe fn push(*mut T)`, and `run_from_thread_pool` kept the raw pointer: it called `run()` and stored `err` through it, and reclaimed the `Box` on two separate paths. The worker owns the task from the first line of the callback, so it now takes the `Box` back right after the `complete_one` guard, as `UninstallTask::run` does. `init` returns the `Box`, and `push` takes it, is a safe fn, and requires `TaskType: Send + 'static`, like `OwnedTask`. The drop order is unchanged: the slot lock, the task, then `complete_one`. #### 45. install: the Windows `Hardlinker` keeps the cwd slice it gets `get_fd_path_w` returns a sub-slice of `cwd_buf`. `link` turned it into an offset, with `offset_from`, and a length, and sliced the buffer again for every file, because the slice supposedly could not stay borrowed across the loop. Nothing else uses `cwd_buf`, so the slice is now kept and used directly. This also saves one bounds check per linked file. #### 46. install: `ROOT_PACKAGE_JSON_PATH` becomes `PackageManager::root_package_json_path` The path was a `pub static RacyCell<&ZStr>` that `init` wrote and eight places read in `unsafe` blocks. Every reader has the manager at hand, so the path is now a field that `allocate_package_manager` fills in. `init` passes the `ZStr` it builds in the static buffer, and auto-install passes `ZStr::EMPTY`, the static's initial value, which `root_patched_dependencies` relies on to fall back to the package.json next to the lockfile. A process allocates one manager: a second `init` only follows `MissingPackageJSON`, which is returned before the path is built and before the manager is allocated, and `init_with_runtime` runs under a `Once`. #### 47. install: `Diff::generate` takes `to_update: bool` instead of the update requests `install_with_manager` passed `Some(&manager.update_requests[..])`, made in an `unsafe` block, next to `&mut *manager` whenever `manager.to_update` was set, and `None` otherwise. `generate_inner` only asked the slice whether it was empty, which it also asks `pm.update_requests` itself at its top. The parameter is now the bool, and the emptiness is read once from `pm` and used in both places. The callers that passed `None` pass `false`. The SAFETY text at that call also says that `Diff::generate` never reaches `manager.lockfile` through `pm`. That was false with `--frozen-lockfile` and for `bun pm`: `parse_with_json` built `SkipIfInLockfile(&pm.lockfile)` on every parse, while `from_lockfile` was a live `&mut` to the same `Lockfile`. Only the two `process_names_array` calls of the `workspaces` group consume the value, so it is now built only under `FEATURES.workspaces`, which the parses that `Diff::generate` and the folder resolver make do not set. The behaviour is the same. The comment in folder_resolver.rs states that property, and the one in prune.rs no longer claims it for its own `Features::main()` parse, which does read `pm.lockfile`, at a point where nothing else borrows it. #### 48. install: `Package::parse_from_real_manager` takes `&mut PackageManager` It was an `unsafe fn` taking `*mut PackageManager`, and its one caller passed `ptr::from_mut(self)` in an `unsafe` block. The contract asked for a live manager whose `lockfile` and `log` point to live allocations outside it. A `&mut PackageManager` gives the first, `lockfile` is a `Box`, and `log` comes from the safe `log_mut()`. What is left is that the `&mut Lockfile` it splits off must be the only way to the lockfile during the parse. `parse_with_json` derefs `pm.lockfile` only under `Features::workspaces`, so the `features` parameter is gone and the function always parses with `Features::NPM`, which is what its caller passed. With that nothing is left for a caller to promise, and the function is safe. One `unsafe` expression remains inside it, the `&mut *lockfile`. #### 49. install: in `init`, `original_cwd` borrows the boxed copy of the cwd `original_cwd` was a slice of `original_package_json_path_buf`, reached through `ZStr::from_raw` so that the borrow checker would not see it. The directory walk, `this_cwd` and a later pointer comparison all hang on that slice, and they are still in use when the buffer is truncated and gets `/package.json` appended again. If package.json is in a parent directory, that write lands inside `original_cwd`. `init` already makes a boxed copy of the cwd to return it. The copy is now made from `top_level_dir_no_trailing_slash`, the bytes the buffer was filled with, and `original_cwd` borrows it, so the walk no longer points into the buffer. The first `original_package_json_path` is only read before the buffer changes and is reassigned before its next use, so it can be a plain `from_buf` borrow. Nothing read the overwritten bytes, so the behaviour is the same. #### 50. install: remove two pieces of dead code that needed `unsafe` - `create_fake_temporary_node_executable` turned a non-empty `optional_bun_path` into a `ZStr` with `from_raw`, trusting the caller of a safe `pub fn` to have put a NUL behind the slice. Both callers pass an empty slice and only read what the function writes back, so the branch is gone. - `init` copied the cwd into `CWD_BUF` right after reading it. Nothing reads the buffer before `init` overwrites it further down and publishes it as `top_level_dir`, so the first copy is gone. #### 51. xml_index: `seek_refill` calls `at` for every index, as it did The commit that made `seek` and `at` read the window with `get` also gave `seek_refill` a loop of its own over the window. That left `at` without a caller outside the unit tests and the benches, which the `unused_pub` lint reports. `seek_refill` has its old body again. Compared with `main`, `seek_refill` is one instruction, one compare and one branch longer (35 to 36 instructions, for `u8` and for `u16`): `at` compares the index with the length of the window again after `fill_to` has returned. That happens once per refill. #### 52. resolver: `EntriesOption::try_entries_mut` returns the listing or the read error `init` and `init_with_runtime_once` of the package manager turned the result of `read_directory` into a `&'static mut DirEntry`, or returned its error, with one `match` each. Since an earlier commit wrote `&mut **e` in both, the two are equal arm for arm, which the `same_match_twice` lint reports. The mapping is now a method of `EntriesOption`, next to `entries_mut`, which panics on `Err`. Both callers apply `?` to it. The result borrows from `&mut self`, which is `'static` for the value that `read_directory` returns. Nothing changes in the compiled code: the method is inlined into both callers. #### 53 and 54. Two fixes for tests of `main`'s, which the merge of `main` takes out again `test/aot/engine/ir-6.test.ts` and `test/aot/build-scaling/build-scaling-2.test.ts` failed without this PR too. Commit 53 brought in the change that #47074 makes to one fixture, and commit 54 the two files of #47329, byte for byte. #47316 has since rewritten all three files on `main`. The fixture no longer has the eleven types that commit 53 relaxed, and the shape of commit 54 no longer has a recorded growth: it went from 17,206 to 4,416 million instructions. Two of the three files conflicted. The merge takes `main`'s version of all three. #### 56. resolver: `read_directory` and its siblings return `&'static EntriesOption` `FileSystem::read_directory`, `RealFS::read_directory`, `read_directory_with_iterator` and `read_directory_error` returned `&'static mut EntriesOption`: a slot of the shared directory cache, or the thread's scratch slot. They are safe functions, so two calls for one directory gave safe code two `&mut` to one slot. That is so on `main` too, for all but `RealFS::read_directory`, whose result change 20 widened to what its callee returns. `entries_at_locked` handed out a `&mut` to the same slots. No caller writes through the result: - The package manager reads `data`, `dir` and `fd` and calls `has_comptime_query`. - The test scanner reads the error or walks `data`. - `DirEntryAccessor::openat` and `load_as_file` made a shared reference of it at once. - Both callers of `entries_at_locked` call `as_entries`. So they all return a shared reference, `PackageManager::root_dir` is a `&'static DirEntry`, and the `try_entries_mut` of commit 52 is `try_entries(&self)`. What stays as it is on `main`: a read with a newer generation rewrites a slot's `DirEntry` in place under `entries_mutex`, and the next miss on a thread overwrites its scratch slot, while an earlier result may still be held. Readers of `data` take the mutex, and the doc of `read_directory` says to read an `Err` before the next call. ### How did you verify your code works? - **Static checks on the first 52 commits, which hold all the code:** `cargo fmt --check`, `cargo clippy --workspace` (0 warnings), the source lints and `cargo check --workspace --release` on Linux, and `cargo check --workspace --release` for `x86_64-pc-windows-msvc` and `aarch64-apple-darwin`, because seven changes touch code that only those targets compile. - **Assembly:** the comparison above. - **230 test files**, on a debug build with ASAN of these commits and on a debug build of the base: all 114 files of `test/cli/install/`, and the files for the rest: TOML and XML, source maps, the shell's brace expansion, `test/js/bun/resolve/`, the bundler's resolver tests, `bun build --compile`, HTTP/3 and TLS, `console.log` and `Bun.inspect`, auto-install and `bunfig.toml`. - 220 files pass completely, 17,876 tests. - 10 files fail, in exactly the same tests on both builds: | File | Tests that fail | Why | |---|---|---| | `bun-run.test.ts` | 28 | They run `bun-debug` by name, and it was not on my `PATH`. | | `bun-install-git-deps.test.ts` | 19 | My `git` refuses to make their tags ("no tag message?"). | | `bun-install.test.ts` | 4 | Git URLs. | | `bun-install-lifecycle-scripts.test.ts` | 2 | "does not use 100% cpu", on a machine that ran other tests at the same time. | | `bundler_compile.test.ts` | 7 | Packages that I did not have. | | `bun-build-compile.test.ts` | 3 | Their time limit of 60 seconds. | | `run-crash-handler.test.ts` | 1 | It expects a frame name that a debug build prints differently. | | `resolve.test.ts` | 1 | `tsyringe`, which I did not have. | | `stringWidth.test.ts` | 1 | `string-width`, which I did not have. | | `inspect-error-leak.test.ts` | 1 | Its time limit of 10 seconds: 115 seconds here and 116 on the base, and +359 MB on both. | - The 13 source-map files and `brace.test.ts` ran after my last edit to change 24, the way a `WindowReader` gets its stream. The other files ran before it. - **After the tests had run** I removed six `#[inline]` attributes that an earlier form of changes 3 and 9 had added, and took one edit out of change 14, so that the crash handler calls `libc::abort()` as it does on `main`. The static checks ran again after that, and so did the assembly comparison of `bun_crash_handler`, with the numbers above. - **`bun.lockb`:** a build of the base, a build of the first 52 commits and Bun 1.4.4 write the same 10,218 bytes for one project, which has ranges with `||`, a hyphen, `*`, `1.x` and prereleases, an alias, a dist-tag, two catalogs, two workspaces, an override, and peer, optional, dev and trusted dependencies. Each of the three reads what each of the three wrote: `bun bun.lockb` prints the same text and the same hash, `bun install --frozen-lockfile` passes and leaves the file as it is, `bun pm ls --all` prints the same tree, and `bun add` turns the file into the same 10,554 bytes. The ten `.lockb` files in the repository print the same text on all three. - **Commits 51 and 52 came after that.** With them I ran all the static checks again and compared the assembly of `bun_parsers`, `bun_resolver` and `bun_install` again: 52 changes no instruction, and the numbers above for `bun_parsers` include 51. On a debug build of these 52 commits, `xml.test.ts`, `xml-test-suite.test.ts`, four auto-install files, `bun-remove.test.ts`, `bun-link.test.ts` and `bun-pm.test.ts` pass completely, 2,315 tests. `bun-add.test.ts` and `run-autoinstall.test.ts` did not finish in the 50 seconds that I gave each file. - **The merge of `main` (3bf1a0646) came last.** Everything above is from before it. - The only conflicts were in two of the three test files of commits 53 and 54. 16 files under `src/` that both sides changed merged by themselves. - The 2,496 lines that this PR adds and removes under `src/` are the same before and after the merge, in the same order, and so are the counts of `unsafe` against `main`. - On the merged tree, `cargo fmt --check`, the source lints and `cargo clippy --workspace` (0 warnings) pass, and a debug build with ASAN and a release build link and run. - On the debug build I ran 30 test files for the code that both sides changed and for the parsers and the package manager: `import.meta.glob`, import queries, `test/js/bun/resolve/`, the bundler's resolver cache, source maps of compiled executables, the crash handler, `bun:ffi`, TOML, XML, `bun pm`, `bun link`, `bun remove` and `bun.lockb`. 29 pass completely, 3,997 tests. In `resolve.test.ts` one test fails, the one that needs `tsyringe`, as in the table above. - **Commit 56** came after the merge. With it, `cargo fmt --check`, the source lints, `cargo clippy --workspace` (0 warnings) and `cargo check --workspace --release` pass on Linux, and so does `cargo check --workspace --release` for `x86_64-pc-windows-msvc` and `aarch64-apple-darwin`. The release binaries with and without it have the same bytes in `.text` (105,908,048 bytes), `.rodata`, `.data`, `.data.rel.ro` and `.eh_frame`. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
amalshaji
reviewed
Nov 4, 2026
| @@ -60,22 +60,3 @@ jobs: | |||
| run: | | |||
| bun x tsc --noEmit -p src/tsconfig.json | |||
This was referenced Nov 7, 2026
springmin
pushed a commit
to springmin/bun
that referenced
this pull request
Nov 8, 2026
Follow-up to oven-sh#47316 for `bun build --compile --aot` and `bun test --aot`. Goes with oven-sh/WebKit#967; WEBKIT_VERSION is pinned to its preview build until it merges. bun test --aot: - Tests can replace `Math`, `JSON`, `Reflect` or `process` on the global object: `global.Math = mockMath`, `globalThis.process = undefined`. - Tests can pose as another runtime: redefining or assigning `process.versions`, `process.release`, `process.isBun`. - `swap(Date, { now() { return 5 } })`, where a helper in another file copies the members in a `for...in` loop, no longer fails with "Cannot assign to Date.now". - In layered runs, built-ins reached through another file stay writable: `export { getPrototypeOf } from "./tools.js"`, `export const above = x => Object.getPrototypeOf(x)`, a static field, a `Map`. - Names that `mock.module()` adds or leaves out are seen through `export *`. - A `mock.module()` factory that throws rejects the import the way `bun test` does, also for a module that is not on disk. Module loading: - `require.cache[key].exports` of a file required as a statement is its exports, not `undefined`. The entry of an ES module shows all of its exports. - `require.cache[key].children` and `.parent` are filled for files required at the top level of a test file. - Imports with nothing to run (`export const value = 7`) are listed in `Object.keys(require.cache)`. - `require("./config.json")` in a CommonJS file returns the data while an ES module that imports the same file is still loading. It returned the ES namespace. - `const name = "bun"; (await import(name)).default === Bun` answers what Bun answers. Crashes: - `mock.module("./below.ts", () => { throw .. })` followed by imports of two files above it aborted in builds with assertions. - When the source text an error message quotes failed its own check, the build stopped with an internal compiler error. Now that one file loses the quote. - `function kind(x) { switch (x) { case 3: .. } } kind([2])` aborted with "FrameCellEscaped" in builds with validation on. Wrong answers: - `function pick(i) { return i % 2 ? null : { a: 1 } } function read(o) { return o.a }`: `read(null)` returned 1 instead of throwing. - ES5 classes that share a method: after `delete A.prototype.twice`, `new A(8).twice()` returned 16 instead of throwing. - `try { null.x } catch (e) { e.constructor.extra = 41 }`: `TypeError.extra` was `undefined`. - `Object.getPrototypeOf(crypto.subtle).extra = 41`: `SubtleCrypto.prototype.extra` was `undefined`. Same for `AbortSignal.prototype` and `Bun.S3Client.prototype`. - `class Tags extends Set { constructor(values) { super(values) } }` calls an overridden `add`. Same for `Map`, `WeakMap`, `WeakSet`. - `Array.from(new Map(..))` after the class's iterator was deleted treats the object as array-like. - `Object.assign({}, source)` kept the wrong key order for a source with a hidden index property and a symbol key. Faster: - Classes that Babel compiled to ES5 are compiled as classes: `_createClass`, `_defineProperties`, `_classCallCheck`, `_inherits` and `_typeof`, written into the file or imported from `@babel/runtime`, Babel 7.18 through 7.24. - `var p = Object.create(A.prototype); p.constructor = B; B.prototype = p`: 254 to 25 instructions per iteration. - `util.inherits = function (ctor, superCtor) {..}` in another file: 712 to 19. - `const f = table[key]; for (..) t += f(..)` checks once, before the loop, which function it got. - `out[key] = value` that adds a property is cached per site. `holder[key] = value` checks the field's type once per kind of value. - `for (const [key, value] of [["a", 1]])` and `const [text, count] = ["abc", n]`: each position has its own type. Smaller: - The names of captured variables are written once for closures that capture the same ones. - The registers around a slow-path call are saved by one shared stub. A caller that reads no floating-point register after a call saves none. Build time and memory: - `let picked = s0; if (n > 1) picked = s1; ..` with 2,000 or more branches. - A linked package reached through a computed `import()` no longer causes a second build. Tests and CI: - A test builds each stored program under several orders of the analysis rules, thread counts and checker passes, and fails if the generated code differs. - The instruction counts in `test/aot/perf` are exact, with no tolerance. Not AOT-specific: - `bun test --isolate`: a `process.nextTick()` queued while the second test file's imports load runs after that file's body again, as before oven-sh#47316. - `bun test --parallel`: a worker waiting for its next file sleeps instead of polling. - `bun test`: the memory reservation while tests run is removed (581 lines). - `bun test --isolate`: a `FileSink` closes its descriptor before `close` returns instead of later on the work pool. - `require.cache` is the function it was before the first AOT PR. The traps for compiled programs are a separate function. - `bun check`: a hash set past 16 type parameters, the narrowing index is kept instead of dropped whole, the regular form of a literal is computed once, a discarded task explains no error. - `path.posix.dirname`, `basename` and `extname` of Latin-1 strings use SIMD kernels.
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.
What does this PR do?
Follow-up to #47129 for
bun build --compile --aotandbun test --aot.Goes with oven-sh/WebKit#942, which is merged.
WEBKIT_VERSIONis WebKit'smainat4d45755fed4a, so oven-sh/WebKit#781 and oven-sh/WebKit#945 reach Bun with this PR too, with and without--aot.bun test --aotspyOn()andmock.module()work on compiled modules.tsconfig.json.mock.module()also works on JSON, text, TOML and YAML imports.delete require.cache[path]makes the nextrequire()orimport()load the module again.--parallelis built in layers instead (more than 16 test files that reach more than 32 MB of source, on Linux, not hardened): the packages and Bun's library as one image, the project's own files as a second, and the test files in groups of 16 on top, built as workers need them. On one shard of a private suite (3,052 test files, 16 cores) that takes 458 s at 24 workers, against 937 s for plainbun testat 8 workers, and the largest builder needs 4.7 GB where one program needed 10.6 GB.BUN_DEBUG_TEST_AOT_WORDS=TestRunLayers:0,:1or:2forces a form, and such a run says its form in one line on stderr.--timings=<file>also reads amemorymap, megabytes by test file. No second file of over 1,024 MB starts while one runs.--update-timingswrites the map on Linux.RegExp.prototype.execreplaced,text.replace(/b/, "-")and its like call the replacement.Module loading
require(name)andawait import(name)with a computed name find compiled CommonJS modules..htmlfile that fails to bundle is a build error. A file that's only passed torequire.resolve()isn't bundled as a page (this brokeplaywright-core).throwfrom a CommonJS module was lost in a CJS → ESM → CJS import cycle.--aotonly.require()of a text file in a compiled executable matchesbun build --compile.node:module no longer compiles the whole module.--aot-log-type-mismatchesand--compile-in-placetake emit options from the working directory'stsconfig.json, asbun rundoes.import(name)andrequire(name)are narrowed by what the name can be. On a large program with one such call, analysis went from 138 s to 19 s and peak memory from 11.2 GB to 5.1 GB.ShadowRealmin code that never runs no longer fails the build.Other fixes
test/aot.bun test --aotwhen a test puts aProxyunder a prototype object.import(), each holding aProxywhose handler literal has the same shape, when only one of them is loaded.function* f(n) { yield "a"; if (!(n > 0)) return; f = g; yield* f(n - 1); }: the second[...f(1)]didn't seef = g.["run", ...(on ? ["--verbose"] : []), name]was one array shared by every call.TypeErrorcost more the larger the program and the longer the function.require("./d/" + name), wherebun builddoesn't link the call: JavaScript under an unknown extension returned a path instead of running, a.shfile returned"", and a file that isn't a program returned a path where plain Bun throws.with { type: "file" }all failed to load as Workers.FinalizationRegistrystopped calling back once the closure that names it died while a sibling closure of the same scope lived on.x = nullon a local that's never read again didn't release the object.exports.p = vwas lost when a branch that never runs definespwithObject.defineProperty().--trim: a Worker started from code the build can't see gets the interpreter, transpiler and loaders it needs."a" in proxyandfor...ingave wrong results when the key lives on the prototype of a proxy's target.Symbol.speciesgetter on a user class could be left out of the executable.str.search()with a frozen global or sticky regex didn't throw.--minify-identifiers: a class with standard decorators keeps its original name in.name, in the decorator'scontext.nameand in stack traces.Object.getPrototypeOf(),Reflect.getPrototypeOf()and__proto__onBun.$,Bun.sql,consoleand similar objects match plain Bun.minify.syntaxjoined into one string is no longer dropped.New
Bun.build({ compile })acceptsaotValidate,aotCheckandaotTesting, matching the CLI flags. They're hidden, likeaotDisableandaotHeat. Builds that need JS plugins can now run under the compiler's checker too.--explainpoints at where a value starts to escape, not at the last hop.Faster
localeCompare()without a locale,sort()with a comparator, array callbacks in untyped code and smallnew Array(n).f(...list),list.push(...other),instanceofon functions below functions, and aswitchover the object that tsc, esbuild, swc or babel emit for an enum.Tests and CI
.irtests are parsed strictly. A misspelled directive is an error instead of being treated as a comment.taron Windows.lint.ymland theaot_checkbuild rule.Not AOT-specific
These replace #47304.
Launch-VsDevShell.ps1reads the environment back line by line asNAME=VALUE.BUILDKITE_MESSAGEholds the whole commit message, so any line of it containing=was parsed as a variable, and some of those names can't be set in PowerShell.scripts/vs-shell.ps1now unsets multi-line variables around that call and restores them afterwards.test/aot/build-scalingnow runs withMIMALLOC_PURGE_DELAY=0. No bound was raised.Bun.build()with many chunks allocated memory proportional to the square of the chunk count when computing chunk hashes.process.stdoutandprocess.stderrwent silent for good after an out-of-memory error during a write.{ name }) is reported at the name.Changes that also touch plain Bun
bun test --files-from <list>,--timings=<json>and--update-timings.realloc()moves a block of up to 64 KB when it shrinks into a smaller size class.mi_realloc()leaves a block where it is while it stays half full, soshrink_to_fit()andinto_boxed_slice()gave nothing back.bun test --isolate, three things kept every finished test file's realm alive: the functions thatbun:sqlhands to native code, streams that were never cancelled, and the cached Structure of aRegExp'sgroupsobject (that one is in AOT: per-global program state, mockable exports, heap-allocated generator frames, bug fixes WebKit#942).SIGBUSamong files that the compiler has mapped for reading with "a source file was made shorter while it was being compiled".posix_spawnkeeps the highestru_maxrssof the children it has reaped, for thememorymap.How did you verify your code works?
test/aotthat fails without it.