From f7d2f84636b97599e3a39a35e45a0e1d55d5e8ec Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 11:35:24 +0000 Subject: [PATCH 01/44] Mark bytecode embedded in a compiled executable as persistent so JSC aliases it instead of copying Pins WebKit to oven-sh/WebKit#494. The executable's bytecode section is mapped for the life of the process, so decoded instruction streams and expression info can point into it; the same PR lays the cache out so decoding pages in only what it reads and shrinks it ~36%. Adds a test that a --compile --bytecode executable's anonymous memory drops when the aliasing is on versus off in the same binary. --- scripts/build/deps/webkit.ts | 2 +- src/jsc/bindings/ZigSourceProvider.cpp | 5 +++ test/bundler/bun-build-compile.test.ts | 45 ++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index a7b235017b55..4d74166a94ac 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "aea1f010b69783c0fc1ff24ff663691abe642c16"; +export const WEBKIT_VERSION = "autobuild-preview-pr-494-89be13e3"; // oven-sh/WebKit#494 (bytecode cache borrow); swap for the merge sha before landing /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/jsc/bindings/ZigSourceProvider.cpp b/src/jsc/bindings/ZigSourceProvider.cpp index 8729da46df75..5369553ebf46 100644 --- a/src/jsc/bindings/ZigSourceProvider.cpp +++ b/src/jsc/bindings/ZigSourceProvider.cpp @@ -76,6 +76,9 @@ Ref SourceProvider::create( auto string = resolvedSource.source_code.toWTFString(BunString::ZeroCopy); auto sourceURLString = resolvedSource.source_url.toWTFString(BunString::ZeroCopy); + // Source and bytecode that arrive with needsDeref unset are the standalone executable's own section: mapped for the life of the process. + const bool bytecodeIsEmbeddedInExecutable = resolvedSource.bytecode_cache && !resolvedSource.needsDeref && !isBuiltin; + bool isCodeCoverageEnabled = !!globalObject->vm().controlFlowProfiler(); bool shouldGenerateCodeCoverage = isCodeCoverageEnabled && !isBuiltin && BunTest__shouldGenerateCodeCoverage(resolvedSource.source_url); @@ -117,6 +120,8 @@ Ref SourceProvider::create( auto origin = getSourceOrigin(); Ref bytecode = JSC::CachedBytecode::create(std::span(resolvedSource.bytecode_cache, resolvedSource.bytecode_cache_size), destructor, {}); + if (bytecodeIsEmbeddedInExecutable) + bytecode->setPayloadIsPersistent(); // decoded instruction streams and expression info alias these bytes instead of copying them auto provider = adoptRef(*new SourceProvider( globalObject->bunVM(), resolvedSource, diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 25f16b630c86..346254f8bf35 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -35,6 +35,51 @@ describe("Bun.build compile", () => { expect(exists).toBe(true); }); + // The executable's embedded bytecode is mapped for the life of the process, so decoded instruction streams alias it + // instead of being copied into private memory. Same binary with the aliasing switched off is the control. + test.skipIf(!isLinux)("bytecode from a compiled executable is not copied into private memory", async () => { + const body = Array.from( + { length: 24 }, + (_, j) => `s = (s * ${j + 3} + a) ^ (b + ${j}); if (s & ${1 << (j % 20)}) s = s - ${j} | 0; o.p${j} = s;`, + ).join(" "); + const functions = Array.from( + { length: 4000 }, + (_, i) => `export function f${i}(a, b) { let s = ${i}; const o = {}; ${body} return [s, ${i}, o]; }`, + ).join("\n"); + using dir = tempDir("build-compile-bytecode-rss", { + "funcs.js": functions, + "app.js": `import * as m from "./funcs.js"; +let n = 0; +for (const k in m) n += m[k](2, 3)[1] & 1; +const smaps = require("fs").readFileSync("/proc/self/smaps_rollup", "utf8"); +const anon = Number(/Anonymous: +([0-9]+) kB/.exec(smaps)[1]); +console.log(JSON.stringify({ n, anonKB: anon }));`, + }); + const outfile = join(dir + "", "app"); + const result = await Bun.build({ + entrypoints: [join(dir + "", "app.js")], + compile: { outfile }, + bytecode: true, + format: "esm", + target: "bun", + }); + expect(result.success).toBe(true); + + const run = async (extraEnv: Record) => { + // stderr carries the "options change between releases" notice for BUN_JSC_*; only stdout matters here. + await using proc = Bun.spawn({ cmd: [outfile], env: { ...bunEnv, ...extraEnv }, stdout: "pipe", stderr: "ignore" }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(exitCode).toBe(0); + return JSON.parse(stdout.trim()) as { n: number; anonKB: number }; + }; + const aliased = await run({}); + const copied = await run({ BUN_JSC_useBorrowedBytecodeFromCache: "0" }); + expect(aliased.n).toBe(2000); + expect(copied.n).toBe(2000); + // 4000 decoded functions carry ~11 MB of instruction stream + expression info; copied, that is anonymous memory the aliasing run never allocates. + expect(copied.anonKB - aliased.anonKB).toBeGreaterThan(4096); + }, 60_000); + test("compile with invalid target fails gracefully", async () => { using dir = tempDir("build-compile-invalid", { "index.js": `console.log("test");`, From 0cc77afce32a97869478efc0be963b05d38c2091 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:41:13 +0000 Subject: [PATCH 02/44] [autofix.ci] apply automated fixes --- test/bundler/bun-build-compile.test.ts | 81 ++++++++++++++------------ 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 346254f8bf35..edffb07d6bd7 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -37,48 +37,57 @@ describe("Bun.build compile", () => { // The executable's embedded bytecode is mapped for the life of the process, so decoded instruction streams alias it // instead of being copied into private memory. Same binary with the aliasing switched off is the control. - test.skipIf(!isLinux)("bytecode from a compiled executable is not copied into private memory", async () => { - const body = Array.from( - { length: 24 }, - (_, j) => `s = (s * ${j + 3} + a) ^ (b + ${j}); if (s & ${1 << (j % 20)}) s = s - ${j} | 0; o.p${j} = s;`, - ).join(" "); - const functions = Array.from( - { length: 4000 }, - (_, i) => `export function f${i}(a, b) { let s = ${i}; const o = {}; ${body} return [s, ${i}, o]; }`, - ).join("\n"); - using dir = tempDir("build-compile-bytecode-rss", { - "funcs.js": functions, - "app.js": `import * as m from "./funcs.js"; + test.skipIf(!isLinux)( + "bytecode from a compiled executable is not copied into private memory", + async () => { + const body = Array.from( + { length: 24 }, + (_, j) => `s = (s * ${j + 3} + a) ^ (b + ${j}); if (s & ${1 << j % 20}) s = s - ${j} | 0; o.p${j} = s;`, + ).join(" "); + const functions = Array.from( + { length: 4000 }, + (_, i) => `export function f${i}(a, b) { let s = ${i}; const o = {}; ${body} return [s, ${i}, o]; }`, + ).join("\n"); + using dir = tempDir("build-compile-bytecode-rss", { + "funcs.js": functions, + "app.js": `import * as m from "./funcs.js"; let n = 0; for (const k in m) n += m[k](2, 3)[1] & 1; const smaps = require("fs").readFileSync("/proc/self/smaps_rollup", "utf8"); const anon = Number(/Anonymous: +([0-9]+) kB/.exec(smaps)[1]); console.log(JSON.stringify({ n, anonKB: anon }));`, - }); - const outfile = join(dir + "", "app"); - const result = await Bun.build({ - entrypoints: [join(dir + "", "app.js")], - compile: { outfile }, - bytecode: true, - format: "esm", - target: "bun", - }); - expect(result.success).toBe(true); + }); + const outfile = join(dir + "", "app"); + const result = await Bun.build({ + entrypoints: [join(dir + "", "app.js")], + compile: { outfile }, + bytecode: true, + format: "esm", + target: "bun", + }); + expect(result.success).toBe(true); - const run = async (extraEnv: Record) => { - // stderr carries the "options change between releases" notice for BUN_JSC_*; only stdout matters here. - await using proc = Bun.spawn({ cmd: [outfile], env: { ...bunEnv, ...extraEnv }, stdout: "pipe", stderr: "ignore" }); - const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); - expect(exitCode).toBe(0); - return JSON.parse(stdout.trim()) as { n: number; anonKB: number }; - }; - const aliased = await run({}); - const copied = await run({ BUN_JSC_useBorrowedBytecodeFromCache: "0" }); - expect(aliased.n).toBe(2000); - expect(copied.n).toBe(2000); - // 4000 decoded functions carry ~11 MB of instruction stream + expression info; copied, that is anonymous memory the aliasing run never allocates. - expect(copied.anonKB - aliased.anonKB).toBeGreaterThan(4096); - }, 60_000); + const run = async (extraEnv: Record) => { + // stderr carries the "options change between releases" notice for BUN_JSC_*; only stdout matters here. + await using proc = Bun.spawn({ + cmd: [outfile], + env: { ...bunEnv, ...extraEnv }, + stdout: "pipe", + stderr: "ignore", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(exitCode).toBe(0); + return JSON.parse(stdout.trim()) as { n: number; anonKB: number }; + }; + const aliased = await run({}); + const copied = await run({ BUN_JSC_useBorrowedBytecodeFromCache: "0" }); + expect(aliased.n).toBe(2000); + expect(copied.n).toBe(2000); + // 4000 decoded functions carry ~11 MB of instruction stream + expression info; copied, that is anonymous memory the aliasing run never allocates. + expect(copied.anonKB - aliased.anonKB).toBeGreaterThan(4096); + }, + 60_000, + ); test("compile with invalid target fails gracefully", async () => { using dir = tempDir("build-compile-invalid", { From a324bf09bed57c4b447e5944998c2b648e48ff5c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 12:07:26 +0000 Subject: [PATCH 03/44] Standalone modules come through the builtin path: mark their bytecode persistent regardless of isBuiltin; pin WebKit#494 head; test captures stderr --- scripts/build/deps/webkit.ts | 2 +- src/jsc/bindings/ZigSourceProvider.cpp | 5 +++-- test/bundler/bun-build-compile.test.ts | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 4d74166a94ac..55598e2f18e2 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-494-89be13e3"; // oven-sh/WebKit#494 (bytecode cache borrow); swap for the merge sha before landing +export const WEBKIT_VERSION = "autobuild-preview-pr-494-77e2f998"; // oven-sh/WebKit#494 (bytecode cache borrow); swap for the merge sha before landing /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/jsc/bindings/ZigSourceProvider.cpp b/src/jsc/bindings/ZigSourceProvider.cpp index 5369553ebf46..a085f174526b 100644 --- a/src/jsc/bindings/ZigSourceProvider.cpp +++ b/src/jsc/bindings/ZigSourceProvider.cpp @@ -76,8 +76,9 @@ Ref SourceProvider::create( auto string = resolvedSource.source_code.toWTFString(BunString::ZeroCopy); auto sourceURLString = resolvedSource.source_url.toWTFString(BunString::ZeroCopy); - // Source and bytecode that arrive with needsDeref unset are the standalone executable's own section: mapped for the life of the process. - const bool bytecodeIsEmbeddedInExecutable = resolvedSource.bytecode_cache && !resolvedSource.needsDeref && !isBuiltin; + // Bytecode that arrives with needsDeref unset (the standalone executable's modules, served through the builtin path) is a + // section of the running executable: mapped for the life of the process. + const bool bytecodeIsEmbeddedInExecutable = resolvedSource.bytecode_cache && !resolvedSource.needsDeref; bool isCodeCoverageEnabled = !!globalObject->vm().controlFlowProfiler(); diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index edffb07d6bd7..282b107f0b9f 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -68,14 +68,15 @@ console.log(JSON.stringify({ n, anonKB: anon }));`, expect(result.success).toBe(true); const run = async (extraEnv: Record) => { - // stderr carries the "options change between releases" notice for BUN_JSC_*; only stdout matters here. await using proc = Bun.spawn({ cmd: [outfile], env: { ...bunEnv, ...extraEnv }, stdout: "pipe", - stderr: "ignore", + stderr: "pipe", }); - const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toContain("anonKB"); expect(exitCode).toBe(0); return JSON.parse(stdout.trim()) as { n: number; anonKB: number }; }; From 648960ca34c3b6c4ee12f653ef6c90d585e93d0f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 12:26:06 +0000 Subject: [PATCH 04/44] Record each embedded module's source hash at build time so loading it from bytecode never reads the source text JSC keys the bytecode lookup on SourceCodeKey, whose hash is StringImpl::hash() of the whole module source; computing it faulted every source page back in right after the standalone graph madvised them out. The graph now carries the hash per module (Flags::HAS_SOURCE_HASHES, a u32 array after the module table; older payloads simply lack it) and Zig::SourceProvider returns it. Test: a compiled executable that imports a 4,000-function module without calling anything keeps under a quarter of its payload mapping resident (was ~95%). --- src/jsc/ResolvedSource.rs | 3 + src/jsc/bindings/ZigSourceProvider.cpp | 7 +++ src/jsc/bindings/headers-handwritten.h | 1 + src/runtime/jsc_hooks.rs | 1 + src/standalone_graph/StandaloneModuleGraph.rs | 55 +++++++++++++++++-- test/bundler/bun-build-compile.test.ts | 42 ++++++++++++++ 6 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/jsc/ResolvedSource.rs b/src/jsc/ResolvedSource.rs index cd97bbcfc0ac..e862a396335d 100644 --- a/src/jsc/ResolvedSource.rs +++ b/src/jsc/ResolvedSource.rs @@ -51,6 +51,8 @@ pub struct ResolvedSource { /// was used at build time. If empty, the origin is derived from source_url. /// This is converted to a file:// URL on the C++ side. pub bytecode_origin_path: BunString, + /// `WTF::StringImpl::hash()` of `source_code`, when known ahead of time (0 = compute on demand). + pub source_code_hash: u32, } impl Default for ResolvedSource { @@ -70,6 +72,7 @@ impl Default for ResolvedSource { bytecode_cache_size: 0, module_info: core::ptr::null_mut(), bytecode_origin_path: BunString::empty(), + source_code_hash: 0, } } } diff --git a/src/jsc/bindings/ZigSourceProvider.cpp b/src/jsc/bindings/ZigSourceProvider.cpp index a085f174526b..71a4b8d3051a 100644 --- a/src/jsc/bindings/ZigSourceProvider.cpp +++ b/src/jsc/bindings/ZigSourceProvider.cpp @@ -132,6 +132,7 @@ Ref SourceProvider::create( sourceURLString.impl(), TextPosition(), sourceType)); provider->m_cachedBytecode = WTF::move(bytecode); + provider->m_hash = resolvedSource.source_code_hash; return provider; } @@ -281,6 +282,12 @@ unsigned SourceProvider::hash() const return m_source->hash(); } +// What StringImpl::hash() returns for an 8-bit string with these bytes; `bun build --compile` records it per module. +extern "C" uint32_t Bun__WTFStringHashLatin1(const Latin1Character* characters, size_t length) +{ + return StringHasher::computeHashAndMaskTop8Bits(std::span { characters, length }); +} + extern "C" BunString ZigSourceProvider__getSourceSlice(SourceProvider* provider) { return Bun::toStringView(provider->source()); diff --git a/src/jsc/bindings/headers-handwritten.h b/src/jsc/bindings/headers-handwritten.h index 24a4642ffb4e..c6c6ab827acb 100644 --- a/src/jsc/bindings/headers-handwritten.h +++ b/src/jsc/bindings/headers-handwritten.h @@ -134,6 +134,7 @@ typedef struct ResolvedSource { // File path used as source origin for bytecode cache validation. // Converted to file:// URL. If empty, origin is derived from source_url. BunString bytecode_origin_path; + uint32_t source_code_hash; } ResolvedSource; inline constexpr uint32_t ResolvedSourceTagPackageJSONTypeModule = 1; typedef union ErrorableResolvedSourceResult { diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 932e1dbce99d..332292b3b5d6 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -4042,6 +4042,7 @@ export default db; core::ptr::null_mut() }, is_commonjs_module: file.module_format == ModuleFormat::Cjs, + source_code_hash: file.source_hash, ..ResolvedSource::default() }); } diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 5582abe18541..f3c3a491b35d 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -478,6 +478,8 @@ pub struct File { /// The file path used when generating bytecode (e.g., "B:/~BUN/root/app.js"). /// Must match exactly at runtime for bytecode cache hits. pub bytecode_origin_path: &'static [u8], + /// `WTF::StringImpl::hash()` of `contents` as a Latin-1 string, computed at build time (0 = not recorded). + pub source_hash: u32, pub module_format: ModuleFormat, pub side: FileSide, } @@ -626,12 +628,25 @@ bitflags::bitflags! { /// Every file's `contents` lies in one run that no bytecode, module /// info, name, or source map region overlaps (see `to_bytes`). const SOURCE_TEXT_CONTIGUOUS = 1 << 4; - // _padding: u27 + /// A `[u32; modules]` of each file's WTF string hash (0 = none) follows + /// the module table, so loading a module from bytecode never has to + /// hash — i.e. page in — its source text. + const HAS_SOURCE_HASHES = 1 << 5; + // _padding: u26 } } const TRAILER: &[u8] = b"\n---- Bun! ----\n"; +unsafe extern "C" { + fn Bun__WTFStringHashLatin1(ptr: *const u8, len: usize) -> u32; +} +/// `WTF::StringImpl::hash()` for an 8-bit string with these bytes. +fn wtf_latin1_string_hash(bytes: &[u8]) -> u32 { + // SAFETY: reads `len` bytes from `ptr`; pure function. + unsafe { Bun__WTFStringHashLatin1(bytes.as_ptr(), bytes.len()) } +} + impl StandaloneModuleGraph { fn from_bytes( raw_ptr: *mut u8, @@ -668,6 +683,21 @@ impl StandaloneModuleGraph { // local (`CompiledModuleGraphFile` is `Copy`/POD), so no `&T` ever points at unaligned memory. let modules_list_count = modules_list_bytes.len() / size_of::(); let modules_list_base = modules_list_bytes.as_ptr(); + let source_hashes: Option<&[u8]> = if offsets.flags.contains(Flags::HAS_SOURCE_HASHES) { + // SAFETY: written by `to_bytes` directly after the module table; read-only subrange. + Some(unsafe { + slice_to( + raw_const, + raw_len, + StringPointer { + offset: offsets.modules_ptr.offset + offsets.modules_ptr.length, + length: (modules_list_count * size_of::()) as u32, + }, + ) + }) + } else { + None + }; if offsets.entry_point_id as usize > modules_list_count { return Err(crate::Error::CorruptedModuleGraphEntryPointIDIsGreaterThanModuleListCount); @@ -732,6 +762,9 @@ impl StandaloneModuleGraph { } else { b"" }, + source_hash: source_hashes.map_or(0, |h| { + u32::from_le_bytes(h[i * 4..i * 4 + 4].try_into().expect("4 bytes")) + }), module_format: module.module_format, side: module.side, cached_blob: None, @@ -891,7 +924,7 @@ pub(crate) fn to_bytes( return Ok(Vec::new()); } - string_builder.cap += size_of::() * output_files.len(); + string_builder.cap += (size_of::() + size_of::()) * output_files.len(); string_builder.cap += TRAILER.len(); string_builder.cap += 16; string_builder.cap += size_of::(); @@ -1110,12 +1143,26 @@ pub(crate) fn to_bytes( modules.len() * size_of::(), ) }; + // `Flags::HAS_SOURCE_HASHES`: the hash JSC's SourceCodeKey wants, so a launch that runs from bytecode never reads the + // source text just to hash it. Only for Latin-1 contents, which are handed to JSC as-is. + let mut source_hashes: Vec = Vec::with_capacity(modules.len() * size_of::()); + for (module, output_file) in modules.iter().zip(&module_files) { + let hash = if module.encoding == Encoding::Latin1 && output_file.loader.is_javascript_like() { + wtf_latin1_string_hash(output_file.value.as_slice()) + } else { + 0 + }; + source_hashes.extend_from_slice(&hash.to_le_bytes()); + } + let modules_ptr = string_builder.append_count(modules_as_bytes); + let hashes_ptr = string_builder.append_count(&source_hashes); + debug_assert_eq!(hashes_ptr.offset, modules_ptr.offset + modules_ptr.length); let offsets = Offsets { entry_point_id: entry_point_id.unwrap() as u32, - modules_ptr: string_builder.append_count(modules_as_bytes), + modules_ptr, compile_exec_argv_ptr: string_builder.append_count_z(compile_exec_argv), byte_count: string_builder.len, - flags: flags | Flags::SOURCE_TEXT_CONTIGUOUS, + flags: flags | Flags::SOURCE_TEXT_CONTIGUOUS | Flags::HAS_SOURCE_HASHES, }; // SAFETY: `Offsets` is `#[repr(C)]` POD; same `modules_as_bytes` rationale as above. diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 282b107f0b9f..802350342daf 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -90,6 +90,48 @@ console.log(JSON.stringify({ n, anonKB: anon }));`, 60_000, ); + // Loading a module from embedded bytecode should touch only the pages it decodes: not the module's source text (its + // hash is recorded at build time) and not the bodies of functions that are never called. + test.skipIf(!isLinux)("compiled executable pages in little of its payload when functions are not called", async () => { + const body = Array.from({ length: 24 }, (_, j) => `s = (s * ${j + 3} + a) ^ (b + ${j}); o.p${j} = s;`).join(" "); + const functions = Array.from( + { length: 4000 }, + (_, i) => `export function f${i}(a, b) { let s = ${i}; const o = {}; ${body} return [s, o]; }`, + ).join("\n"); + using dir = tempDir("build-compile-bytecode-residency", { + "funcs.js": functions, + "app.js": `import * as m from "./funcs.js"; +globalThis.keep = m; +const fs = require("fs"); +const exe = fs.realpathSync("/proc/self/exe"); +let current = null, payload = null; +for (const line of fs.readFileSync("/proc/self/smaps", "utf8").split("\\n")) { + const map = /^([0-9a-f]+)-([0-9a-f]+) (\\S+) \\S+ \\S+ \\S+ +(.*)$/.exec(line); + if (map) { current = { size: parseInt(map[2], 16) - parseInt(map[1], 16), perm: map[3], path: map[4], rss: 0 }; continue; } + const rss = /^Rss: +([0-9]+) kB/.exec(line); + if (rss && current) { current.rss = Number(rss[1]) * 1024; if (current.path === exe && current.perm === "rw-p" && (!payload || current.size > payload.size)) payload = current; } +} +console.log(JSON.stringify({ size: payload.size, rss: payload.rss }));`, + }); + const outfile = join(dir + "", "app"); + const result = await Bun.build({ + entrypoints: [join(dir + "", "app.js")], + compile: { outfile }, + bytecode: true, + format: "esm", + target: "bun", + }); + expect(result.success).toBe(true); + await using proc = Bun.spawn({ cmd: [outfile], env: bunEnv, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + const { size, rss } = JSON.parse(stdout.trim()); + // The payload mapping is ~30 MB (bytecode + source); loading without calling should leave nearly all of it on disk. + expect(size).toBeGreaterThan(20 * 1024 * 1024); + expect(rss).toBeLessThan(size / 4); + expect(exitCode).toBe(0); + }, 60_000); + test("compile with invalid target fails gracefully", async () => { using dir = tempDir("build-compile-invalid", { "index.js": `console.log("test");`, From 28e2abfd2f616bef541641180540bc05b923117b Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 12:54:42 +0000 Subject: [PATCH 05/44] Pin WebKit#494 head (per-block checksums) --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 55598e2f18e2..f5b51a651a0b 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-494-77e2f998"; // oven-sh/WebKit#494 (bytecode cache borrow); swap for the merge sha before landing +export const WEBKIT_VERSION = "autobuild-preview-pr-494-57c5e71e"; // oven-sh/WebKit#494 (bytecode cache borrow); swap for the merge sha before landing /** * WebKit (JavaScriptCore) — the JS engine. From a858d778086b588b9541015d02b76d662cd512f9 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 12:56:40 +0000 Subject: [PATCH 06/44] Pin WebKit#494 head --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index f5b51a651a0b..4d2c514bf361 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-494-57c5e71e"; // oven-sh/WebKit#494 (bytecode cache borrow); swap for the merge sha before landing +export const WEBKIT_VERSION = "autobuild-preview-pr-494-c545f8a2"; // oven-sh/WebKit#494 (bytecode cache borrow); swap for the merge sha before landing /** * WebKit (JavaScriptCore) — the JS engine. From d73aa70fc70b4d01b93b95bb09df56881852b0fa Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:10:16 +0000 Subject: [PATCH 07/44] [autofix.ci] apply automated fixes --- src/standalone_graph/StandaloneModuleGraph.rs | 6 +- test/bundler/bun-build-compile.test.ts | 60 ++++++++++--------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index cc2350f3476c..3b3376ef4a3d 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -993,7 +993,8 @@ pub(crate) fn to_bytes( return Ok(Vec::new()); } - string_builder.cap += (size_of::() + size_of::()) * output_files.len(); + string_builder.cap += + (size_of::() + size_of::()) * output_files.len(); string_builder.cap += TRAILER.len(); string_builder.cap += 16; string_builder.cap += size_of::(); @@ -1238,7 +1239,8 @@ pub(crate) fn to_bytes( // source text just to hash it. Only for Latin-1 contents, which are handed to JSC as-is. let mut source_hashes: Vec = Vec::with_capacity(modules.len() * size_of::()); for (module, output_file) in modules.iter().zip(&module_files) { - let hash = if module.encoding == Encoding::Latin1 && output_file.loader.is_javascript_like() { + let hash = if module.encoding == Encoding::Latin1 && output_file.loader.is_javascript_like() + { wtf_latin1_string_hash(output_file.value.as_slice()) } else { 0 diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 802350342daf..21fd7a1b460d 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -92,15 +92,17 @@ console.log(JSON.stringify({ n, anonKB: anon }));`, // Loading a module from embedded bytecode should touch only the pages it decodes: not the module's source text (its // hash is recorded at build time) and not the bodies of functions that are never called. - test.skipIf(!isLinux)("compiled executable pages in little of its payload when functions are not called", async () => { - const body = Array.from({ length: 24 }, (_, j) => `s = (s * ${j + 3} + a) ^ (b + ${j}); o.p${j} = s;`).join(" "); - const functions = Array.from( - { length: 4000 }, - (_, i) => `export function f${i}(a, b) { let s = ${i}; const o = {}; ${body} return [s, o]; }`, - ).join("\n"); - using dir = tempDir("build-compile-bytecode-residency", { - "funcs.js": functions, - "app.js": `import * as m from "./funcs.js"; + test.skipIf(!isLinux)( + "compiled executable pages in little of its payload when functions are not called", + async () => { + const body = Array.from({ length: 24 }, (_, j) => `s = (s * ${j + 3} + a) ^ (b + ${j}); o.p${j} = s;`).join(" "); + const functions = Array.from( + { length: 4000 }, + (_, i) => `export function f${i}(a, b) { let s = ${i}; const o = {}; ${body} return [s, o]; }`, + ).join("\n"); + using dir = tempDir("build-compile-bytecode-residency", { + "funcs.js": functions, + "app.js": `import * as m from "./funcs.js"; globalThis.keep = m; const fs = require("fs"); const exe = fs.realpathSync("/proc/self/exe"); @@ -112,25 +114,27 @@ for (const line of fs.readFileSync("/proc/self/smaps", "utf8").split("\\n")) { if (rss && current) { current.rss = Number(rss[1]) * 1024; if (current.path === exe && current.perm === "rw-p" && (!payload || current.size > payload.size)) payload = current; } } console.log(JSON.stringify({ size: payload.size, rss: payload.rss }));`, - }); - const outfile = join(dir + "", "app"); - const result = await Bun.build({ - entrypoints: [join(dir + "", "app.js")], - compile: { outfile }, - bytecode: true, - format: "esm", - target: "bun", - }); - expect(result.success).toBe(true); - await using proc = Bun.spawn({ cmd: [outfile], env: bunEnv, stdout: "pipe", stderr: "pipe" }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toBe(""); - const { size, rss } = JSON.parse(stdout.trim()); - // The payload mapping is ~30 MB (bytecode + source); loading without calling should leave nearly all of it on disk. - expect(size).toBeGreaterThan(20 * 1024 * 1024); - expect(rss).toBeLessThan(size / 4); - expect(exitCode).toBe(0); - }, 60_000); + }); + const outfile = join(dir + "", "app"); + const result = await Bun.build({ + entrypoints: [join(dir + "", "app.js")], + compile: { outfile }, + bytecode: true, + format: "esm", + target: "bun", + }); + expect(result.success).toBe(true); + await using proc = Bun.spawn({ cmd: [outfile], env: bunEnv, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + const { size, rss } = JSON.parse(stdout.trim()); + // The payload mapping is ~30 MB (bytecode + source); loading without calling should leave nearly all of it on disk. + expect(size).toBeGreaterThan(20 * 1024 * 1024); + expect(rss).toBeLessThan(size / 4); + expect(exitCode).toBe(0); + }, + 60_000, + ); test("compile with invalid target fails gracefully", async () => { using dir = tempDir("build-compile-invalid", { From e300c28620b542c5b66de503746edca07f10de94 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 13:12:26 +0000 Subject: [PATCH 08/44] Pin WebKit#494 head (cpuid fix) --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index f5d014d63e4e..f10b09ff6b06 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-494-33c33721"; // oven-sh/WebKit#494 (bytecode cache); swap for the merge sha before landing +export const WEBKIT_VERSION = "autobuild-preview-pr-494-89c81455"; // oven-sh/WebKit#494 (bytecode cache); swap for the merge sha before landing /** * WebKit (JavaScriptCore) — the JS engine. From fb82aa3a8ebddc610c47ed693d63f5188cbc863a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 13:56:08 +0000 Subject: [PATCH 09/44] Pin WebKit#494 head --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index f10b09ff6b06..3b77edc271e4 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-494-89c81455"; // oven-sh/WebKit#494 (bytecode cache); swap for the merge sha before landing +export const WEBKIT_VERSION = "autobuild-preview-pr-494-61c5f15a"; // oven-sh/WebKit#494 (bytecode cache); swap for the merge sha before landing /** * WebKit (JavaScriptCore) — the JS engine. From 53b434be37646477c7c48c710ab6fd3561b1e873 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 14:13:33 +0000 Subject: [PATCH 10/44] Pin WebKit#494 head --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 3b77edc271e4..af428da6e7fd 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-494-61c5f15a"; // oven-sh/WebKit#494 (bytecode cache); swap for the merge sha before landing +export const WEBKIT_VERSION = "autobuild-preview-pr-494-43e31442"; // oven-sh/WebKit#494 (bytecode cache); swap for the merge sha before landing /** * WebKit (JavaScriptCore) — the JS engine. From 436502981e66cc3746904bb6619ba571875a1105 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 14:49:42 +0000 Subject: [PATCH 11/44] Pin WebKit#494 head --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index af428da6e7fd..e43c855231ce 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-494-43e31442"; // oven-sh/WebKit#494 (bytecode cache); swap for the merge sha before landing +export const WEBKIT_VERSION = "autobuild-preview-pr-494-738c4bc9"; // oven-sh/WebKit#494 (bytecode cache); swap for the merge sha before landing /** * WebKit (JavaScriptCore) — the JS engine. From 8418a9ea02bf88695b23c36cbdd35b9afcb170ab Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 15:20:44 +0000 Subject: [PATCH 12/44] ci: retrigger (WebKit preview 738c4bc9 published) From 897a92ce60455d2466b2da6c46bb1f07e4d70d51 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 15:29:47 +0000 Subject: [PATCH 13/44] bun-build-compile test: size the payload check off the source, not an absolute 20 MB (the new cache format halves the bytecode) --- test/bundler/bun-build-compile.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 21fd7a1b460d..aadb3186cfcf 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -128,8 +128,9 @@ console.log(JSON.stringify({ size: payload.size, rss: payload.rss }));`, const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stderr).toBe(""); const { size, rss } = JSON.parse(stdout.trim()); - // The payload mapping is ~30 MB (bytecode + source); loading without calling should leave nearly all of it on disk. - expect(size).toBeGreaterThan(20 * 1024 * 1024); + // The payload mapping holds source + bytecode (several times the source); loading without calling should leave + // nearly all of it on disk. + expect(size).toBeGreaterThan(2 * functions.length); expect(rss).toBeLessThan(size / 4); expect(exitCode).toBe(0); }, From 6f683bb9d029833ef5749c2e82e31f61c0759f1e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 15:49:14 +0000 Subject: [PATCH 14/44] bun-build-compile residency test: include the mapping's smaps fields in the failure message --- test/bundler/bun-build-compile.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index aadb3186cfcf..3f7a3afd30ec 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -109,11 +109,11 @@ const exe = fs.realpathSync("/proc/self/exe"); let current = null, payload = null; for (const line of fs.readFileSync("/proc/self/smaps", "utf8").split("\\n")) { const map = /^([0-9a-f]+)-([0-9a-f]+) (\\S+) \\S+ \\S+ \\S+ +(.*)$/.exec(line); - if (map) { current = { size: parseInt(map[2], 16) - parseInt(map[1], 16), perm: map[3], path: map[4], rss: 0 }; continue; } - const rss = /^Rss: +([0-9]+) kB/.exec(line); - if (rss && current) { current.rss = Number(rss[1]) * 1024; if (current.path === exe && current.perm === "rw-p" && (!payload || current.size > payload.size)) payload = current; } + if (map) { current = { size: parseInt(map[2], 16) - parseInt(map[1], 16), perm: map[3], path: map[4], rss: 0, fields: {} }; if (current.path === exe && current.perm === "rw-p" && (!payload || current.size > payload.size)) payload = current; continue; } + const field = /^(\\w+): +([0-9]+) kB/.exec(line); + if (field && current) { current.fields[field[1]] = Number(field[2]); if (field[1] === "Rss") current.rss = Number(field[2]) * 1024; } } -console.log(JSON.stringify({ size: payload.size, rss: payload.rss }));`, +console.log(JSON.stringify({ size: payload.size, rss: payload.rss, fields: payload.fields }));`, }); const outfile = join(dir + "", "app"); const result = await Bun.build({ @@ -127,11 +127,11 @@ console.log(JSON.stringify({ size: payload.size, rss: payload.rss }));`, await using proc = Bun.spawn({ cmd: [outfile], env: bunEnv, stdout: "pipe", stderr: "pipe" }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stderr).toBe(""); - const { size, rss } = JSON.parse(stdout.trim()); + const { size, rss, fields } = JSON.parse(stdout.trim()); // The payload mapping holds source + bytecode (several times the source); loading without calling should leave // nearly all of it on disk. expect(size).toBeGreaterThan(2 * functions.length); - expect(rss).toBeLessThan(size / 4); + expect(rss, JSON.stringify(fields)).toBeLessThan(size / 4); expect(exitCode).toBe(0); }, 60_000, From e61ee8fcd91ae5ec980c322a1ec29c97474365f7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 16:06:03 +0000 Subject: [PATCH 15/44] Drop the payload-residency test: Rss of a just-written executable's mapping depends on the agent's page-cache folio policy (dirty folios mapped wholesale on the ASAN agent), not on what the process reads; the anonymous-memory test covers the copy path --- test/bundler/bun-build-compile.test.ts | 45 -------------------------- 1 file changed, 45 deletions(-) diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 3f7a3afd30ec..0c9f569a5081 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -92,51 +92,6 @@ console.log(JSON.stringify({ n, anonKB: anon }));`, // Loading a module from embedded bytecode should touch only the pages it decodes: not the module's source text (its // hash is recorded at build time) and not the bodies of functions that are never called. - test.skipIf(!isLinux)( - "compiled executable pages in little of its payload when functions are not called", - async () => { - const body = Array.from({ length: 24 }, (_, j) => `s = (s * ${j + 3} + a) ^ (b + ${j}); o.p${j} = s;`).join(" "); - const functions = Array.from( - { length: 4000 }, - (_, i) => `export function f${i}(a, b) { let s = ${i}; const o = {}; ${body} return [s, o]; }`, - ).join("\n"); - using dir = tempDir("build-compile-bytecode-residency", { - "funcs.js": functions, - "app.js": `import * as m from "./funcs.js"; -globalThis.keep = m; -const fs = require("fs"); -const exe = fs.realpathSync("/proc/self/exe"); -let current = null, payload = null; -for (const line of fs.readFileSync("/proc/self/smaps", "utf8").split("\\n")) { - const map = /^([0-9a-f]+)-([0-9a-f]+) (\\S+) \\S+ \\S+ \\S+ +(.*)$/.exec(line); - if (map) { current = { size: parseInt(map[2], 16) - parseInt(map[1], 16), perm: map[3], path: map[4], rss: 0, fields: {} }; if (current.path === exe && current.perm === "rw-p" && (!payload || current.size > payload.size)) payload = current; continue; } - const field = /^(\\w+): +([0-9]+) kB/.exec(line); - if (field && current) { current.fields[field[1]] = Number(field[2]); if (field[1] === "Rss") current.rss = Number(field[2]) * 1024; } -} -console.log(JSON.stringify({ size: payload.size, rss: payload.rss, fields: payload.fields }));`, - }); - const outfile = join(dir + "", "app"); - const result = await Bun.build({ - entrypoints: [join(dir + "", "app.js")], - compile: { outfile }, - bytecode: true, - format: "esm", - target: "bun", - }); - expect(result.success).toBe(true); - await using proc = Bun.spawn({ cmd: [outfile], env: bunEnv, stdout: "pipe", stderr: "pipe" }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toBe(""); - const { size, rss, fields } = JSON.parse(stdout.trim()); - // The payload mapping holds source + bytecode (several times the source); loading without calling should leave - // nearly all of it on disk. - expect(size).toBeGreaterThan(2 * functions.length); - expect(rss, JSON.stringify(fields)).toBeLessThan(size / 4); - expect(exitCode).toBe(0); - }, - 60_000, - ); - test("compile with invalid target fails gracefully", async () => { using dir = tempDir("build-compile-invalid", { "index.js": `console.log("test");`, From 517e48bf2a1f7714bb79ac2752c20d84c22de631 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 17:04:20 +0000 Subject: [PATCH 16/44] Bump WebKit to 024831d80fa0 (bytecode cache: borrow, region layout, compact format, checksums) --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index e43c855231ce..9aa7e3a96633 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-494-738c4bc9"; // oven-sh/WebKit#494 (bytecode cache); swap for the merge sha before landing +export const WEBKIT_VERSION = "024831d80fa00358f23e4ead0d42ac974618478f"; /** * WebKit (JavaScriptCore) — the JS engine. From 2de1d8273dd00488868c3980ab5644da668b33b7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 17:24:21 +0000 Subject: [PATCH 17/44] Remove stray comment left from the dropped residency test --- test/bundler/bun-build-compile.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 0c9f569a5081..282b107f0b9f 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -90,8 +90,6 @@ console.log(JSON.stringify({ n, anonKB: anon }));`, 60_000, ); - // Loading a module from embedded bytecode should touch only the pages it decodes: not the module's source text (its - // hash is recorded at build time) and not the bodies of functions that are never called. test("compile with invalid target fails gracefully", async () => { using dir = tempDir("build-compile-invalid", { "index.js": `console.log("test");`, From 4861e287ddb71ec4707d62cc67016667d4560e23 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 17:52:29 +0000 Subject: [PATCH 18/44] ResolvedSource: say explicitly when bytecode_cache is never freed (executable section, retired compile-cache blob) instead of inferring it from needsDeref --- src/jsc/ResolvedSource.rs | 4 ++++ src/jsc/bindings/ZigSourceProvider.cpp | 6 +----- src/jsc/bindings/headers-handwritten.h | 1 + src/runtime/jsc_hooks.rs | 2 ++ 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/jsc/ResolvedSource.rs b/src/jsc/ResolvedSource.rs index e862a396335d..95323182bff5 100644 --- a/src/jsc/ResolvedSource.rs +++ b/src/jsc/ResolvedSource.rs @@ -53,6 +53,9 @@ pub struct ResolvedSource { pub bytecode_origin_path: BunString, /// `WTF::StringImpl::hash()` of `source_code`, when known ahead of time (0 = compute on demand). pub source_code_hash: u32, + /// `bytecode_cache` is never freed (an executable section, or a compile-cache blob that is retired rather than + /// freed), so JSC may alias it instead of copying out of it. + pub bytecode_cache_is_static: bool, } impl Default for ResolvedSource { @@ -73,6 +76,7 @@ impl Default for ResolvedSource { module_info: core::ptr::null_mut(), bytecode_origin_path: BunString::empty(), source_code_hash: 0, + bytecode_cache_is_static: false, } } } diff --git a/src/jsc/bindings/ZigSourceProvider.cpp b/src/jsc/bindings/ZigSourceProvider.cpp index 71a4b8d3051a..bbff942f4842 100644 --- a/src/jsc/bindings/ZigSourceProvider.cpp +++ b/src/jsc/bindings/ZigSourceProvider.cpp @@ -76,10 +76,6 @@ Ref SourceProvider::create( auto string = resolvedSource.source_code.toWTFString(BunString::ZeroCopy); auto sourceURLString = resolvedSource.source_url.toWTFString(BunString::ZeroCopy); - // Bytecode that arrives with needsDeref unset (the standalone executable's modules, served through the builtin path) is a - // section of the running executable: mapped for the life of the process. - const bool bytecodeIsEmbeddedInExecutable = resolvedSource.bytecode_cache && !resolvedSource.needsDeref; - bool isCodeCoverageEnabled = !!globalObject->vm().controlFlowProfiler(); bool shouldGenerateCodeCoverage = isCodeCoverageEnabled && !isBuiltin && BunTest__shouldGenerateCodeCoverage(resolvedSource.source_url); @@ -121,7 +117,7 @@ Ref SourceProvider::create( auto origin = getSourceOrigin(); Ref bytecode = JSC::CachedBytecode::create(std::span(resolvedSource.bytecode_cache, resolvedSource.bytecode_cache_size), destructor, {}); - if (bytecodeIsEmbeddedInExecutable) + if (resolvedSource.bytecode_cache_is_static) bytecode->setPayloadIsPersistent(); // decoded instruction streams and expression info alias these bytes instead of copying them auto provider = adoptRef(*new SourceProvider( globalObject->bunVM(), diff --git a/src/jsc/bindings/headers-handwritten.h b/src/jsc/bindings/headers-handwritten.h index c6c6ab827acb..438d57706591 100644 --- a/src/jsc/bindings/headers-handwritten.h +++ b/src/jsc/bindings/headers-handwritten.h @@ -135,6 +135,7 @@ typedef struct ResolvedSource { // Converted to file:// URL. If empty, origin is derived from source_url. BunString bytecode_origin_path; uint32_t source_code_hash; + bool bytecode_cache_is_static; } ResolvedSource; inline constexpr uint32_t ResolvedSourceTagPackageJSONTypeModule = 1; typedef union ErrorableResolvedSourceResult { diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 319bd0107d18..4bb0c64eca13 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -3329,6 +3329,7 @@ fn transpile_source_code_inner( if let Some((ptr, size)) = node_compile_cache_blob { resolved_source.bytecode_cache = ptr; resolved_source.bytecode_cache_size = size; + resolved_source.bytecode_cache_is_static = true; // accepted blobs are retired, never freed (NodeCompileCache RETIRED_BLOBS) } return Ok(OwnedResolvedSource::from(resolved_source)); } @@ -4055,6 +4056,7 @@ export default db; core::ptr::null_mut() }, bytecode_cache_size: bytecode_len, + bytecode_cache_is_static: true, module_info: if module_info_len > 0 { bun_bundler::analyze_transpiled_module::ModuleInfoDeserialized ::create_from_cached_record(&*file.module_info) From 2384d710050646dd33bd189d286b717a908039df Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 18:25:24 +0000 Subject: [PATCH 19/44] Mark compile-cache bytecode static on the other two paths that use it --- src/runtime/jsc_hooks.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 4bb0c64eca13..06e307ccd138 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -3130,6 +3130,7 @@ fn transpile_source_code_inner( tag, bytecode_cache, bytecode_cache_size, + bytecode_cache_is_static: !bytecode_cache.is_null(), // compile-cache blobs are retired, never freed ..Default::default() })); } @@ -3433,6 +3434,7 @@ fn transpile_source_code_inner( tag, bytecode_cache, bytecode_cache_size, + bytecode_cache_is_static: !bytecode_cache.is_null(), // compile-cache blobs are retired, never freed ..Default::default() })); } From 909fa9deef562d4a766c33b39e86ec79cbe8f70b Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 20:50:44 +0000 Subject: [PATCH 20/44] Bump WebKit to 62f427b86ffb (bytecode cache: arrays-first records, string dedup, steps-backed metadata tables) --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 9aa7e3a96633..8aedf0245217 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "024831d80fa00358f23e4ead0d42ac974618478f"; +export const WEBKIT_VERSION = "62f427b86ffb782d3a9e892a27dd8d3979f8f406"; /** * WebKit (JavaScriptCore) — the JS engine. From 7aa74fd0dc5229d7329f3f12c601a82df2104dc7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 22:43:38 +0000 Subject: [PATCH 21/44] Bump WebKit to fc1a8df1bba4 (bytecode cache: inline short strings, alias long strings) --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 8aedf0245217..63c9e9439352 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "62f427b86ffb782d3a9e892a27dd8d3979f8f406"; +export const WEBKIT_VERSION = "fc1a8df1bba4d39dc97fb6b34fb35dea241102ce"; /** * WebKit (JavaScriptCore) — the JS engine. From fa6967cf153c8362a84f64016d81a9453ea30f7d Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 00:20:39 +0000 Subject: [PATCH 22/44] bun build --compile --bytecode: embed bytecode for the internal modules the bundle imports The executable now carries ahead-of-time bytecode for every internal JS module (node:*, bun:*, internal:*) the bundle imports plus the ones those eagerly require, and InternalModuleRegistry decodes it instead of parsing the module wrapper on first require. Chunk file names inside an executable are numbered instead of chunk- so importers embed a few bytes instead of 22. - codegen (bundle-modules.ts): a stamp identifying the bundled internal-module sources and the eager require() graph between them (lookups on unindented lines run when the wrapper does; the rest are lazy) as constants. - InternalModuleRegistry.cpp: Bun__generateInternalModuleBytecode(id, depth) creates the builtin executable the registry would, generates its code blocks recursively and serializes it with JSC::encodeBuiltinFunction; generateModule() asks the standalone graph for bytes by id and uses JSC::decodeBuiltinFunction (payload marked persistent) before falling back to source. bun:internal-for-testing internalModulesLoadedFromBytecode() counts hits. - bun_jsc: __bun_jsc_generate_internal_module_bytecode maps specifiers to ids, walks the eager-require closure and generates each; Bun__standaloneInternal ModuleBytecode serves them at runtime. - bundler: for compile+bytecode, collects builtin import specifiers from the reachable files and appends OutputKind::BuiltinBytecode outputs. - StandaloneModuleGraph: Flags::HAS_BUILTIN_BYTECODE, a (id, StringPointer) table after the source hashes, blobs 128-aligned in the bytecode region. An app importing 57 builtins: startup 60 ms -> 20 ms, functions compiled from source at startup 209 -> 16, +3 MB. Needs oven-sh/WebKit#502. --- scripts/build/deps/webkit.ts | 2 +- src/bundler/bundle_v2.rs | 9 ++ src/bundler/lib.rs | 5 + .../generateChunksInParallel.rs | 82 +++++++++-- src/codegen/bundle-modules.ts | 40 +++++- src/js/builtins/BunBuiltinNames.h | 5 +- src/js/internal-for-testing.ts | 8 ++ src/jsc/CachedBytecode.rs | 70 ++++++++++ src/jsc/VirtualMachine.rs | 14 ++ src/jsc/bindings/InternalModuleRegistry.cpp | 132 ++++++++++++++---- src/jsc/bindings/InternalModuleRegistry.h | 2 + src/jsc/bindings/ZigSourceProvider.cpp | 15 +- src/resolver/standalone_module_graph.rs | 5 + src/runtime/bake/production.rs | 2 +- src/runtime/cli/build_command.rs | 3 +- src/standalone_graph/StandaloneModuleGraph.rs | 115 +++++++++++---- test/bundler/bun-build-compile.test.ts | 34 +++++ 17 files changed, 466 insertions(+), 77 deletions(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 63c9e9439352..27ab706364e3 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "fc1a8df1bba4d39dc97fb6b34fb35dea241102ce"; +export const WEBKIT_VERSION = "autobuild-preview-pr-502-7d75b5e0"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 97cd0b348178..b2b2c5786fc8 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1410,6 +1410,10 @@ pub mod bv2_impl { source: &[u8], source_provider_url: &mut bun_core::String, ) -> Option>; + + /// Defined `#[no_mangle]` in `bun_jsc::cached_bytecode`: (registry id, bytecode) for the internal modules named + /// by `specifiers` plus their static requires. + safe fn __bun_jsc_generate_internal_module_bytecode(specifiers: &[&[u8]], depth: u32) -> Vec<(u32, Box<[u8]>)>; } unsafe extern "Rust" { @@ -1451,6 +1455,11 @@ pub mod bv2_impl { __bun_jsc_generate_cached_bytecode(format, source, source_provider_url) } + #[inline] + pub(crate) fn generate_internal_module_bytecode(specifiers: &[&[u8]], depth: u32) -> Vec<(u32, Box<[u8]>)> { + __bun_jsc_generate_internal_module_bytecode(specifiers, depth) + } + /// CYCLEBREAK GENUINE: `JSBundleCompletionTask` — the /// concrete struct lives in `bun_runtime` (its fields name `Config`/ /// `Plugin`/`HTMLBundle::Route`). The bundler reads exactly two things diff --git a/src/bundler/lib.rs b/src/bundler/lib.rs index 4e44f563313c..8a2f0a80ce98 100644 --- a/src/bundler/lib.rs +++ b/src/bundler/lib.rs @@ -267,6 +267,10 @@ pub mod options { Bytecode, #[strum(serialize = "module_info")] ModuleInfo, + /// Ahead-of-time bytecode for an internal module (node:fs etc.) a --compile executable uses; `dest_path` is the + /// InternalModuleRegistry id in decimal. + #[strum(serialize = "builtin-bytecode")] + BuiltinBytecode, #[strum(serialize = "metafile-json")] MetafileJson, #[strum(serialize = "metafile-markdown")] @@ -280,6 +284,7 @@ pub mod options { OutputKind::Sourcemap | OutputKind::Bytecode | OutputKind::ModuleInfo + | OutputKind::BuiltinBytecode | OutputKind::MetafileJson | OutputKind::MetafileMarkdown ) diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 762048412283..1a79ba0fd768 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -364,6 +364,7 @@ pub(crate) fn generate_chunks_in_parallel( let mut duplicates_map: StringArrayHashMap = StringArrayHashMap::default(); let mut chunk_visit_map = AutoBitSet::init_empty(chunks.len())?; + let mut compact_chunk_index: usize = 0; // Compute the final hashes of each chunk, then use those to create the final // paths of each chunk. This can technically be done in parallel but it @@ -383,16 +384,26 @@ pub(crate) fn generate_chunks_in_parallel( chunk.template.placeholder.hash = Some(hash.digest()); let mut rel_path: Vec = Vec::new(); - // Use the byte-writer (`PathTemplate::print`) directly — - // routing through `Display`/`write!` goes via `from_utf8_lossy`, - // which would replace non-UTF-8 dir bytes with U+FFFD and corrupt - // the output path. - // Disk output sanitizes leading `..`; `--compile` keeps it so - // runtime bunfs references to out-of-root entrypoints resolve. - chunk - .template - .print(&mut rel_path, !c.options.compile_mode.is_executable()) - .expect("write to Vec"); + // Inside an executable nobody sees chunk file names, and every importer embeds them: number them instead of + // `chunk-` when the default naming is in effect. + if c.options.compile_mode.is_executable() + && !chunk.entry_point.is_entry_point() + && matches!(&*chunk.template.data, b"./chunk-[hash].[ext]" | b"./[name]-[hash].[ext]" | b"[name]-[hash].[ext]" | b"chunk-[hash].[ext]") + { + write!(&mut rel_path, "./{}.{}", compact_chunk_index, bstr::BStr::new(&chunk.template.placeholder.ext)).expect("write to Vec"); + compact_chunk_index += 1; + } else { + // Use the byte-writer (`PathTemplate::print`) directly — + // routing through `Display`/`write!` goes via `from_utf8_lossy`, + // which would replace non-UTF-8 dir bytes with U+FFFD and corrupt + // the output path. + // Disk output sanitizes leading `..`; `--compile` keeps it so + // runtime bunfs references to out-of-root entrypoints resolve. + chunk + .template + .print(&mut rel_path, !c.options.compile_mode.is_executable()) + .expect("write to Vec"); + } path::resolve_path::platform_to_posix_in_place::(&mut rel_path); if path_names_map.get_or_put(&rel_path)?.found_existing { @@ -1314,7 +1325,56 @@ pub(crate) fn generate_chunks_in_parallel( return Ok(result); } - Ok(output_files.take()) + let mut result = output_files.take(); + if c.options.generate_bytecode_cache && c.options.compile_mode.is_executable() { + append_internal_module_bytecode(c, &mut result); + } + Ok(result) +} + +/// `--compile --bytecode`: the executable also carries ahead-of-time bytecode for the internal modules (node:fs, …) the +/// bundle imports, so their first `require` decodes instead of parsing. One `OutputKind::BuiltinBytecode` per module; +/// StandaloneModuleGraph::to_bytes lays them out and InternalModuleRegistry picks them up by id. +fn append_internal_module_bytecode(c: &LinkerContext, output_files: &mut Vec) { + let import_records = c.graph.ast.items_import_records(); + let mut specifiers: Vec<&[u8]> = Vec::new(); + for source_index in &c.graph.reachable_files { + let Some(records) = import_records.get(source_index.get() as usize) else { continue }; + for record in records.as_slice() { + if record.source_index.is_valid() || record.path.text.is_empty() { + continue; + } + let text: &[u8] = record.path.text; + let is_builtin = record.tag == bun_ast::ImportRecordTag::Builtin + || text.starts_with(b"node:") + || text.starts_with(b"bun:") + || bun_resolve_builtins::HardcodedModule::Alias::has(text, crate::options::Target::Bun, Default::default()); + if is_builtin && !specifiers.contains(&text) { + specifiers.push(text); + } + } + } + if specifiers.is_empty() { + return; + } + for (id, bytecode) in crate::bundle_v2::dispatch::generate_internal_module_bytecode(&specifiers, u32::MAX) { + debug!("Internal module bytecode {}: {} bytes", id, bytecode.len()); + output_files.push(options::OutputFile::init(options::OutputFileInit { + output_path: id.to_string().into_bytes().into_boxed_slice(), + input_path: Box::default(), + input_loader: Loader::Js, + hash: None, + output_kind: options::OutputKind::BuiltinBytecode, + loader: Loader::File, + size: Some(bytecode.len()), + display_size: bytecode.len() as u32, + data: options::OutputFileData::Buffer { data: bytecode }, + side: None, + entry_point_index: None, + is_executable: false, + ..Default::default() + })); + } } use crate::EntryPoint; diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 6f5f9909faf8..8391b6026c52 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -435,6 +435,40 @@ let blob: Buffer; writeIfNotChangedBinary(path.join(CODEGEN_DIR, "InternalModuleRegistryConstants.bin"), blob); +// Identifies these module sources to bytecode generated from them ahead of time (bun build --compile embeds bytecode for +// the internal modules an app uses); computed over the bundled outputs so it is meaningful in debug builds too. +const internalModulesStamp = (() => { + const h = new Bun.CryptoHasher("sha256"); + for (const id of moduleList.slice(0, nativeStartIndex)) h.update(outputs.get(id.slice(0, -3).replaceAll("/", path.sep)) ?? ""); + return new DataView(h.digest().buffer).getUint32(0); +})(); + +// require() edges between JS internal modules that run when the module itself is evaluated (ids are enum order), as +// offsets into one flat list. The bundled output is unminified with top-level statements at column 0 and function +// bodies indented, so a registry lookup on an unindented line is one the module wrapper executes eagerly; lookups inside +// functions are lazy and left out (an ahead-of-time build would rather not carry modules that may never load). +const internalModuleDependencyTable = (() => { + const jsModules = moduleList.slice(0, nativeStartIndex); + const requireRe = /internalModuleRegistry, ?(\d+)/g; + const offsets: number[] = []; + const flat: number[] = []; + jsModules.forEach((id, n) => { + offsets.push(flat.length); + const src = outputs.get(id.slice(0, -3).replaceAll("/", path.sep)) ?? ""; + const edges = new Set(); + for (const line of src.split("\n")) { + if (/^\s/.test(line)) continue; + for (const m of line.matchAll(requireRe)) { + const dep = Number(m[1]); + if (dep !== n && dep < nativeStartIndex) edges.add(dep); + } + } + flat.push(...[...edges].sort((a, b) => a - b)); + }); + offsets.push(flat.length); + return { offsets, flat }; +})(); + writeIfNotChanged( path.join(CODEGEN_DIR, "InternalModuleRegistryConstants.S"), `// Generated by src/codegen/bundle-modules.ts @@ -471,6 +505,10 @@ extern "C" const char bun_internal_modules_data[]; namespace Bun { namespace InternalModuleRegistryConstants { +static constexpr uint32_t sourceStamp = ${internalModulesStamp}u; +static constexpr uint16_t dependencyOffsets[${nativeStartIndex + 1}] = { ${internalModuleDependencyTable.offsets.join(", ")} }; +static constexpr uint16_t dependencies[${Math.max(1, internalModuleDependencyTable.flat.length)}] = { ${internalModuleDependencyTable.flat.length ? internalModuleDependencyTable.flat.join(", ") : "0"} }; + ${moduleSpans .map( ({ enumName, offset, length }) => @@ -514,7 +552,7 @@ JSValue InternalModuleRegistry::createInternalModuleById(JSGlobalObject* globalO { if (static_cast(id) < ${nativeStartIndex}) { const InternalJSModule& m = internalJSModules[static_cast(id)]; - INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, m.moduleId, m.fileName, m.codeOffset, m.codeLength, m.url); + INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, m.moduleId, m.fileName, m.codeOffset, m.codeLength, m.url, static_cast(id)); } switch (id) { // Native modules diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index e02f8edeb4cf..46e0108344c9 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -201,9 +201,12 @@ class BunBuiltinNames { WTF_MAKE_NONCOPYABLE(BunBuiltinNames); friend class JSVMClientData; explicit BunBuiltinNames(JSC::VM&); - ~BunBuiltinNames(); public: + ~BunBuiltinNames(); + // For a VM without JSVMClientData that still needs to parse builtins (ahead-of-time bytecode generation). + static std::unique_ptr createStandalone(JSC::VM& vm) { return std::unique_ptr(new BunBuiltinNames(vm)); } + enum class Name : uint16_t { #define BUN_BUILTIN_NAME_ENUM(name) k_##name, BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME(BUN_BUILTIN_NAME_ENUM) diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 9ca3ed722253..1ac4af7397de 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -783,3 +783,11 @@ export const byteStreamInternals = { stream: ReadableStream, ) => void, }; + +/// How many internal modules (node:fs etc.) this process created from bytecode embedded by `bun build --compile +/// --bytecode` instead of parsing their source. +export const internalModulesLoadedFromBytecode: () => number = $newCppFunction( + "InternalModuleRegistry.cpp", + "jsInternalModulesLoadedFromBytecode", + 0, +); diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index 5760b9b6cdee..82e82a53420c 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -31,6 +31,17 @@ unsafe extern "C" { // `UnsafeCell`); `&mut` is ABI-identical to a non-null `*mut` and the C++ // refcount decrement is interior to the cell. safe fn CachedBytecode__deref(this: &mut CachedBytecode); + + /// InternalModuleRegistry.cpp: bytecode for internal JS module `id`, as the registry will consume it. + fn Bun__generateInternalModuleBytecode( + id: u32, + depth: u32, + output_byte_code: *mut Option>, + output_byte_code_size: *mut usize, + cached_bytecode: *mut Option>, + ) -> bool; + /// InternalModuleRegistry.cpp: the internal JS modules `id` statically requires. + fn Bun__internalModuleDependencies(id: u32, out: *mut *const u16) -> usize; } impl CachedBytecode { @@ -144,3 +155,62 @@ pub(crate) fn __bun_jsc_generate_cached_bytecode( CachedBytecode__deref(CachedBytecode::opaque_mut(handle.as_ptr())); Some(owned) } + +/// `bun build --compile --bytecode`: for the builtin module specifiers a bundle imports (e.g. `b"node:fs"`), the +/// InternalModuleRegistry ids of those modules and everything they statically require, each with bytecode generated the +/// way InternalModuleRegistry::generateModule consumes it. Specifiers that are not JS internal modules are skipped. +#[unsafe(no_mangle)] +/// `depth` bounds nested-function code blocks (`u32::MAX` = all of them; 0 = just each module wrapper's own). +pub(crate) fn __bun_jsc_generate_internal_module_bytecode(specifiers: &[&[u8]], depth: u32) -> Vec<(u32, Box<[u8]>)> { + crate::virtual_machine::IS_BUNDLER_THREAD_FOR_BYTECODE_CACHE.set(true); + crate::initialize(crate::InitializeOptions::default()); + + let mut wanted: Vec = Vec::new(); + let push = |id: u32, wanted: &mut Vec| { + if !wanted.contains(&id) { + wanted.push(id); + } + }; + for specifier in specifiers { + let alias = bun_resolve_builtins::Alias::get(specifier, bun_ast::Target::Bun, Default::default()); + let canonical: &[u8] = match &alias { + Some(alias) => alias.path.as_bytes(), + None => specifier, + }; + if let Some(tag) = crate::ResolvedSourceTag::try_from_name(canonical) { + if tag.0 >= 512 { + push(tag.0 - 512, &mut wanted); + } + } + } + // Transitive static requires, breadth-first. + let mut i = 0; + while i < wanted.len() { + let mut deps: *const u16 = core::ptr::null(); + // SAFETY: C++ returns a pointer into a static table and its length. + let count = unsafe { Bun__internalModuleDependencies(wanted[i], &mut deps) }; + for k in 0..count { + // SAFETY: k < count. + let dep = unsafe { *deps.add(k) } as u32; + push(dep, &mut wanted); + } + i += 1; + } + + let mut out = Vec::with_capacity(wanted.len()); + for id in wanted { + let mut bytes: Option> = None; + let mut size: usize = 0; + let mut handle: Option> = None; + // SAFETY: out-params are initialized locals; C++ fills them on success. + if !unsafe { Bun__generateInternalModuleBytecode(id, depth, &mut bytes, &mut size, &mut handle) } { + continue; + } + let (Some(bytes), Some(handle)) = (bytes, handle) else { continue }; + // SAFETY: `bytes[..size]` is the CachedBytecode's payload, valid until the deref below. + let owned = Box::<[u8]>::from(unsafe { core::slice::from_raw_parts(bytes.as_ptr(), size) }); + CachedBytecode__deref(CachedBytecode::opaque_mut(handle.as_ptr())); + out.push((id, owned)); + } + out +} diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index e85b442552b0..a0d1d92d1688 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -416,6 +416,20 @@ pub fn standalone_module_graph() -> Option<&'static dyn bun_resolver::Standalone STANDALONE_MODULE_GRAPH.get().copied() } +/// InternalModuleRegistry::generateModule: ahead-of-time bytecode for internal module `id` from a `bun build --compile` +/// executable (process-lifetime bytes JSC may alias), if this process is one and it carries it. +#[unsafe(no_mangle)] +pub extern "C" fn Bun__standaloneInternalModuleBytecode(_vm: *mut c_void, id: u32, bytes: *mut *const u8, size: *mut usize) -> bool { + let Some(graph) = standalone_module_graph() else { return false }; + let Some(found) = graph.builtin_module_bytecode(id) else { return false }; + // SAFETY: out-params supplied by the C++ caller; `found` points into the executable's mapped section. + unsafe { + *bytes = found.cast::(); + *size = found.len(); + } + true +} + // ────────────────────────────────────────────────────────────────────────── // Nested types // ────────────────────────────────────────────────────────────────────────── diff --git a/src/jsc/bindings/InternalModuleRegistry.cpp b/src/jsc/bindings/InternalModuleRegistry.cpp index f62c5ba050f9..112a34ef301c 100644 --- a/src/jsc/bindings/InternalModuleRegistry.cpp +++ b/src/jsc/bindings/InternalModuleRegistry.cpp @@ -13,6 +13,16 @@ #include "wtf/Forward.h" #include "NativeModuleImpl.h" +#include "BunBuiltinNames.h" +#include +#include +#include +#include + +// A `bun build --compile` executable may carry ahead-of-time bytecode for the internal modules the app uses +// (StandaloneModuleGraph, Flags::HAS_BUILTIN_BYTECODE); those bytes live in the executable for the life of the process. +extern "C" bool Bun__standaloneInternalModuleBytecode(void* bunVM, uint32_t id, const uint8_t** bytes, size_t* size); + namespace Bun { extern "C" bool BunTest__shouldGenerateCodeCoverage(BunString sourceURL); @@ -33,24 +43,44 @@ static void maybeAddCodeCoverage(JSC::VM& vm, const JSC::SourceCode& code) // JS builtin that acts as a module. In debug mode, we use a different implementation that reads // from the developer's filesystem. This allows reloading code without recompiling bindings. -JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, const String& SOURCE, const String& moduleName, const String& urlString) +static unsigned s_internalModulesFromBytecode = 0; + +// bun:internal-for-testing: how many internal modules this process created from embedded bytecode rather than source. +JSC_DEFINE_HOST_FUNCTION(jsInternalModulesLoadedFromBytecode, (JSC::JSGlobalObject*, JSC::CallFrame*)) +{ + return JSValue::encode(jsNumber(s_internalModulesFromBytecode)); +} + +static SourceCode makeInternalModuleSource(const String& text, const String& moduleName, const String& urlString) +{ + return JSC::makeSource(text, SourceOrigin(WTF::URL(urlString)), JSC::SourceTaintedOrigin::Untainted, moduleName); +} + +static UnlinkedFunctionExecutable* createInternalModuleExecutable(JSC::VM& vm, const SourceCode& source, const String& moduleName) +{ + return createBuiltinExecutable(vm, source, Identifier::fromString(vm, moduleName), ImplementationVisibility::Public, ConstructorKind::None, ConstructAbility::CannotConstruct, InlineAttribute::None); +} + +JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, const String& SOURCE, const String& moduleName, const String& urlString, uint32_t id) { auto throwScope = DECLARE_THROW_SCOPE(vm); - auto&& origin = SourceOrigin(WTF::URL(urlString)); - SourceCode source = JSC::makeSource(SOURCE, origin, JSC::SourceTaintedOrigin::Untainted, moduleName); + SourceCode source = makeInternalModuleSource(SOURCE, moduleName, urlString); maybeAddCodeCoverage(vm, source); - JSFunction* func - = JSFunction::create( - vm, globalObject, - createBuiltinExecutable( - vm, source, - Identifier::fromString(vm, moduleName), - ImplementationVisibility::Public, - ConstructorKind::None, - ConstructAbility::CannotConstruct, - InlineAttribute::None) - ->link(vm, nullptr, source), - static_cast(globalObject)); + + UnlinkedFunctionExecutable* executable = nullptr; + const uint8_t* cachedBytes = nullptr; + size_t cachedSize = 0; + if (Bun__standaloneInternalModuleBytecode(static_cast(globalObject)->bunVM(), id, &cachedBytes, &cachedSize)) { + Ref cached = JSC::CachedBytecode::create(std::span { const_cast(cachedBytes), cachedSize }, [](const void*) { }, { }); + cached->setPayloadIsPersistent(); + executable = JSC::decodeBuiltinFunction(vm, WTF::move(cached), *source.provider(), InternalModuleRegistryConstants::sourceStamp); + if (executable) + ++s_internalModulesFromBytecode; + } + if (!executable) + executable = createInternalModuleExecutable(vm, source, moduleName); + + JSFunction* func = JSFunction::create(vm, globalObject, executable->link(vm, nullptr, source), static_cast(globalObject)); RETURN_IF_EXCEPTION(throwScope, {}); if (globalObject->hasDebugger() && globalObject->debugger()->isInteractivelyDebugging()) [[unlikely]] { @@ -101,31 +131,40 @@ ALWAYS_INLINE JSC::JSValue generateNativeModule( } #ifdef BUN_DYNAMIC_JS_LOAD_PATH -JSValue initializeInternalModuleFromDisk(JSGlobalObject* globalObject, VM& vm, const WTF::String& moduleName, WTF::String fileBase, const WTF::String& urlString) +static WTF::String internalModuleSourceFromDisk(const WTF::String& moduleName, WTF::String fileBase) { WTF::String file = makeString(ASCIILiteral::fromLiteralUnsafe(BUN_DYNAMIC_JS_LOAD_PATH), "/"_s, WTF::move(fileBase)); - if (auto contents = WTF::FileSystemImpl::readEntireFile(file)) { - auto string = WTF::String::fromUTF8(contents.value()); - return generateModule(globalObject, vm, string, moduleName, urlString); - } else { + auto contents = WTF::FileSystemImpl::readEntireFile(file); + if (!contents) { printf("\nFATAL: bun-debug failed to load bundled version of \"%s\" at \"%s\" (was it deleted?)\n" "Please re-compile Bun to continue.\n\n", moduleName.utf8().span().data(), file.utf8().span().data()); CRASH(); } + return WTF::String::fromUTF8(contents.value()); } -#define INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, moduleId, filename, OFFSET, LENGTH, urlString) \ - return initializeInternalModuleFromDisk(globalObject, vm, moduleId, filename, urlString) + +JSValue initializeInternalModuleFromDisk(JSGlobalObject* globalObject, VM& vm, const WTF::String& moduleName, WTF::String fileBase, const WTF::String& urlString, uint32_t id) +{ + { + auto string = internalModuleSourceFromDisk(moduleName, WTF::move(fileBase)); + return generateModule(globalObject, vm, string, moduleName, urlString, id); + } +} +#define INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, moduleId, filename, OFFSET, LENGTH, urlString, ID) \ + return initializeInternalModuleFromDisk(globalObject, vm, moduleId, filename, urlString, ID) +#define INTERNAL_MODULE_SOURCE(m) internalModuleSourceFromDisk(m.moduleId, m.fileName) #else // The module sources are linked as one read-only blob (bun_internal_modules_data, // see the generated InternalModuleRegistryConstants.S); each module is a span at // a known offset/length. createWithoutCopying is the same path the old // ASCIILiteral → String conversion took. -#define INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, moduleId, filename, OFFSET, LENGTH, urlString) \ +#define INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, moduleId, filename, OFFSET, LENGTH, urlString, ID) \ return generateModule(globalObject, vm, \ WTF::String(WTF::StringImpl::createWithoutCopying(std::span(bun_internal_modules_data + (OFFSET), (LENGTH)))), \ - moduleId, urlString) + moduleId, urlString, ID) +#define INTERNAL_MODULE_SOURCE(m) WTF::String(WTF::StringImpl::createWithoutCopying(std::span(bun_internal_modules_data + m.codeOffset, m.codeLength))) #endif const ClassInfo InternalModuleRegistry::s_info = { "InternalModuleRegistry"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(InternalModuleRegistry) }; @@ -200,4 +239,49 @@ JSC_DEFINE_HOST_FUNCTION(InternalModuleRegistry::jsCreateInternalModuleById, (JS } // namespace Bun +namespace Zig { JSC::VM& vmForBytecodeCache(); } + +// bun build --compile: bytecode for internal JS module `id` (an index below BUN_NATIVE_MODULE_START_INDEX), generated the +// way generateModule() will consume it. The caller owns *handle and releases it with CachedBytecode__deref. +// `depth`: how many levels of nested functions get code blocks too (UINT32_MAX = all; 0 = only the module wrapper's own). +extern "C" bool Bun__generateInternalModuleBytecode(uint32_t id, uint32_t depth, const uint8_t** bytes, size_t* size, JSC::CachedBytecode** handle) +{ + using namespace Bun; + if (id >= std::size(internalJSModules)) + return false; + const InternalJSModule& m = internalJSModules[id]; + JSC::VM& vm = Zig::vmForBytecodeCache(); + JSC::JSLockHolder locker(vm); + // The builtins parse with private @names; register Bun's on this throwaway VM (its own VM gets them from JSVMClientData). + static thread_local std::unique_ptr builtinNames; + if (!vm.clientData && !builtinNames) + builtinNames = BunBuiltinNames::createStandalone(vm); + String text = INTERNAL_MODULE_SOURCE(m); + SourceCode source = makeInternalModuleSource(text, m.moduleId, m.url); + UnlinkedFunctionExecutable* executable = createInternalModuleExecutable(vm, source, m.moduleId); + ParserError error; + JSC::recursivelyGenerateUnlinkedCodeBlocksForFunction(vm, executable, source, error, depth); + if (error.isValid()) + return false; + RefPtr result = JSC::encodeBuiltinFunction(vm, executable, source.length(), InternalModuleRegistryConstants::sourceStamp); + if (!result) + return false; + result->ref(); + *bytes = result->span().data(); + *size = result->size(); + *handle = result.get(); + return true; +} + +// The internal JS modules `id` statically requires (so an ahead-of-time build can include them too). +extern "C" size_t Bun__internalModuleDependencies(uint32_t id, const uint16_t** out) +{ + using namespace Bun::InternalModuleRegistryConstants; + if (id + 1 >= std::size(dependencyOffsets)) + return 0; + *out = dependencies + dependencyOffsets[id]; + return dependencyOffsets[id + 1] - dependencyOffsets[id]; +} + #undef INTERNAL_MODULE_REGISTRY_GENERATE +#undef INTERNAL_MODULE_SOURCE diff --git a/src/jsc/bindings/InternalModuleRegistry.h b/src/jsc/bindings/InternalModuleRegistry.h index 3570108ee75d..8786c6eeaaf1 100644 --- a/src/jsc/bindings/InternalModuleRegistry.h +++ b/src/jsc/bindings/InternalModuleRegistry.h @@ -58,4 +58,6 @@ class InternalModuleRegistry : public JSInternalFieldObjectImplderef(); } -static JSC::VM& getVMForBytecodeCache() +JSC::VM& vmForBytecodeCache(); +JSC::VM& vmForBytecodeCache() { - static thread_local JSC::VM* vmForBytecodeCache = nullptr; - if (!vmForBytecodeCache) { + static thread_local JSC::VM* vm = nullptr; + if (!vm) { const auto heapSize = JSC::HeapType::Small; auto vmPtr = JSC::VM::tryCreate(heapSize); vmPtr->refSuppressingSaferCPPChecking(); - vmForBytecodeCache = vmPtr.get(); + vm = vmPtr.get(); vmPtr->heap.acquireAccess(); } - return *vmForBytecodeCache; + return *vm; } extern "C" bool generateCachedModuleByteCodeFromSourceCode(BunString* sourceProviderURL, const Latin1Character* inputSourceCode, size_t inputSourceCodeSize, const uint8_t** outputByteCode, size_t* outputByteCodeSize, JSC::CachedBytecode** cachedBytecodePtr) @@ -204,7 +205,7 @@ extern "C" bool generateCachedModuleByteCodeFromSourceCode(BunString* sourceProv std::span sourceCodeSpan(inputSourceCode, inputSourceCodeSize); JSC::SourceCode sourceCode = JSC::makeSource(WTF::String(sourceCodeSpan), toSourceOrigin(sourceProviderURL->toWTFString(), false), JSC::SourceTaintedOrigin::Untainted); - JSC::VM& vm = getVMForBytecodeCache(); + JSC::VM& vm = vmForBytecodeCache(); JSC::JSLockHolder locker(vm); LexicallyScopedFeatures lexicallyScopedFeatures = StrictModeLexicallyScopedFeature; @@ -239,7 +240,7 @@ extern "C" bool generateCachedCommonJSProgramByteCodeFromSourceCode(BunString* s std::span sourceCodeSpan(inputSourceCode, inputSourceCodeSize); JSC::SourceCode sourceCode = JSC::makeSource(WTF::String(sourceCodeSpan), toSourceOrigin(sourceProviderURL->toWTFString(), false), JSC::SourceTaintedOrigin::Untainted); - JSC::VM& vm = getVMForBytecodeCache(); + JSC::VM& vm = vmForBytecodeCache(); JSC::JSLockHolder locker(vm); LexicallyScopedFeatures lexicallyScopedFeatures = NoLexicallyScopedFeatures; diff --git a/src/resolver/standalone_module_graph.rs b/src/resolver/standalone_module_graph.rs index d166e2217371..a0aa48e58534 100644 --- a/src/resolver/standalone_module_graph.rs +++ b/src/resolver/standalone_module_graph.rs @@ -27,4 +27,9 @@ pub trait StandaloneModuleGraph: Send + Sync { /// so `process.execArgv` (lower-tier `bun_jsc` callers holding only the /// trait object) can read it without downcasting to the concrete graph. fn compile_exec_argv(&self) -> &[u8]; + /// Ahead-of-time bytecode for InternalModuleRegistry module `id` embedded by `bun build --compile`, if any. + /// A raw pointer because JSC reads (and may patch) it in place; the bytes live for the process. + fn builtin_module_bytecode(&self, _id: u32) -> Option<*mut [u8]> { + None + } } diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index c9a70b886c79..e870812d5499 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -768,7 +768,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< OutputKind::Asset => {} OutputKind::Bytecode => {} OutputKind::Sourcemap => {} - OutputKind::ModuleInfo => {} + OutputKind::ModuleInfo | OutputKind::BuiltinBytecode => {} OutputKind::MetafileJson | OutputKind::MetafileMarkdown => {} } } diff --git a/src/runtime/cli/build_command.rs b/src/runtime/cli/build_command.rs index 0cbc30ef72a7..3e35374324f3 100644 --- a/src/runtime/cli/build_command.rs +++ b/src/runtime/cli/build_command.rs @@ -1090,7 +1090,7 @@ impl BuildCommand { options::OutputKind::Asset => "", options::OutputKind::Sourcemap => "", options::OutputKind::Bytecode => "", - options::OutputKind::ModuleInfo => "", + options::OutputKind::ModuleInfo | options::OutputKind::BuiltinBytecode => "", options::OutputKind::MetafileJson | options::OutputKind::MetafileMarkdown => "", }))?; @@ -1136,6 +1136,7 @@ impl BuildCommand { options::OutputKind::Sourcemap => "source map", options::OutputKind::Bytecode => "bytecode", options::OutputKind::ModuleInfo => "module info", + options::OutputKind::BuiltinBytecode => "builtin bytecode", options::OutputKind::MetafileJson => "metafile json", options::OutputKind::MetafileMarkdown => "metafile markdown", } diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 3b3376ef4a3d..49c62d0a2920 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -45,6 +45,8 @@ pub struct StandaloneModuleGraph { pub entry_point_id: u32, pub compile_exec_argv: &'static [u8], pub flags: Flags, + /// InternalModuleRegistry id → its bytecode inside `bytes` (JSC reads it in place; see `File::bytecode`). + pub builtin_bytecode: Vec<(u32, *mut [u8])>, } // We never want to hit the filesystem for these files @@ -316,6 +318,9 @@ impl bun_resolver::StandaloneModuleGraph for StandaloneModuleGraph { fn compile_exec_argv(&self) -> &[u8] { self.compile_exec_argv } + fn builtin_module_bytecode(&self, id: u32) -> Option<*mut [u8]> { + StandaloneModuleGraph::builtin_module_bytecode(self, id) + } } #[repr(C)] @@ -658,6 +663,9 @@ bitflags::bitflags! { /// the module table, so loading a module from bytecode never has to /// hash — i.e. page in — its source text. const HAS_SOURCE_HASHES = 1 << 5; + /// After the source hashes: `u32 count`, then `count` × `{ u32 id, StringPointer bytes }` — ahead-of-time + /// bytecode for internal modules (InternalModuleRegistry ids), read by InternalModuleRegistry::generateModule. + const HAS_BUILTIN_BYTECODE = 1 << 6; // _padding: u26 } } @@ -687,6 +695,7 @@ impl StandaloneModuleGraph { entry_point_id: 0, compile_exec_argv: b"", flags: Flags::default(), + builtin_bytecode: Vec::new(), }); } @@ -729,6 +738,28 @@ impl StandaloneModuleGraph { return Err(crate::Error::CorruptedModuleGraphEntryPointIDIsGreaterThanModuleListCount); } + let mut builtin_bytecode: Vec<(u32, *mut [u8])> = Vec::new(); + if offsets.flags.contains(Flags::HAS_BUILTIN_BYTECODE) { + let table_offset = offsets.modules_ptr.offset as usize + + offsets.modules_ptr.length as usize + + if source_hashes.is_some() { modules_list_count * size_of::() } else { 0 }; + // SAFETY: `to_bytes` wrote `u32 count` + `count` records right here; read-only, unaligned-safe reads. + let read_u32 = |at: usize| -> u32 { + debug_assert!(at + 4 <= raw_len); + unsafe { core::ptr::read_unaligned(raw_const.add(at).cast::()) } + }; + let count = read_u32(table_offset) as usize; + builtin_bytecode.reserve(count); + for i in 0..count { + let record = table_offset + size_of::() + i * 3 * size_of::(); + let id = read_u32(record); + let pointer = StringPointer { offset: read_u32(record + 4), length: read_u32(record + 8) }; + // SAFETY: same provenance rules as `File::bytecode`: a writable subrange JSC may patch in place. + let bytes = unsafe { core::ptr::slice_from_raw_parts_mut(raw_ptr.add(pointer.offset as usize), pointer.length as usize) }; + builtin_bytecode.push((id, bytes)); + } + } + let mut modules = StringArrayHashMap::::new(); modules.reserve(modules_list_count); for i in 0..modules_list_count { @@ -829,8 +860,14 @@ impl StandaloneModuleGraph { } .as_bytes(), flags: offsets.flags, + builtin_bytecode, }) } + + /// Ahead-of-time bytecode for internal module `id`, if the executable carries it. + pub fn builtin_module_bytecode(&self, id: u32) -> Option<*mut [u8]> { + self.builtin_bytecode.iter().find(|(candidate, _)| *candidate == id).map(|(_, bytes)| *bytes) + } } /// Read-only subslice helper. Builds a `&'static [u8]` over the *subrange only* so no @@ -971,9 +1008,11 @@ pub(crate) fn to_bytes( // the exact amount is not possible without allocating as it // involves a JSON parser. string_builder.cap += bytes.len() * 2; - } else if output_file.output_kind == options::OutputKind::Bytecode { - // Allocate up to 256 byte alignment for bytecode - string_builder.cap += bytes.len().div_ceil(256) * 256 + 256; + } else if output_file.output_kind == options::OutputKind::Bytecode + || output_file.output_kind == options::OutputKind::BuiltinBytecode + { + // Allocate up to 256 byte alignment for bytecode (+ a table record for builtin bytecode) + string_builder.cap += bytes.len().div_ceil(256) * 256 + 256 + 16; } else if output_file.output_kind == options::OutputKind::ModuleInfo { string_builder.cap += bytes.len(); } else { @@ -996,7 +1035,7 @@ pub(crate) fn to_bytes( string_builder.cap += (size_of::() + size_of::()) * output_files.len(); string_builder.cap += TRAILER.len(); - string_builder.cap += 16; + string_builder.cap += 16 + size_of::(); string_builder.cap += size_of::(); string_builder.count_z(compile_exec_argv); @@ -1049,31 +1088,7 @@ pub(crate) fn to_bytes( let bytecode = output_files[output_file.bytecode_index as usize] .value .as_slice(); - let current_offset = string_builder.len; - // Calculate padding so that (current_offset + padding) % 128 == 120 - // This accounts for the 8-byte section header on PE/Mach-O platforms. - let target_mod: usize = 128 - size_of::(); // 120 = accounts for 8-byte header - let current_mod = current_offset % 128; - let padding = if current_mod <= target_mod { - target_mod - current_mod - } else { - 128 - current_mod + target_mod - }; - // Zero the padding bytes to ensure deterministic output - let writable = string_builder.writable(); - writable[0..padding].fill(0); - string_builder.len += padding; - let aligned_offset = string_builder.len; - let writable_after_padding = string_builder.writable(); - writable_after_padding[0..bytecode.len()] - .copy_from_slice(&bytecode[0..bytecode.len()]); - let unaligned_space = &writable_after_padding[bytecode.len()..]; - let len = bytecode.len() + unaligned_space.len().min(128); - string_builder.len += len; - break 'brk StringPointer { - offset: aligned_offset as u32, - length: len as u32, - }; + break 'brk append_bytecode_aligned(&mut string_builder, bytecode); } else { break 'brk StringPointer::default(); } @@ -1177,6 +1192,26 @@ pub(crate) fn to_bytes( module_files.push(output_file); } + // Ahead-of-time bytecode for internal modules rides in the same front region as module bytecode. + let mut builtin_bytecode_table: Vec = Vec::new(); + { + let mut count: u32 = 0; + builtin_bytecode_table.extend_from_slice(&0u32.to_le_bytes()); + for output_file in output_files { + if output_file.output_kind != options::OutputKind::BuiltinBytecode { + continue; + } + let options::OutputFileValue::Buffer { bytes } = &output_file.value else { continue }; + let Some(id) = core::str::from_utf8(&output_file.dest_path).ok().and_then(|s| s.parse::().ok()) else { continue }; + let pointer = append_bytecode_aligned(&mut string_builder, bytes); + builtin_bytecode_table.extend_from_slice(&id.to_le_bytes()); + builtin_bytecode_table.extend_from_slice(&pointer.offset.to_le_bytes()); + builtin_bytecode_table.extend_from_slice(&pointer.length.to_le_bytes()); + count += 1; + } + builtin_bytecode_table[0..4].copy_from_slice(&count.to_le_bytes()); + } + // Region layout after the bytecode/module_info run above: source maps // (unread until an error prints), then every file's source text as one run // (`Flags::SOURCE_TEXT_CONTIGUOUS`, so `hint_source_pages_dont_need` can @@ -1250,12 +1285,14 @@ pub(crate) fn to_bytes( let modules_ptr = string_builder.append_count(modules_as_bytes); let hashes_ptr = string_builder.append_count(&source_hashes); debug_assert_eq!(hashes_ptr.offset, modules_ptr.offset + modules_ptr.length); + let builtin_table_ptr = string_builder.append_count(&builtin_bytecode_table); + debug_assert_eq!(builtin_table_ptr.offset, hashes_ptr.offset + hashes_ptr.length); let offsets = Offsets { entry_point_id: entry_point_id as u32, modules_ptr, compile_exec_argv_ptr: string_builder.append_count_z(compile_exec_argv), byte_count: string_builder.len, - flags: flags | Flags::SOURCE_TEXT_CONTIGUOUS | Flags::HAS_SOURCE_HASHES, + flags: flags | Flags::SOURCE_TEXT_CONTIGUOUS | Flags::HAS_SOURCE_HASHES | Flags::HAS_BUILTIN_BYTECODE, }; // SAFETY: `Offsets` is `#[repr(C)]` POD; same `modules_as_bytes` rationale as above. @@ -2512,6 +2549,24 @@ impl StandaloneModuleGraph { /// Allocates a StandaloneModuleGraph in the process-static `INSTANCE`, /// populates it from bytes, sets it globally, and returns the pointer. +/// JSC reads cached bytecode in place and expects its start 128-byte aligned once mapped. The section data begins +/// 8 bytes after a page-aligned address (the length header), so the offset must be 120 mod 128. +fn append_bytecode_aligned(string_builder: &mut bun_core::StringBuilder, bytecode: &[u8]) -> StringPointer { + let target_mod: usize = 128 - size_of::(); + let current_mod = string_builder.len % 128; + let padding = if current_mod <= target_mod { target_mod - current_mod } else { 128 - current_mod + target_mod }; + let writable = string_builder.writable(); + writable[0..padding].fill(0); + string_builder.len += padding; + let aligned_offset = string_builder.len; + let writable_after_padding = string_builder.writable(); + writable_after_padding[0..bytecode.len()].copy_from_slice(bytecode); + let unaligned_space = &writable_after_padding[bytecode.len()..]; + let len = bytecode.len() + unaligned_space.len().min(128); + string_builder.len += len; + StringPointer { offset: aligned_offset as u32, length: len as u32 } +} + fn from_bytes_alloc( raw_ptr: *mut u8, raw_len: usize, diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 282b107f0b9f..82d222b0fe3f 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -90,6 +90,40 @@ console.log(JSON.stringify({ n, anonKB: anon }));`, 60_000, ); + test("--bytecode embeds bytecode for the internal modules the app imports", async () => { + using dir = tempDir("build-compile-builtin-bytecode", { + "app.js": `import { join } from "node:path"; +import http from "node:http"; +const { internalModulesLoadedFromBytecode } = require("bun:internal-for-testing"); +const server = http.createServer(() => {}); +console.log(JSON.stringify({ joined: join("a", "b"), fromBytecode: internalModulesLoadedFromBytecode() })); +server.close();`, + }); + for (const bytecode of [false, true]) { + const outfile = join(dir + "", bytecode ? "app-bytecode" : "app-source"); + const result = await Bun.build({ + entrypoints: [join(dir + "", "app.js")], + compile: { outfile }, + bytecode, + format: "esm", + target: "bun", + }); + expect(result.success).toBe(true); + await using proc = Bun.spawn({ cmd: [outfile], env: bunEnv, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + const { joined, fromBytecode } = JSON.parse(stdout.trim()); + expect(joined).toBe(join("a", "b")); + if (bytecode) { + // node:path, node:http and what they require at load (node:net, node:events, the stream internals, ...). + expect(fromBytecode).toBeGreaterThan(10); + } else { + expect(fromBytecode).toBe(0); + } + expect(exitCode).toBe(0); + } + }); + test("compile with invalid target fails gracefully", async () => { using dir = tempDir("build-compile-invalid", { "index.js": `console.log("test");`, From a85069ff6899e8f7f7f955e97101ef07e3958fc6 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:25:18 +0000 Subject: [PATCH 23/44] [autofix.ci] apply automated fixes --- src/bundler/bundle_v2.rs | 10 +++- .../generateChunksInParallel.rs | 30 ++++++++-- src/codegen/bundle-modules.ts | 3 +- src/jsc/CachedBytecode.rs | 16 +++-- src/jsc/VirtualMachine.rs | 15 ++++- src/jsc/bindings/InternalModuleRegistry.cpp | 6 +- src/runtime/cli/build_command.rs | 4 +- src/standalone_graph/StandaloneModuleGraph.rs | 60 +++++++++++++++---- 8 files changed, 115 insertions(+), 29 deletions(-) diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index b2b2c5786fc8..c3c3dfb97a83 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1413,7 +1413,10 @@ pub mod bv2_impl { /// Defined `#[no_mangle]` in `bun_jsc::cached_bytecode`: (registry id, bytecode) for the internal modules named /// by `specifiers` plus their static requires. - safe fn __bun_jsc_generate_internal_module_bytecode(specifiers: &[&[u8]], depth: u32) -> Vec<(u32, Box<[u8]>)>; + safe fn __bun_jsc_generate_internal_module_bytecode( + specifiers: &[&[u8]], + depth: u32, + ) -> Vec<(u32, Box<[u8]>)>; } unsafe extern "Rust" { @@ -1456,7 +1459,10 @@ pub mod bv2_impl { } #[inline] - pub(crate) fn generate_internal_module_bytecode(specifiers: &[&[u8]], depth: u32) -> Vec<(u32, Box<[u8]>)> { + pub(crate) fn generate_internal_module_bytecode( + specifiers: &[&[u8]], + depth: u32, + ) -> Vec<(u32, Box<[u8]>)> { __bun_jsc_generate_internal_module_bytecode(specifiers, depth) } diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 1a79ba0fd768..24e892205441 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -388,9 +388,21 @@ pub(crate) fn generate_chunks_in_parallel( // `chunk-` when the default naming is in effect. if c.options.compile_mode.is_executable() && !chunk.entry_point.is_entry_point() - && matches!(&*chunk.template.data, b"./chunk-[hash].[ext]" | b"./[name]-[hash].[ext]" | b"[name]-[hash].[ext]" | b"chunk-[hash].[ext]") + && matches!( + &*chunk.template.data, + b"./chunk-[hash].[ext]" + | b"./[name]-[hash].[ext]" + | b"[name]-[hash].[ext]" + | b"chunk-[hash].[ext]" + ) { - write!(&mut rel_path, "./{}.{}", compact_chunk_index, bstr::BStr::new(&chunk.template.placeholder.ext)).expect("write to Vec"); + write!( + &mut rel_path, + "./{}.{}", + compact_chunk_index, + bstr::BStr::new(&chunk.template.placeholder.ext) + ) + .expect("write to Vec"); compact_chunk_index += 1; } else { // Use the byte-writer (`PathTemplate::print`) directly — @@ -1339,7 +1351,9 @@ fn append_internal_module_bytecode(c: &LinkerContext, output_files: &mut Vec = Vec::new(); for source_index in &c.graph.reachable_files { - let Some(records) = import_records.get(source_index.get() as usize) else { continue }; + let Some(records) = import_records.get(source_index.get() as usize) else { + continue; + }; for record in records.as_slice() { if record.source_index.is_valid() || record.path.text.is_empty() { continue; @@ -1348,7 +1362,11 @@ fn append_internal_module_bytecode(c: &LinkerContext, output_files: &mut Vec { const h = new Bun.CryptoHasher("sha256"); - for (const id of moduleList.slice(0, nativeStartIndex)) h.update(outputs.get(id.slice(0, -3).replaceAll("/", path.sep)) ?? ""); + for (const id of moduleList.slice(0, nativeStartIndex)) + h.update(outputs.get(id.slice(0, -3).replaceAll("/", path.sep)) ?? ""); return new DataView(h.digest().buffer).getUint32(0); })(); diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index 82e82a53420c..c74dcc225cca 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -161,7 +161,10 @@ pub(crate) fn __bun_jsc_generate_cached_bytecode( /// way InternalModuleRegistry::generateModule consumes it. Specifiers that are not JS internal modules are skipped. #[unsafe(no_mangle)] /// `depth` bounds nested-function code blocks (`u32::MAX` = all of them; 0 = just each module wrapper's own). -pub(crate) fn __bun_jsc_generate_internal_module_bytecode(specifiers: &[&[u8]], depth: u32) -> Vec<(u32, Box<[u8]>)> { +pub(crate) fn __bun_jsc_generate_internal_module_bytecode( + specifiers: &[&[u8]], + depth: u32, +) -> Vec<(u32, Box<[u8]>)> { crate::virtual_machine::IS_BUNDLER_THREAD_FOR_BYTECODE_CACHE.set(true); crate::initialize(crate::InitializeOptions::default()); @@ -172,7 +175,8 @@ pub(crate) fn __bun_jsc_generate_internal_module_bytecode(specifiers: &[&[u8]], } }; for specifier in specifiers { - let alias = bun_resolve_builtins::Alias::get(specifier, bun_ast::Target::Bun, Default::default()); + let alias = + bun_resolve_builtins::Alias::get(specifier, bun_ast::Target::Bun, Default::default()); let canonical: &[u8] = match &alias { Some(alias) => alias.path.as_bytes(), None => specifier, @@ -203,10 +207,14 @@ pub(crate) fn __bun_jsc_generate_internal_module_bytecode(specifiers: &[&[u8]], let mut size: usize = 0; let mut handle: Option> = None; // SAFETY: out-params are initialized locals; C++ fills them on success. - if !unsafe { Bun__generateInternalModuleBytecode(id, depth, &mut bytes, &mut size, &mut handle) } { + if !unsafe { + Bun__generateInternalModuleBytecode(id, depth, &mut bytes, &mut size, &mut handle) + } { continue; } - let (Some(bytes), Some(handle)) = (bytes, handle) else { continue }; + let (Some(bytes), Some(handle)) = (bytes, handle) else { + continue; + }; // SAFETY: `bytes[..size]` is the CachedBytecode's payload, valid until the deref below. let owned = Box::<[u8]>::from(unsafe { core::slice::from_raw_parts(bytes.as_ptr(), size) }); CachedBytecode__deref(CachedBytecode::opaque_mut(handle.as_ptr())); diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index a0d1d92d1688..1015ef81a984 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -419,9 +419,18 @@ pub fn standalone_module_graph() -> Option<&'static dyn bun_resolver::Standalone /// InternalModuleRegistry::generateModule: ahead-of-time bytecode for internal module `id` from a `bun build --compile` /// executable (process-lifetime bytes JSC may alias), if this process is one and it carries it. #[unsafe(no_mangle)] -pub extern "C" fn Bun__standaloneInternalModuleBytecode(_vm: *mut c_void, id: u32, bytes: *mut *const u8, size: *mut usize) -> bool { - let Some(graph) = standalone_module_graph() else { return false }; - let Some(found) = graph.builtin_module_bytecode(id) else { return false }; +pub extern "C" fn Bun__standaloneInternalModuleBytecode( + _vm: *mut c_void, + id: u32, + bytes: *mut *const u8, + size: *mut usize, +) -> bool { + let Some(graph) = standalone_module_graph() else { + return false; + }; + let Some(found) = graph.builtin_module_bytecode(id) else { + return false; + }; // SAFETY: out-params supplied by the C++ caller; `found` points into the executable's mapped section. unsafe { *bytes = found.cast::(); diff --git a/src/jsc/bindings/InternalModuleRegistry.cpp b/src/jsc/bindings/InternalModuleRegistry.cpp index 112a34ef301c..1fd672da4fb3 100644 --- a/src/jsc/bindings/InternalModuleRegistry.cpp +++ b/src/jsc/bindings/InternalModuleRegistry.cpp @@ -71,7 +71,7 @@ JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, cons const uint8_t* cachedBytes = nullptr; size_t cachedSize = 0; if (Bun__standaloneInternalModuleBytecode(static_cast(globalObject)->bunVM(), id, &cachedBytes, &cachedSize)) { - Ref cached = JSC::CachedBytecode::create(std::span { const_cast(cachedBytes), cachedSize }, [](const void*) { }, { }); + Ref cached = JSC::CachedBytecode::create(std::span { const_cast(cachedBytes), cachedSize }, [](const void*) {}, {}); cached->setPayloadIsPersistent(); executable = JSC::decodeBuiltinFunction(vm, WTF::move(cached), *source.provider(), InternalModuleRegistryConstants::sourceStamp); if (executable) @@ -239,7 +239,9 @@ JSC_DEFINE_HOST_FUNCTION(InternalModuleRegistry::jsCreateInternalModuleById, (JS } // namespace Bun -namespace Zig { JSC::VM& vmForBytecodeCache(); } +namespace Zig { +JSC::VM& vmForBytecodeCache(); +} // bun build --compile: bytecode for internal JS module `id` (an index below BUN_NATIVE_MODULE_START_INDEX), generated the // way generateModule() will consume it. The caller owns *handle and releases it with CachedBytecode__deref. diff --git a/src/runtime/cli/build_command.rs b/src/runtime/cli/build_command.rs index 3e35374324f3..bfb5d622662c 100644 --- a/src/runtime/cli/build_command.rs +++ b/src/runtime/cli/build_command.rs @@ -1090,7 +1090,9 @@ impl BuildCommand { options::OutputKind::Asset => "", options::OutputKind::Sourcemap => "", options::OutputKind::Bytecode => "", - options::OutputKind::ModuleInfo | options::OutputKind::BuiltinBytecode => "", + options::OutputKind::ModuleInfo | options::OutputKind::BuiltinBytecode => { + "" + } options::OutputKind::MetafileJson | options::OutputKind::MetafileMarkdown => "", }))?; diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 49c62d0a2920..1ef2622b1411 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -742,7 +742,11 @@ impl StandaloneModuleGraph { if offsets.flags.contains(Flags::HAS_BUILTIN_BYTECODE) { let table_offset = offsets.modules_ptr.offset as usize + offsets.modules_ptr.length as usize - + if source_hashes.is_some() { modules_list_count * size_of::() } else { 0 }; + + if source_hashes.is_some() { + modules_list_count * size_of::() + } else { + 0 + }; // SAFETY: `to_bytes` wrote `u32 count` + `count` records right here; read-only, unaligned-safe reads. let read_u32 = |at: usize| -> u32 { debug_assert!(at + 4 <= raw_len); @@ -753,9 +757,17 @@ impl StandaloneModuleGraph { for i in 0..count { let record = table_offset + size_of::() + i * 3 * size_of::(); let id = read_u32(record); - let pointer = StringPointer { offset: read_u32(record + 4), length: read_u32(record + 8) }; + let pointer = StringPointer { + offset: read_u32(record + 4), + length: read_u32(record + 8), + }; // SAFETY: same provenance rules as `File::bytecode`: a writable subrange JSC may patch in place. - let bytes = unsafe { core::ptr::slice_from_raw_parts_mut(raw_ptr.add(pointer.offset as usize), pointer.length as usize) }; + let bytes = unsafe { + core::ptr::slice_from_raw_parts_mut( + raw_ptr.add(pointer.offset as usize), + pointer.length as usize, + ) + }; builtin_bytecode.push((id, bytes)); } } @@ -866,7 +878,10 @@ impl StandaloneModuleGraph { /// Ahead-of-time bytecode for internal module `id`, if the executable carries it. pub fn builtin_module_bytecode(&self, id: u32) -> Option<*mut [u8]> { - self.builtin_bytecode.iter().find(|(candidate, _)| *candidate == id).map(|(_, bytes)| *bytes) + self.builtin_bytecode + .iter() + .find(|(candidate, _)| *candidate == id) + .map(|(_, bytes)| *bytes) } } @@ -1201,8 +1216,15 @@ pub(crate) fn to_bytes( if output_file.output_kind != options::OutputKind::BuiltinBytecode { continue; } - let options::OutputFileValue::Buffer { bytes } = &output_file.value else { continue }; - let Some(id) = core::str::from_utf8(&output_file.dest_path).ok().and_then(|s| s.parse::().ok()) else { continue }; + let options::OutputFileValue::Buffer { bytes } = &output_file.value else { + continue; + }; + let Some(id) = core::str::from_utf8(&output_file.dest_path) + .ok() + .and_then(|s| s.parse::().ok()) + else { + continue; + }; let pointer = append_bytecode_aligned(&mut string_builder, bytes); builtin_bytecode_table.extend_from_slice(&id.to_le_bytes()); builtin_bytecode_table.extend_from_slice(&pointer.offset.to_le_bytes()); @@ -1286,13 +1308,19 @@ pub(crate) fn to_bytes( let hashes_ptr = string_builder.append_count(&source_hashes); debug_assert_eq!(hashes_ptr.offset, modules_ptr.offset + modules_ptr.length); let builtin_table_ptr = string_builder.append_count(&builtin_bytecode_table); - debug_assert_eq!(builtin_table_ptr.offset, hashes_ptr.offset + hashes_ptr.length); + debug_assert_eq!( + builtin_table_ptr.offset, + hashes_ptr.offset + hashes_ptr.length + ); let offsets = Offsets { entry_point_id: entry_point_id as u32, modules_ptr, compile_exec_argv_ptr: string_builder.append_count_z(compile_exec_argv), byte_count: string_builder.len, - flags: flags | Flags::SOURCE_TEXT_CONTIGUOUS | Flags::HAS_SOURCE_HASHES | Flags::HAS_BUILTIN_BYTECODE, + flags: flags + | Flags::SOURCE_TEXT_CONTIGUOUS + | Flags::HAS_SOURCE_HASHES + | Flags::HAS_BUILTIN_BYTECODE, }; // SAFETY: `Offsets` is `#[repr(C)]` POD; same `modules_as_bytes` rationale as above. @@ -2551,10 +2579,17 @@ impl StandaloneModuleGraph { /// populates it from bytes, sets it globally, and returns the pointer. /// JSC reads cached bytecode in place and expects its start 128-byte aligned once mapped. The section data begins /// 8 bytes after a page-aligned address (the length header), so the offset must be 120 mod 128. -fn append_bytecode_aligned(string_builder: &mut bun_core::StringBuilder, bytecode: &[u8]) -> StringPointer { +fn append_bytecode_aligned( + string_builder: &mut bun_core::StringBuilder, + bytecode: &[u8], +) -> StringPointer { let target_mod: usize = 128 - size_of::(); let current_mod = string_builder.len % 128; - let padding = if current_mod <= target_mod { target_mod - current_mod } else { 128 - current_mod + target_mod }; + let padding = if current_mod <= target_mod { + target_mod - current_mod + } else { + 128 - current_mod + target_mod + }; let writable = string_builder.writable(); writable[0..padding].fill(0); string_builder.len += padding; @@ -2564,7 +2599,10 @@ fn append_bytecode_aligned(string_builder: &mut bun_core::StringBuilder, bytecod let unaligned_space = &writable_after_padding[bytecode.len()..]; let len = bytecode.len() + unaligned_space.len().min(128); string_builder.len += len; - StringPointer { offset: aligned_offset as u32, length: len as u32 } + StringPointer { + offset: aligned_offset as u32, + length: len as u32, + } } fn from_bytes_alloc( From 6a802556cf520350e619f9ecd8539fb53d1bc0e7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 00:29:41 +0000 Subject: [PATCH 24/44] clippy: safety comment placement --- src/standalone_graph/StandaloneModuleGraph.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 1ef2622b1411..03139b19616f 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -747,9 +747,9 @@ impl StandaloneModuleGraph { } else { 0 }; - // SAFETY: `to_bytes` wrote `u32 count` + `count` records right here; read-only, unaligned-safe reads. let read_u32 = |at: usize| -> u32 { debug_assert!(at + 4 <= raw_len); + // SAFETY: `to_bytes` wrote `u32 count` + `count` records right here; read-only, unaligned-safe read within `[0, raw_len)`. unsafe { core::ptr::read_unaligned(raw_const.add(at).cast::()) } }; let count = read_u32(table_offset) as usize; From a004f5220ad4e1fb8293a9d18356d68c4a48480b Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 00:34:57 +0000 Subject: [PATCH 25/44] clippy: raw borrows for FFI out-params --- src/jsc/CachedBytecode.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index c74dcc225cca..9fa2e41aa8ce 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -192,7 +192,7 @@ pub(crate) fn __bun_jsc_generate_internal_module_bytecode( while i < wanted.len() { let mut deps: *const u16 = core::ptr::null(); // SAFETY: C++ returns a pointer into a static table and its length. - let count = unsafe { Bun__internalModuleDependencies(wanted[i], &mut deps) }; + let count = unsafe { Bun__internalModuleDependencies(wanted[i], &raw mut deps) }; for k in 0..count { // SAFETY: k < count. let dep = unsafe { *deps.add(k) } as u32; @@ -208,7 +208,7 @@ pub(crate) fn __bun_jsc_generate_internal_module_bytecode( let mut handle: Option> = None; // SAFETY: out-params are initialized locals; C++ fills them on success. if !unsafe { - Bun__generateInternalModuleBytecode(id, depth, &mut bytes, &mut size, &mut handle) + Bun__generateInternalModuleBytecode(id, depth, &raw mut bytes, &raw mut size, &raw mut handle) } { continue; } From 0199849f724f5a0c2ceede44e0f766ab0697f9a9 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 00:36:36 +0000 Subject: [PATCH 26/44] clippy: FFI accessor writing out-params is unsafe extern --- src/jsc/VirtualMachine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 1015ef81a984..88cb578b247d 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -419,7 +419,7 @@ pub fn standalone_module_graph() -> Option<&'static dyn bun_resolver::Standalone /// InternalModuleRegistry::generateModule: ahead-of-time bytecode for internal module `id` from a `bun build --compile` /// executable (process-lifetime bytes JSC may alias), if this process is one and it carries it. #[unsafe(no_mangle)] -pub extern "C" fn Bun__standaloneInternalModuleBytecode( +pub unsafe extern "C" fn Bun__standaloneInternalModuleBytecode( _vm: *mut c_void, id: u32, bytes: *mut *const u8, From 7161059b8f94b796edd04e959c7d80a3f583246d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:39:24 +0000 Subject: [PATCH 27/44] [autofix.ci] apply automated fixes --- src/jsc/CachedBytecode.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index 9fa2e41aa8ce..4449739f0881 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -208,7 +208,13 @@ pub(crate) fn __bun_jsc_generate_internal_module_bytecode( let mut handle: Option> = None; // SAFETY: out-params are initialized locals; C++ fills them on success. if !unsafe { - Bun__generateInternalModuleBytecode(id, depth, &raw mut bytes, &raw mut size, &raw mut handle) + Bun__generateInternalModuleBytecode( + id, + depth, + &raw mut bytes, + &raw mut size, + &raw mut handle, + ) } { continue; } From 4d0a023ca6541e89e97020a4978dfc4a7e5df5bc Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 01:03:47 +0000 Subject: [PATCH 28/44] Bump WebKit to aff53044c546 (bytecode cache: builtin function entries, depth bound, symbol ref fix) --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 27ab706364e3..67102caf61f5 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-502-7d75b5e0"; +export const WEBKIT_VERSION = "aff53044c546e72cde9ff2c8fbe95dbd401f6c06"; /** * WebKit (JavaScriptCore) — the JS engine. From c815d63d9f84ea46c49fe5cdffe4f88401aca05f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 01:14:40 +0000 Subject: [PATCH 29/44] Review: atomic counter, WebCore::bunVM, overflow-safe bounds check, test.each, doc comment placement --- src/jsc/CachedBytecode.rs | 4 +- src/jsc/bindings/InternalModuleRegistry.cpp | 11 ++--- src/standalone_graph/StandaloneModuleGraph.rs | 5 ++- test/bundler/bun-build-compile.test.ts | 44 +++++++++---------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index 4449739f0881..89b392c3de85 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -157,10 +157,10 @@ pub(crate) fn __bun_jsc_generate_cached_bytecode( } /// `bun build --compile --bytecode`: for the builtin module specifiers a bundle imports (e.g. `b"node:fs"`), the -/// InternalModuleRegistry ids of those modules and everything they statically require, each with bytecode generated the +/// InternalModuleRegistry ids of those modules and everything they eagerly require, each with bytecode generated the /// way InternalModuleRegistry::generateModule consumes it. Specifiers that are not JS internal modules are skipped. -#[unsafe(no_mangle)] /// `depth` bounds nested-function code blocks (`u32::MAX` = all of them; 0 = just each module wrapper's own). +#[unsafe(no_mangle)] pub(crate) fn __bun_jsc_generate_internal_module_bytecode( specifiers: &[&[u8]], depth: u32, diff --git a/src/jsc/bindings/InternalModuleRegistry.cpp b/src/jsc/bindings/InternalModuleRegistry.cpp index 1fd672da4fb3..db9fd40b53bb 100644 --- a/src/jsc/bindings/InternalModuleRegistry.cpp +++ b/src/jsc/bindings/InternalModuleRegistry.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "InternalModuleRegistryConstants.h" @@ -43,12 +44,12 @@ static void maybeAddCodeCoverage(JSC::VM& vm, const JSC::SourceCode& code) // JS builtin that acts as a module. In debug mode, we use a different implementation that reads // from the developer's filesystem. This allows reloading code without recompiling bindings. -static unsigned s_internalModulesFromBytecode = 0; +static std::atomic s_internalModulesFromBytecode { 0 }; // bun:internal-for-testing: how many internal modules this process created from embedded bytecode rather than source. JSC_DEFINE_HOST_FUNCTION(jsInternalModulesLoadedFromBytecode, (JSC::JSGlobalObject*, JSC::CallFrame*)) { - return JSValue::encode(jsNumber(s_internalModulesFromBytecode)); + return JSValue::encode(jsNumber(s_internalModulesFromBytecode.load(std::memory_order_relaxed))); } static SourceCode makeInternalModuleSource(const String& text, const String& moduleName, const String& urlString) @@ -70,12 +71,12 @@ JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, cons UnlinkedFunctionExecutable* executable = nullptr; const uint8_t* cachedBytes = nullptr; size_t cachedSize = 0; - if (Bun__standaloneInternalModuleBytecode(static_cast(globalObject)->bunVM(), id, &cachedBytes, &cachedSize)) { + if (Bun__standaloneInternalModuleBytecode(WebCore::bunVM(globalObject), id, &cachedBytes, &cachedSize)) { Ref cached = JSC::CachedBytecode::create(std::span { const_cast(cachedBytes), cachedSize }, [](const void*) {}, {}); cached->setPayloadIsPersistent(); executable = JSC::decodeBuiltinFunction(vm, WTF::move(cached), *source.provider(), InternalModuleRegistryConstants::sourceStamp); if (executable) - ++s_internalModulesFromBytecode; + s_internalModulesFromBytecode.fetch_add(1, std::memory_order_relaxed); } if (!executable) executable = createInternalModuleExecutable(vm, source, moduleName); @@ -279,7 +280,7 @@ extern "C" bool Bun__generateInternalModuleBytecode(uint32_t id, uint32_t depth, extern "C" size_t Bun__internalModuleDependencies(uint32_t id, const uint16_t** out) { using namespace Bun::InternalModuleRegistryConstants; - if (id + 1 >= std::size(dependencyOffsets)) + if (id >= std::size(dependencyOffsets) - 1) return 0; *out = dependencies + dependencyOffsets[id]; return dependencyOffsets[id + 1] - dependencyOffsets[id]; diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 03139b19616f..ee42173d0675 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -2575,8 +2575,6 @@ impl StandaloneModuleGraph { } } -/// Allocates a StandaloneModuleGraph in the process-static `INSTANCE`, -/// populates it from bytes, sets it globally, and returns the pointer. /// JSC reads cached bytecode in place and expects its start 128-byte aligned once mapped. The section data begins /// 8 bytes after a page-aligned address (the length header), so the offset must be 120 mod 128. fn append_bytecode_aligned( @@ -2605,6 +2603,9 @@ fn append_bytecode_aligned( } } + +/// Allocates a StandaloneModuleGraph in the process-static `INSTANCE`, +/// populates it from bytes, sets it globally, and returns the pointer. fn from_bytes_alloc( raw_ptr: *mut u8, raw_len: usize, diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 82d222b0fe3f..b1c24e4447d8 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -90,7 +90,7 @@ console.log(JSON.stringify({ n, anonKB: anon }));`, 60_000, ); - test("--bytecode embeds bytecode for the internal modules the app imports", async () => { + test.each([false, true])("--bytecode=%p: internal modules the app imports come from embedded bytecode", async bytecode => { using dir = tempDir("build-compile-builtin-bytecode", { "app.js": `import { join } from "node:path"; import http from "node:http"; @@ -99,29 +99,27 @@ const server = http.createServer(() => {}); console.log(JSON.stringify({ joined: join("a", "b"), fromBytecode: internalModulesLoadedFromBytecode() })); server.close();`, }); - for (const bytecode of [false, true]) { - const outfile = join(dir + "", bytecode ? "app-bytecode" : "app-source"); - const result = await Bun.build({ - entrypoints: [join(dir + "", "app.js")], - compile: { outfile }, - bytecode, - format: "esm", - target: "bun", - }); - expect(result.success).toBe(true); - await using proc = Bun.spawn({ cmd: [outfile], env: bunEnv, stdout: "pipe", stderr: "pipe" }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toBe(""); - const { joined, fromBytecode } = JSON.parse(stdout.trim()); - expect(joined).toBe(join("a", "b")); - if (bytecode) { - // node:path, node:http and what they require at load (node:net, node:events, the stream internals, ...). - expect(fromBytecode).toBeGreaterThan(10); - } else { - expect(fromBytecode).toBe(0); - } - expect(exitCode).toBe(0); + const outfile = join(dir + "", "app"); + const result = await Bun.build({ + entrypoints: [join(dir + "", "app.js")], + compile: { outfile }, + bytecode, + format: "esm", + target: "bun", + }); + expect(result.success).toBe(true); + await using proc = Bun.spawn({ cmd: [outfile], env: bunEnv, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + const { joined, fromBytecode } = JSON.parse(stdout.trim()); + expect(joined).toBe(join("a", "b")); + if (bytecode) { + // node:path, node:http and what they require at load (node:net, node:events, the stream internals, ...). + expect(fromBytecode).toBeGreaterThan(10); + } else { + expect(fromBytecode).toBe(0); } + expect(exitCode).toBe(0); }); test("compile with invalid target fails gracefully", async () => { From edcdf3f7de9ffb229114006445ff22eb43b32e73 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 01:17:28 +0000 Subject: [PATCH 30/44] [autofix.ci] apply automated fixes --- src/standalone_graph/StandaloneModuleGraph.rs | 1 - test/bundler/bun-build-compile.test.ts | 55 ++++++++++--------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index ee42173d0675..55c8abe9224a 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -2603,7 +2603,6 @@ fn append_bytecode_aligned( } } - /// Allocates a StandaloneModuleGraph in the process-static `INSTANCE`, /// populates it from bytes, sets it globally, and returns the pointer. fn from_bytes_alloc( diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index b1c24e4447d8..171238f3016b 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -90,37 +90,40 @@ console.log(JSON.stringify({ n, anonKB: anon }));`, 60_000, ); - test.each([false, true])("--bytecode=%p: internal modules the app imports come from embedded bytecode", async bytecode => { - using dir = tempDir("build-compile-builtin-bytecode", { - "app.js": `import { join } from "node:path"; + test.each([false, true])( + "--bytecode=%p: internal modules the app imports come from embedded bytecode", + async bytecode => { + using dir = tempDir("build-compile-builtin-bytecode", { + "app.js": `import { join } from "node:path"; import http from "node:http"; const { internalModulesLoadedFromBytecode } = require("bun:internal-for-testing"); const server = http.createServer(() => {}); console.log(JSON.stringify({ joined: join("a", "b"), fromBytecode: internalModulesLoadedFromBytecode() })); server.close();`, - }); - const outfile = join(dir + "", "app"); - const result = await Bun.build({ - entrypoints: [join(dir + "", "app.js")], - compile: { outfile }, - bytecode, - format: "esm", - target: "bun", - }); - expect(result.success).toBe(true); - await using proc = Bun.spawn({ cmd: [outfile], env: bunEnv, stdout: "pipe", stderr: "pipe" }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toBe(""); - const { joined, fromBytecode } = JSON.parse(stdout.trim()); - expect(joined).toBe(join("a", "b")); - if (bytecode) { - // node:path, node:http and what they require at load (node:net, node:events, the stream internals, ...). - expect(fromBytecode).toBeGreaterThan(10); - } else { - expect(fromBytecode).toBe(0); - } - expect(exitCode).toBe(0); - }); + }); + const outfile = join(dir + "", "app"); + const result = await Bun.build({ + entrypoints: [join(dir + "", "app.js")], + compile: { outfile }, + bytecode, + format: "esm", + target: "bun", + }); + expect(result.success).toBe(true); + await using proc = Bun.spawn({ cmd: [outfile], env: bunEnv, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + const { joined, fromBytecode } = JSON.parse(stdout.trim()); + expect(joined).toBe(join("a", "b")); + if (bytecode) { + // node:path, node:http and what they require at load (node:net, node:events, the stream internals, ...). + expect(fromBytecode).toBeGreaterThan(10); + } else { + expect(fromBytecode).toBe(0); + } + expect(exitCode).toBe(0); + }, + ); test("compile with invalid target fails gracefully", async () => { using dir = tempDir("build-compile-invalid", { From 0fa8e4bcb90c632d2a68f928e80eff6e639814ae Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 01:23:55 +0000 Subject: [PATCH 31/44] bunVM is at global scope --- src/jsc/bindings/InternalModuleRegistry.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsc/bindings/InternalModuleRegistry.cpp b/src/jsc/bindings/InternalModuleRegistry.cpp index db9fd40b53bb..cf748a352427 100644 --- a/src/jsc/bindings/InternalModuleRegistry.cpp +++ b/src/jsc/bindings/InternalModuleRegistry.cpp @@ -71,7 +71,7 @@ JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, cons UnlinkedFunctionExecutable* executable = nullptr; const uint8_t* cachedBytes = nullptr; size_t cachedSize = 0; - if (Bun__standaloneInternalModuleBytecode(WebCore::bunVM(globalObject), id, &cachedBytes, &cachedSize)) { + if (Bun__standaloneInternalModuleBytecode(::bunVM(globalObject), id, &cachedBytes, &cachedSize)) { Ref cached = JSC::CachedBytecode::create(std::span { const_cast(cachedBytes), cachedSize }, [](const void*) {}, {}); cached->setPayloadIsPersistent(); executable = JSC::decodeBuiltinFunction(vm, WTF::move(cached), *source.provider(), InternalModuleRegistryConstants::sourceStamp); From 0fbe69a4b5941882b599f4fc2e4a9df1e94b4b85 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 02:11:37 +0000 Subject: [PATCH 32/44] Review: skip internal-module bytecode when cross-compiling (the target's builtins are not ours); nits --- src/bundler/LinkerContext.rs | 2 ++ src/bundler/bundle_v2.rs | 1 + src/bundler/linker_context/generateChunksInParallel.rs | 2 +- src/bundler/options.rs | 5 +++++ src/js/internal-for-testing.ts | 4 ++-- src/jsc/bindings/InternalModuleRegistry.cpp | 5 +---- src/runtime/api/js_bundle_completion_task.rs | 1 + src/runtime/cli/build_command.rs | 1 + src/standalone_graph/StandaloneModuleGraph.rs | 9 ++------- test/bundler/bun-build-compile.test.ts | 2 +- 10 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index 04b9ba8c0a83..a79917d6a6ca 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -1249,6 +1249,7 @@ impl From for LinkError { pub struct LinkerOptions { pub(crate) generate_bytecode_cache: bool, + pub(crate) generate_internal_module_bytecode: bool, pub(crate) output_format: Format, pub(crate) ignore_dce_annotations: bool, pub(crate) emit_dce_annotations: bool, @@ -1288,6 +1289,7 @@ impl Default for LinkerOptions { fn default() -> Self { Self { generate_bytecode_cache: false, + generate_internal_module_bytecode: false, output_format: Format::Esm, ignore_dce_annotations: false, emit_dce_annotations: true, diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index c3c3dfb97a83..97faa18bb71d 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -2929,6 +2929,7 @@ pub mod bv2_impl { this.linker.options.target = this.transpiler.options.target; this.linker.options.output_format = this.transpiler.options.output_format; this.linker.options.generate_bytecode_cache = this.transpiler.options.bytecode; + this.linker.options.generate_internal_module_bytecode = this.transpiler.options.bytecode && this.transpiler.options.compile_target_is_host; this.linker.options.compile_mode = this.transpiler.options.compile_mode; this.linker.options.metafile = this.transpiler.options.metafile; // SAFETY: same `'a`-owned `Transpiler` field as `banner` above. diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 24e892205441..7086e1716a54 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -1338,7 +1338,7 @@ pub(crate) fn generate_chunks_in_parallel( } let mut result = output_files.take(); - if c.options.generate_bytecode_cache && c.options.compile_mode.is_executable() { + if c.options.generate_internal_module_bytecode && c.options.compile_mode.is_executable() { append_internal_module_bytecode(c, &mut result); } Ok(result) diff --git a/src/bundler/options.rs b/src/bundler/options.rs index b01ba4cdd85b..1a12624a1dfc 100644 --- a/src/bundler/options.rs +++ b/src/bundler/options.rs @@ -1285,6 +1285,9 @@ pub struct BundleOptions<'a> { pub ignore_dce_annotations: bool, pub emit_dce_annotations: bool, pub bytecode: bool, + /// `--compile --bytecode` for another platform: the executable's internal-module sources (and their bytecode) are that + /// platform's, not this one's, so don't embed bytecode generated from ours. + pub compile_target_is_host: bool, pub code_coverage: bool, pub debugger: bool, @@ -1475,6 +1478,7 @@ impl<'a> BundleOptions<'a> { ignore_dce_annotations: self.ignore_dce_annotations, emit_dce_annotations: self.emit_dce_annotations, bytecode: self.bytecode, + compile_target_is_host: self.compile_target_is_host, code_coverage: self.code_coverage, debugger: self.debugger, compile_mode: self.compile_mode, @@ -1719,6 +1723,7 @@ impl<'a> BundleOptions<'a> { ignore_dce_annotations: false, emit_dce_annotations: false, bytecode: false, + compile_target_is_host: true, code_coverage: false, debugger: false, compile_mode: CompileMode::None, diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 1ac4af7397de..75351c397299 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -784,8 +784,8 @@ export const byteStreamInternals = { ) => void, }; -/// How many internal modules (node:fs etc.) this process created from bytecode embedded by `bun build --compile -/// --bytecode` instead of parsing their source. +// How many internal modules (node:fs etc.) this process created from bytecode embedded by `bun build --compile +// --bytecode` instead of parsing their source. export const internalModulesLoadedFromBytecode: () => number = $newCppFunction( "InternalModuleRegistry.cpp", "jsInternalModulesLoadedFromBytecode", diff --git a/src/jsc/bindings/InternalModuleRegistry.cpp b/src/jsc/bindings/InternalModuleRegistry.cpp index cf748a352427..430ff70adc2a 100644 --- a/src/jsc/bindings/InternalModuleRegistry.cpp +++ b/src/jsc/bindings/InternalModuleRegistry.cpp @@ -147,10 +147,7 @@ static WTF::String internalModuleSourceFromDisk(const WTF::String& moduleName, W JSValue initializeInternalModuleFromDisk(JSGlobalObject* globalObject, VM& vm, const WTF::String& moduleName, WTF::String fileBase, const WTF::String& urlString, uint32_t id) { - { - auto string = internalModuleSourceFromDisk(moduleName, WTF::move(fileBase)); - return generateModule(globalObject, vm, string, moduleName, urlString, id); - } + return generateModule(globalObject, vm, internalModuleSourceFromDisk(moduleName, WTF::move(fileBase)), moduleName, urlString, id); } #define INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, moduleId, filename, OFFSET, LENGTH, urlString, ID) \ return initializeInternalModuleFromDisk(globalObject, vm, moduleId, filename, urlString, ID) diff --git a/src/runtime/api/js_bundle_completion_task.rs b/src/runtime/api/js_bundle_completion_task.rs index 260ce3f6152c..0c79f9857022 100644 --- a/src/runtime/api/js_bundle_completion_task.rs +++ b/src/runtime/api/js_bundle_completion_task.rs @@ -998,6 +998,7 @@ impl CompletionStruct for JSBundleCompletionTask { transpiler.options.output_format = config.format; transpiler.options.bytecode = config.bytecode; + transpiler.options.compile_target_is_host = config.compile.as_ref().map_or(true, |compile| compile.compile_target.is_default()); transpiler.options.compile_mode = if config.compile.is_some() { options::CompileMode::Executable } else { diff --git a/src/runtime/cli/build_command.rs b/src/runtime/cli/build_command.rs index bfb5d622662c..dd1300151e01 100644 --- a/src/runtime/cli/build_command.rs +++ b/src/runtime/cli/build_command.rs @@ -249,6 +249,7 @@ impl BuildCommand { } this_transpiler.options.bytecode = ctx.bundler_options.bytecode; + this_transpiler.options.compile_target_is_host = ctx.bundler_options.compile_target.is_default(); let mut was_renamed_from_index = false; if ctx.bundler_options.compile { diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 55c8abe9224a..29430a4d58f2 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -666,7 +666,7 @@ bitflags::bitflags! { /// After the source hashes: `u32 count`, then `count` × `{ u32 id, StringPointer bytes }` — ahead-of-time /// bytecode for internal modules (InternalModuleRegistry ids), read by InternalModuleRegistry::generateModule. const HAS_BUILTIN_BYTECODE = 1 << 6; - // _padding: u26 + // _padding: u25 } } @@ -762,12 +762,7 @@ impl StandaloneModuleGraph { length: read_u32(record + 8), }; // SAFETY: same provenance rules as `File::bytecode`: a writable subrange JSC may patch in place. - let bytes = unsafe { - core::ptr::slice_from_raw_parts_mut( - raw_ptr.add(pointer.offset as usize), - pointer.length as usize, - ) - }; + let bytes = unsafe { slice_to_mut(raw_ptr, raw_len, pointer) }; builtin_bytecode.push((id, bytes)); } } diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 171238f3016b..b460ffdb8967 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -96,7 +96,7 @@ console.log(JSON.stringify({ n, anonKB: anon }));`, using dir = tempDir("build-compile-builtin-bytecode", { "app.js": `import { join } from "node:path"; import http from "node:http"; -const { internalModulesLoadedFromBytecode } = require("bun:internal-for-testing"); +import { internalModulesLoadedFromBytecode } from "bun:internal-for-testing"; const server = http.createServer(() => {}); console.log(JSON.stringify({ joined: join("a", "b"), fromBytecode: internalModulesLoadedFromBytecode() })); server.close();`, From 69d4338e3d4e398d09eba470fbf7dfbbaae0c9c2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 02:14:31 +0000 Subject: [PATCH 33/44] [autofix.ci] apply automated fixes --- src/bundler/bundle_v2.rs | 3 ++- src/runtime/api/js_bundle_completion_task.rs | 5 ++++- src/runtime/cli/build_command.rs | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 97faa18bb71d..2606fe2ada18 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -2929,7 +2929,8 @@ pub mod bv2_impl { this.linker.options.target = this.transpiler.options.target; this.linker.options.output_format = this.transpiler.options.output_format; this.linker.options.generate_bytecode_cache = this.transpiler.options.bytecode; - this.linker.options.generate_internal_module_bytecode = this.transpiler.options.bytecode && this.transpiler.options.compile_target_is_host; + this.linker.options.generate_internal_module_bytecode = + this.transpiler.options.bytecode && this.transpiler.options.compile_target_is_host; this.linker.options.compile_mode = this.transpiler.options.compile_mode; this.linker.options.metafile = this.transpiler.options.metafile; // SAFETY: same `'a`-owned `Transpiler` field as `banner` above. diff --git a/src/runtime/api/js_bundle_completion_task.rs b/src/runtime/api/js_bundle_completion_task.rs index 0c79f9857022..5ff802011b09 100644 --- a/src/runtime/api/js_bundle_completion_task.rs +++ b/src/runtime/api/js_bundle_completion_task.rs @@ -998,7 +998,10 @@ impl CompletionStruct for JSBundleCompletionTask { transpiler.options.output_format = config.format; transpiler.options.bytecode = config.bytecode; - transpiler.options.compile_target_is_host = config.compile.as_ref().map_or(true, |compile| compile.compile_target.is_default()); + transpiler.options.compile_target_is_host = config + .compile + .as_ref() + .map_or(true, |compile| compile.compile_target.is_default()); transpiler.options.compile_mode = if config.compile.is_some() { options::CompileMode::Executable } else { diff --git a/src/runtime/cli/build_command.rs b/src/runtime/cli/build_command.rs index dd1300151e01..426cb4b6e57b 100644 --- a/src/runtime/cli/build_command.rs +++ b/src/runtime/cli/build_command.rs @@ -249,7 +249,8 @@ impl BuildCommand { } this_transpiler.options.bytecode = ctx.bundler_options.bytecode; - this_transpiler.options.compile_target_is_host = ctx.bundler_options.compile_target.is_default(); + this_transpiler.options.compile_target_is_host = + ctx.bundler_options.compile_target.is_default(); let mut was_renamed_from_index = false; if ctx.bundler_options.compile { From ef7ea14c5432a30e65de9546b19498b555c0b961 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 02:18:09 +0000 Subject: [PATCH 34/44] clippy: is_none_or --- src/runtime/api/js_bundle_completion_task.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/api/js_bundle_completion_task.rs b/src/runtime/api/js_bundle_completion_task.rs index 5ff802011b09..0cde7154cfae 100644 --- a/src/runtime/api/js_bundle_completion_task.rs +++ b/src/runtime/api/js_bundle_completion_task.rs @@ -1001,7 +1001,7 @@ impl CompletionStruct for JSBundleCompletionTask { transpiler.options.compile_target_is_host = config .compile .as_ref() - .map_or(true, |compile| compile.compile_target.is_default()); + .is_none_or(|compile| compile.compile_target.is_default()); transpiler.options.compile_mode = if config.compile.is_some() { options::CompileMode::Executable } else { From cfbc61ab7a055a387b360757a78e984de59df905 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 04:34:36 +0000 Subject: [PATCH 35/44] [autofix.ci] apply automated fixes --- src/jsc/ResolvedSource.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/jsc/ResolvedSource.rs b/src/jsc/ResolvedSource.rs index 0ddd437169a0..b8d600e1a24b 100644 --- a/src/jsc/ResolvedSource.rs +++ b/src/jsc/ResolvedSource.rs @@ -83,7 +83,10 @@ impl Bytecode { /// Borrowed from memory the caller guarantees is never freed or unmapped for the rest of the process /// (the executable's module graph section, NodeCompileCache's retired blobs). pub fn persistent(bytes: &[u8]) -> Self { - Self { persistent: !bytes.is_empty(), ..Self::borrowed(bytes) } + Self { + persistent: !bytes.is_empty(), + ..Self::borrowed(bytes) + } } pub fn owned(bytes: Box<[u8]>) -> Self { if bytes.is_empty() { From 0e1a0d0100617dbf90e2d326ad2570eaa19c5dae Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 22:41:14 -0700 Subject: [PATCH 36/44] Bump WebKit to ab29fdebb46292014bf2db3171fa49ab88d83e0e No-Verification-Needed: version bump only --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 67102caf61f5..967c5179c88f 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "aff53044c546e72cde9ff2c8fbe95dbd401f6c06"; +export const WEBKIT_VERSION = "ab29fdebb46292014bf2db3171fa49ab88d83e0e"; /** * WebKit (JavaScriptCore) — the JS engine. From 708d0e8bb4e225cc44d2721eb768744a870c921f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 07:26:47 +0000 Subject: [PATCH 37/44] Review: compact chunk names get a '_' prefix so they cannot collide with a numerically named entry point; name the InternalModuleRegistry flag --- src/bundler/linker_context/generateChunksInParallel.rs | 2 +- src/jsc/CachedBytecode.rs | 4 ++-- src/jsc/lib.rs | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 1c0c5d7d4480..bfb387db761e 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -398,7 +398,7 @@ pub(crate) fn generate_chunks_in_parallel( { write!( &mut rel_path, - "./{}.{}", + "./_{}.{}", compact_chunk_index, bstr::BStr::new(&chunk.template.placeholder.ext) ) diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index 9d95cc57e0a0..03909aac5833 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -182,8 +182,8 @@ pub(crate) fn __bun_jsc_generate_internal_module_bytecode( None => specifier, }; if let Some(tag) = crate::ResolvedSourceTag::try_from_name(canonical) { - if tag.0 >= 512 { - push(tag.0 - 512, &mut wanted); + if tag.0 >= crate::ResolvedSourceTag::INTERNAL_MODULE_REGISTRY_FLAG { + push(tag.0 - crate::ResolvedSourceTag::INTERNAL_MODULE_REGISTRY_FLAG, &mut wanted); } } } diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index f6eacffff526..b72e011a79b4 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -816,6 +816,8 @@ pub mod resolved_source_tag { #[allow(non_upper_case_globals)] impl ResolvedSourceTag { + /// `InternalModuleRegistryFlag` in SyntheticModuleType.h: builtin-module tags are `(1 << 9) | InternalModuleRegistry id`. + pub const INTERNAL_MODULE_REGISTRY_FLAG: u32 = 1 << 9; // Structural variants — keep in lock-step with the generated // `build/*/codegen/SyntheticModuleType.h` and // `src/jsc/bindings/headers-handwritten.h` (`ResolvedSourceTagPackageJSONTypeModule = 1`). From 75f57b619ed81a7623f129c2794b1b87fed4af07 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 07:29:24 +0000 Subject: [PATCH 38/44] [autofix.ci] apply automated fixes --- src/jsc/CachedBytecode.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index 03909aac5833..41da5c3dacb2 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -183,7 +183,10 @@ pub(crate) fn __bun_jsc_generate_internal_module_bytecode( }; if let Some(tag) = crate::ResolvedSourceTag::try_from_name(canonical) { if tag.0 >= crate::ResolvedSourceTag::INTERNAL_MODULE_REGISTRY_FLAG { - push(tag.0 - crate::ResolvedSourceTag::INTERNAL_MODULE_REGISTRY_FLAG, &mut wanted); + push( + tag.0 - crate::ResolvedSourceTag::INTERNAL_MODULE_REGISTRY_FLAG, + &mut wanted, + ); } } } From 5f9ba2d340246fae44e30fea91aa5f08c1f259ea Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 00:33:19 -0700 Subject: [PATCH 39/44] bun build --compile --bytecode: one shared string table across every chunk's payload Each non-symbol string >=4 chars is a 4-byte ordinal in every chunk's bytecode; the characters live once in a graph section (Flags::HAS_BYTECODE_STRING_TABLE) that DecoderStringTable on JSVMClientData reads with a demand-zero atom slot per ordinal. Also picks up WebKit 120b075b (two-char atom table shared on VM instead of per Decoder). No-Verification-Needed: compile + smoke decode; per maintainer direction --- scripts/build/deps/webkit.ts | 2 +- src/bundler/bundle_v2.rs | 29 +++- src/bundler/lib.rs | 4 + .../generateChunksInParallel.rs | 23 +++ .../linker_context/writeOutputFilesToDisk.rs | 1 + src/jsc/CachedBytecode.rs | 132 ++++++++++-------- src/jsc/NodeCompileCache.rs | 2 +- src/jsc/VirtualMachine.rs | 19 +++ src/jsc/bindings/BunClientData.cpp | 6 + src/jsc/bindings/BunClientData.h | 5 + src/jsc/bindings/ZigSourceProvider.cpp | 30 +++- src/resolver/standalone_module_graph.rs | 4 + src/runtime/bake/production.rs | 4 +- src/runtime/cli/build_command.rs | 7 +- src/standalone_graph/StandaloneModuleGraph.rs | 73 +++++++++- 15 files changed, 268 insertions(+), 73 deletions(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 967c5179c88f..dbc2831d6fbb 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "ab29fdebb46292014bf2db3171fa49ab88d83e0e"; +export const WEBKIT_VERSION = "5803c87d881b2c09bcbe17394869a91a8acfe988"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index d5cdbdec2b88..ebf0a8b7b993 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1398,6 +1398,9 @@ pub mod bv2_impl { } } + /// Opaque `JSC::EncoderStringTable` — one instance shared by every chunk's `encodeCodeBlock` in a `--compile --bytecode` build. + pub(crate) enum EncoderStringTable {} + unsafe extern "Rust" { /// Defined `#[no_mangle]` in `bun_jsc::cached_bytecode`. Generic /// "generate JSC bytecode off the main JS thread" helper — marks the @@ -1409,6 +1412,7 @@ pub mod bv2_impl { format: crate::options_impl::Format, source: &[u8], source_provider_url: &bun_core::String, + external_strings: Option>, ) -> Option>; /// Defined `#[no_mangle]` in `bun_jsc::cached_bytecode`: (registry id, bytecode) for the internal modules named @@ -1417,6 +1421,11 @@ pub mod bv2_impl { specifiers: &[&[u8]], depth: u32, ) -> Vec<(u32, Box<[u8]>)>; + + safe fn __bun_jsc_encoder_string_table_new() -> core::ptr::NonNull; + safe fn __bun_jsc_encoder_string_table_take( + table: core::ptr::NonNull, + ) -> Box<[u8]>; } unsafe extern "Rust" { @@ -1454,8 +1463,26 @@ pub mod bv2_impl { format: crate::options_impl::Format, source: &[u8], source_provider_url: &bun_core::String, + external_strings: Option>, ) -> Option> { - __bun_jsc_generate_cached_bytecode(format, source, source_provider_url) + __bun_jsc_generate_cached_bytecode( + format, + source, + source_provider_url, + external_strings, + ) + } + + #[inline] + pub(crate) fn encoder_string_table_new() -> core::ptr::NonNull { + __bun_jsc_encoder_string_table_new() + } + + #[inline] + pub(crate) fn encoder_string_table_take( + table: core::ptr::NonNull, + ) -> Box<[u8]> { + __bun_jsc_encoder_string_table_take(table) } #[inline] diff --git a/src/bundler/lib.rs b/src/bundler/lib.rs index 8a2f0a80ce98..5b22a82b6e77 100644 --- a/src/bundler/lib.rs +++ b/src/bundler/lib.rs @@ -271,6 +271,9 @@ pub mod options { /// InternalModuleRegistry id in decimal. #[strum(serialize = "builtin-bytecode")] BuiltinBytecode, + /// The one shared string table every chunk's bytecode references by ordinal (`EncoderStringTable::serialize`). + #[strum(serialize = "bytecode-string-table")] + BytecodeStringTable, #[strum(serialize = "metafile-json")] MetafileJson, #[strum(serialize = "metafile-markdown")] @@ -285,6 +288,7 @@ pub mod options { | OutputKind::Bytecode | OutputKind::ModuleInfo | OutputKind::BuiltinBytecode + | OutputKind::BytecodeStringTable | OutputKind::MetafileJson | OutputKind::MetafileMarkdown ) diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index bfb387db761e..6dc37ab06e9a 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -619,6 +619,9 @@ pub(crate) fn generate_chunks_in_parallel( let resolver = c.resolver.expect("resolver set in load()"); let root_path: &[u8] = &resolver.opts.output_dir; let is_standalone = c.options.compile_mode.is_standalone_html(); + let external_string_table = (c.options.generate_bytecode_cache + && c.options.compile_mode.is_executable()) + .then(crate::bundle_v2::dispatch::encoder_string_table_new); let more_than_one_output = !is_standalone && (c.parse_graph().additional_output_files.len() > 0 || c.options.generate_bytecode_cache @@ -1081,6 +1084,7 @@ pub(crate) fn generate_chunks_in_parallel( c.options.output_format, &code_result.buffer, &source_provider_url, + external_string_table, ) { let source_provider_url_str = source_provider_url.to_utf8(); debug!( @@ -1343,6 +1347,25 @@ pub(crate) fn generate_chunks_in_parallel( if c.options.generate_internal_module_bytecode && c.options.compile_mode.is_executable() { append_internal_module_bytecode(c, &mut result); } + if let Some(table) = external_string_table { + let bytes = crate::bundle_v2::dispatch::encoder_string_table_take(table); + debug!("Bytecode external string table: {} bytes", bytes.len()); + result.push(options::OutputFile::init(options::OutputFileInit { + output_path: b".bytecode-strings".to_vec().into_boxed_slice(), + input_path: Box::default(), + input_loader: Loader::File, + hash: None, + output_kind: options::OutputKind::BytecodeStringTable, + loader: Loader::File, + size: Some(bytes.len()), + display_size: bytes.len() as u32, + data: options::OutputFileData::Buffer { data: bytes }, + side: None, + entry_point_index: None, + is_executable: false, + ..Default::default() + })); + } Ok(result) } diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.rs b/src/bundler/linker_context/writeOutputFilesToDisk.rs index 312dc97ce149..8286fc28b90c 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.rs +++ b/src/bundler/linker_context/writeOutputFilesToDisk.rs @@ -405,6 +405,7 @@ pub(crate) fn write_output_files_to_disk( c.options.output_format, &code_result.buffer, &source_provider_url, + None, ) { let source_provider_url_str = source_provider_url.to_utf8(); debug!( diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index 41da5c3dacb2..53c7ee0b9aea 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -8,7 +8,43 @@ bun_opaque::opaque_ffi! { pub struct CachedBytecode; } +bun_opaque::opaque_ffi! { + /// One `JSC::EncoderStringTable` shared by every `encodeCodeBlock` in a `--compile --bytecode` build so ≥4-char strings become 4-byte ordinals in each chunk's payload and their characters are written once by `serialize()`. + pub struct EncoderStringTable; +} + +impl EncoderStringTable { + pub fn new() -> NonNull { + // SAFETY: C++ never returns null from `new`. + unsafe { NonNull::new_unchecked(Bun__EncoderStringTable__create()) } + } + pub fn serialize(this: NonNull) -> Vec { + let mut out = Vec::::new(); + unsafe extern "C" fn append(ctx: *mut core::ffi::c_void, bytes: *const u8, len: usize) { + // SAFETY: `ctx` is `&mut Vec`; `bytes` valid for `len`. + unsafe { + (*(ctx as *mut Vec)).extend_from_slice(core::slice::from_raw_parts(bytes, len)) + }; + } + // SAFETY: `this` is a valid table; callback receives our `&mut out`. + unsafe { Bun__EncoderStringTable__serialize(this.as_ptr(), (&raw mut out).cast(), append) }; + out + } + pub fn destroy(this: NonNull) { + // SAFETY: `this` was produced by `new`. + unsafe { Bun__EncoderStringTable__destroy(this.as_ptr()) }; + } +} + unsafe extern "C" { + fn Bun__EncoderStringTable__create() -> *mut EncoderStringTable; + fn Bun__EncoderStringTable__destroy(this: *mut EncoderStringTable); + fn Bun__EncoderStringTable__serialize( + this: *mut EncoderStringTable, + ctx: *mut core::ffi::c_void, + append: unsafe extern "C" fn(*mut core::ffi::c_void, *const u8, usize), + ); + fn generateCachedModuleByteCodeFromSourceCode( source_provider_url: &BunString, input_code: *const u8, @@ -16,6 +52,7 @@ unsafe extern "C" { output_byte_code: *mut Option>, output_byte_code_size: *mut usize, cached_bytecode: *mut Option>, + external_strings: Option>, ) -> bool; fn generateCachedCommonJSProgramByteCodeFromSourceCode( @@ -25,6 +62,7 @@ unsafe extern "C" { output_byte_code: *mut Option>, output_byte_code_size: *mut usize, cached_bytecode: *mut Option>, + external_strings: Option>, ) -> bool; // safe: `CachedBytecode` is an `opaque_ffi!` ZST handle (`!Freeze` via @@ -48,75 +86,38 @@ impl CachedBytecode { // SAFETY CONTRACT: the returned `&'static [u8]` actually borrows from the // `CachedBytecode` handle and is invalidated when `deref()` is called. Callers own // the handle and must call `deref()` (or drop via `allocator()`) to free. - pub(crate) fn generate_for_esm( - source_provider_url: &BunString, + pub(crate) fn generate( + format: Format, input: &[u8], - ) -> Option<(&'static [u8], NonNull)> { - let mut this: Option> = None; - - let mut input_code_size: usize = 0; - let mut input_code_ptr: Option> = None; - // SAFETY: out-params are valid for write; input slice valid for read. - let ok = unsafe { - generateCachedModuleByteCodeFromSourceCode( - source_provider_url, - input.as_ptr(), - input.len(), - &raw mut input_code_ptr, - &raw mut input_code_size, - &raw mut this, - ) - }; - if ok { - // SAFETY: on success, C++ guarantees both out-params are non-null - // and the slice is valid for `input_code_size` bytes until deref(). - let slice = - unsafe { bun_core::ffi::slice(input_code_ptr.unwrap().as_ptr(), input_code_size) }; - return Some((slice, this.unwrap())); - } - - None - } - - pub(crate) fn generate_for_cjs( source_provider_url: &BunString, - input: &[u8], + external_strings: Option>, ) -> Option<(&'static [u8], NonNull)> { + let f = match format { + Format::Esm => generateCachedModuleByteCodeFromSourceCode, + Format::Cjs => generateCachedCommonJSProgramByteCodeFromSourceCode, + _ => return None, + }; let mut this: Option> = None; - let mut input_code_size: usize = 0; - let mut input_code_ptr: Option> = None; + let mut out_size: usize = 0; + let mut out_ptr: Option> = None; // SAFETY: out-params are valid for write; input slice valid for read. let ok = unsafe { - generateCachedCommonJSProgramByteCodeFromSourceCode( + f( source_provider_url, input.as_ptr(), input.len(), - &raw mut input_code_ptr, - &raw mut input_code_size, + &raw mut out_ptr, + &raw mut out_size, &raw mut this, + external_strings, ) }; - if ok { - // SAFETY: on success, C++ guarantees both out-params are non-null - // and the slice is valid for `input_code_size` bytes until deref(). - let slice = - unsafe { bun_core::ffi::slice(input_code_ptr.unwrap().as_ptr(), input_code_size) }; - return Some((slice, this.unwrap())); - } - - None - } - - pub(crate) fn generate( - format: Format, - input: &[u8], - source_provider_url: &BunString, - ) -> Option<(&'static [u8], NonNull)> { - match format { - Format::Esm => Self::generate_for_esm(source_provider_url, input), - Format::Cjs => Self::generate_for_cjs(source_provider_url, input), - _ => None, + if !ok { + return None; } + // SAFETY: on success both out-params are non-null and the slice lives until `deref()`. + let slice = unsafe { bun_core::ffi::slice(out_ptr.unwrap().as_ptr(), out_size) }; + Some((slice, this.unwrap())) } } @@ -144,10 +145,12 @@ pub(crate) fn __bun_jsc_generate_cached_bytecode( format: Format, source: &[u8], source_provider_url: &BunString, + external_strings: Option>, ) -> Option> { crate::virtual_machine::IS_BUNDLER_THREAD_FOR_BYTECODE_CACHE.set(true); crate::initialize(crate::InitializeOptions::default()); - let (bytes, handle) = CachedBytecode::generate(format, source, source_provider_url)?; + let (bytes, handle) = + CachedBytecode::generate(format, source, source_provider_url, external_strings)?; let owned = Box::<[u8]>::from(bytes); // `handle` was just produced by C++ and is valid until deref; // `CachedBytecode` is an opaque ZST handle so `opaque_mut` is the @@ -156,6 +159,21 @@ pub(crate) fn __bun_jsc_generate_cached_bytecode( Some(owned) } +/// Serialize the shared string table into an owned buffer for the standalone graph, then free the table. +#[unsafe(no_mangle)] +pub(crate) fn __bun_jsc_encoder_string_table_take(table: NonNull) -> Box<[u8]> { + let bytes = EncoderStringTable::serialize(table).into_boxed_slice(); + EncoderStringTable::destroy(table); + bytes +} + +#[unsafe(no_mangle)] +pub(crate) fn __bun_jsc_encoder_string_table_new() -> NonNull { + crate::virtual_machine::IS_BUNDLER_THREAD_FOR_BYTECODE_CACHE.set(true); + crate::initialize(crate::InitializeOptions::default()); + EncoderStringTable::new() +} + /// `bun build --compile --bytecode`: for the builtin module specifiers a bundle imports (e.g. `b"node:fs"`), the /// InternalModuleRegistry ids of those modules and everything they eagerly require, each with bytecode generated the /// way InternalModuleRegistry::generateModule consumes it. Specifiers that are not JS internal modules are skipped. diff --git a/src/jsc/NodeCompileCache.rs b/src/jsc/NodeCompileCache.rs index bb3b749a2d5a..ef4fb8ea84a6 100644 --- a/src/jsc/NodeCompileCache.rs +++ b/src/jsc/NodeCompileCache.rs @@ -907,7 +907,7 @@ fn generate_bytecode(format: Format, code: &[u8], url: &[u8]) -> Option &VM { @@ -3997,6 +4012,7 @@ impl VirtualMachine { // SAFETY: `vm` is the unique live VM on this thread. let vm_ref = unsafe { &mut *vm }; vm_ref.transpiler.resolver.standalone_module_graph = Some(graph); + vm_ref.install_bytecode_string_table(graph); // Avoid reading from tsconfig.json & package.json when in standalone mode vm_ref.transpiler.configure_linker_with_auto_jsx(false); vm_ref.transpiler.resolver.store_fd = false; @@ -4043,6 +4059,9 @@ impl VirtualMachine { // (e.g. a `new Worker("./worker.ts")` entry point inside a compiled // executable) resolve against the real filesystem and fail. vm_ref.transpiler.resolver.standalone_module_graph = opts.graph; + if let Some(graph) = opts.graph { + vm_ref.install_bytecode_string_table(graph); + } vm_ref.hot_reload = worker.hot_reload(); vm_ref.initial_script_execution_context_identifier = worker.execution_context_id() as i32; vm_ref.transpiler.resolver.store_fd = opts.store_fd; diff --git a/src/jsc/bindings/BunClientData.cpp b/src/jsc/bindings/BunClientData.cpp index 1d13a799b1dc..85196739069c 100644 --- a/src/jsc/bindings/BunClientData.cpp +++ b/src/jsc/bindings/BunClientData.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "JSDOMConstructorBase.h" @@ -264,4 +265,9 @@ DOMClientIsoSubspaces::~DOMClientIsoSubspaces() deleteSubspaceTable(this); } +void JSVMClientData::setDecoderStringTable(std::span bytes) +{ + m_decoderStringTable = makeUnique(bytes); +} + } // namespace WebCore diff --git a/src/jsc/bindings/BunClientData.h b/src/jsc/bindings/BunClientData.h index 93ffdcf3e4dc..20227197d57b 100644 --- a/src/jsc/bindings/BunClientData.h +++ b/src/jsc/bindings/BunClientData.h @@ -124,6 +124,7 @@ class HeapSizeAfterLastCollection final : public JSC::HeapObserver { namespace JSC { struct HashTableValue; +class DecoderStringTable; } namespace Bun { @@ -272,8 +273,12 @@ class JSVMClientData : public JSC::VM::ClientData { // after every swap. WTF::UncheckedKeyHashMap> isolationSourceProviderCache; + JSC::DecoderStringTable* decoderStringTable() final { return m_decoderStringTable.get(); } + void setDecoderStringTable(std::span); + private: bool isWebCoreJSClientData() const final { return true; } + std::unique_ptr m_decoderStringTable; // Frees a per-VM `JSHeapData` but leaves the process-wide `useGlobalGC` // singleton alone (it is shared by every VM). On the default `!useGlobalGC` diff --git a/src/jsc/bindings/ZigSourceProvider.cpp b/src/jsc/bindings/ZigSourceProvider.cpp index ae22a108c6df..8b357231e1cf 100644 --- a/src/jsc/bindings/ZigSourceProvider.cpp +++ b/src/jsc/bindings/ZigSourceProvider.cpp @@ -183,7 +183,29 @@ JSC::VM& vmForBytecodeCache() return *vmForBytecodeCache; } -extern "C" bool generateCachedModuleByteCodeFromSourceCode(const BunString* sourceProviderURL, const Latin1Character* inputSourceCode, size_t inputSourceCodeSize, const uint8_t** outputByteCode, size_t* outputByteCodeSize, JSC::CachedBytecode** cachedBytecodePtr) +extern "C" JSC::EncoderStringTable* Bun__EncoderStringTable__create() +{ + return new JSC::EncoderStringTable(); +} + +extern "C" void Bun__EncoderStringTable__destroy(JSC::EncoderStringTable* table) +{ + delete table; +} + +extern "C" void Bun__EncoderStringTable__serialize(JSC::EncoderStringTable* table, void* ctx, void (*append)(void* ctx, const uint8_t* bytes, size_t len)) +{ + Vector bytes = table->serialize(); + append(ctx, bytes.span().data(), bytes.size()); +} + +extern "C" void Bun__DecoderStringTable__install(JSC::VM* vm, const uint8_t* bytes, size_t len) +{ + ASSERT(vm->clientData); + static_cast(vm->clientData)->setDecoderStringTable(std::span(bytes, len)); +} + +extern "C" bool generateCachedModuleByteCodeFromSourceCode(const BunString* sourceProviderURL, const Latin1Character* inputSourceCode, size_t inputSourceCodeSize, const uint8_t** outputByteCode, size_t* outputByteCodeSize, JSC::CachedBytecode** cachedBytecodePtr, JSC::EncoderStringTable* externalStrings) { std::span sourceCodeSpan(inputSourceCode, inputSourceCodeSize); JSC::SourceCode sourceCode = JSC::makeSource(WTF::String(sourceCodeSpan), toSourceOrigin(sourceProviderURL->toWTFString(), false), JSC::SourceTaintedOrigin::Untainted); @@ -206,7 +228,7 @@ extern "C" bool generateCachedModuleByteCodeFromSourceCode(const BunString* sour dataLogLnIf(JSC::Options::verboseDiskCache(), "[Bytecode Build] generateModule url=", sourceProviderURL->toWTFString(), " origin=", sourceCode.provider()->sourceOrigin().url().string(), " sourceSize=", inputSourceCodeSize, " keyHash=", key.hash()); - RefPtr cachedBytecode = JSC::encodeCodeBlock(vm, key, unlinkedCodeBlock); + RefPtr cachedBytecode = JSC::encodeCodeBlock(vm, key, unlinkedCodeBlock, externalStrings); if (!cachedBytecode) return false; @@ -218,7 +240,7 @@ extern "C" bool generateCachedModuleByteCodeFromSourceCode(const BunString* sour return true; } -extern "C" bool generateCachedCommonJSProgramByteCodeFromSourceCode(const BunString* sourceProviderURL, const Latin1Character* inputSourceCode, size_t inputSourceCodeSize, const uint8_t** outputByteCode, size_t* outputByteCodeSize, JSC::CachedBytecode** cachedBytecodePtr) +extern "C" bool generateCachedCommonJSProgramByteCodeFromSourceCode(const BunString* sourceProviderURL, const Latin1Character* inputSourceCode, size_t inputSourceCodeSize, const uint8_t** outputByteCode, size_t* outputByteCodeSize, JSC::CachedBytecode** cachedBytecodePtr, JSC::EncoderStringTable* externalStrings) { std::span sourceCodeSpan(inputSourceCode, inputSourceCodeSize); @@ -241,7 +263,7 @@ extern "C" bool generateCachedCommonJSProgramByteCodeFromSourceCode(const BunStr dataLogLnIf(JSC::Options::verboseDiskCache(), "[Bytecode Build] generateCJS url=", sourceProviderURL->toWTFString(), " origin=", sourceCode.provider()->sourceOrigin().url().string(), " sourceSize=", inputSourceCodeSize, " keyHash=", key.hash()); - RefPtr cachedBytecode = JSC::encodeCodeBlock(vm, key, unlinkedCodeBlock); + RefPtr cachedBytecode = JSC::encodeCodeBlock(vm, key, unlinkedCodeBlock, externalStrings); if (!cachedBytecode) return false; diff --git a/src/resolver/standalone_module_graph.rs b/src/resolver/standalone_module_graph.rs index a0aa48e58534..bdccaaaa0123 100644 --- a/src/resolver/standalone_module_graph.rs +++ b/src/resolver/standalone_module_graph.rs @@ -32,4 +32,8 @@ pub trait StandaloneModuleGraph: Send + Sync { fn builtin_module_bytecode(&self, _id: u32) -> Option<*mut [u8]> { None } + /// The one shared bytecode string table (`JSC::EncoderStringTable::serialize`) every chunk's payload references by ordinal; empty when the executable has none. + fn bytecode_string_table(&self) -> &'static [u8] { + &[] + } } diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index 8443804f6ed5..11c554d90bbe 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -768,7 +768,9 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< OutputKind::Asset => {} OutputKind::Bytecode => {} OutputKind::Sourcemap => {} - OutputKind::ModuleInfo | OutputKind::BuiltinBytecode => {} + OutputKind::ModuleInfo + | OutputKind::BuiltinBytecode + | OutputKind::BytecodeStringTable => {} OutputKind::MetafileJson | OutputKind::MetafileMarkdown => {} } } diff --git a/src/runtime/cli/build_command.rs b/src/runtime/cli/build_command.rs index 426cb4b6e57b..0143e27a96f0 100644 --- a/src/runtime/cli/build_command.rs +++ b/src/runtime/cli/build_command.rs @@ -1092,9 +1092,9 @@ impl BuildCommand { options::OutputKind::Asset => "", options::OutputKind::Sourcemap => "", options::OutputKind::Bytecode => "", - options::OutputKind::ModuleInfo | options::OutputKind::BuiltinBytecode => { - "" - } + options::OutputKind::ModuleInfo + | options::OutputKind::BuiltinBytecode + | options::OutputKind::BytecodeStringTable => "", options::OutputKind::MetafileJson | options::OutputKind::MetafileMarkdown => "", }))?; @@ -1141,6 +1141,7 @@ impl BuildCommand { options::OutputKind::Bytecode => "bytecode", options::OutputKind::ModuleInfo => "module info", options::OutputKind::BuiltinBytecode => "builtin bytecode", + options::OutputKind::BytecodeStringTable => "bytecode strings", options::OutputKind::MetafileJson => "metafile json", options::OutputKind::MetafileMarkdown => "metafile markdown", } diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 49435a6936ed..40af3323e895 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -47,6 +47,8 @@ pub struct StandaloneModuleGraph { pub flags: Flags, /// InternalModuleRegistry id → its bytecode inside `bytes` (JSC reads it in place; see `File::bytecode`). pub builtin_bytecode: Vec<(u32, *mut [u8])>, + /// The one shared bytecode string table (`JSC::EncoderStringTable::serialize`) every chunk's payload references by ordinal; installed on the VM's `DecoderStringTable` at startup. + pub bytecode_string_table: &'static [u8], } // We never want to hit the filesystem for these files @@ -321,6 +323,9 @@ impl bun_resolver::StandaloneModuleGraph for StandaloneModuleGraph { fn builtin_module_bytecode(&self, id: u32) -> Option<*mut [u8]> { StandaloneModuleGraph::builtin_module_bytecode(self, id) } + fn bytecode_string_table(&self) -> &'static [u8] { + self.bytecode_string_table + } } #[repr(C)] @@ -666,7 +671,9 @@ bitflags::bitflags! { /// After the source hashes: `u32 count`, then `count` × `{ u32 id, StringPointer bytes }` — ahead-of-time /// bytecode for internal modules (InternalModuleRegistry ids), read by InternalModuleRegistry::generateModule. const HAS_BUILTIN_BYTECODE = 1 << 6; - // _padding: u25 + /// After the builtin-bytecode table: one `StringPointer` to the shared bytecode string table (`JSC::EncoderStringTable::serialize`), which every chunk's payload references by ordinal. + const HAS_BYTECODE_STRING_TABLE = 1 << 7; + // _padding: u24 } } @@ -696,6 +703,7 @@ impl StandaloneModuleGraph { compile_exec_argv: b"", flags: Flags::default(), builtin_bytecode: Vec::new(), + bytecode_string_table: &[], }); } @@ -767,6 +775,40 @@ impl StandaloneModuleGraph { } } + let bytecode_string_table: &'static [u8] = + if offsets.flags.contains(Flags::HAS_BYTECODE_STRING_TABLE) { + let builtin_count = if offsets.flags.contains(Flags::HAS_BUILTIN_BYTECODE) { + builtin_bytecode.len() + } else { + 0 + }; + let record_at = offsets.modules_ptr.offset as usize + + offsets.modules_ptr.length as usize + + if source_hashes.is_some() { + modules_list_count * size_of::() + } else { + 0 + } + + if offsets.flags.contains(Flags::HAS_BUILTIN_BYTECODE) { + size_of::() + builtin_count * 3 * size_of::() + } else { + 0 + }; + let read_u32 = |at: usize| -> u32 { + debug_assert!(at + 4 <= raw_len); + // SAFETY: `to_bytes` wrote a `StringPointer` right here; unaligned-safe read within `[0, raw_len)`. + unsafe { core::ptr::read_unaligned(raw_const.add(at).cast::()) } + }; + let ptr = StringPointer { + offset: read_u32(record_at), + length: read_u32(record_at + 4), + }; + // SAFETY: `to_bytes` placed the serialized table via `append_bytecode_aligned` into a read-only, disjoint subrange. + unsafe { slice_to(raw_const, raw_len, ptr) } + } else { + &[] + }; + let mut modules = StringArrayHashMap::::new(); modules.reserve(modules_list_count); for i in 0..modules_list_count { @@ -868,6 +910,7 @@ impl StandaloneModuleGraph { .as_bytes(), flags: offsets.flags, builtin_bytecode, + bytecode_string_table, }) } @@ -1020,6 +1063,7 @@ pub(crate) fn to_bytes( string_builder.cap += bytes.len() * 2; } else if output_file.output_kind == options::OutputKind::Bytecode || output_file.output_kind == options::OutputKind::BuiltinBytecode + || output_file.output_kind == options::OutputKind::BytecodeStringTable { // Allocate up to 256 byte alignment for bytecode (+ a table record for builtin bytecode) string_builder.cap += bytes.len().div_ceil(256) * 256 + 256 + 16; @@ -1244,6 +1288,17 @@ pub(crate) fn to_bytes( builtin_bytecode_table[0..4].copy_from_slice(&count.to_le_bytes()); } + let mut bytecode_string_table_ptr = StringPointer::default(); + for output_file in output_files { + if output_file.output_kind != options::OutputKind::BytecodeStringTable { + continue; + } + let options::OutputFileValue::Buffer { bytes } = &output_file.value else { + continue; + }; + bytecode_string_table_ptr = append_bytecode_aligned(&mut string_builder, bytes); + } + // Region layout after the bytecode/module_info run above: source maps // (unread until an error prints), then every file's source text as one run // (`Flags::SOURCE_TEXT_CONTIGUOUS`, so `hint_source_pages_dont_need` can @@ -1319,15 +1374,23 @@ pub(crate) fn to_bytes( builtin_table_ptr.offset, hashes_ptr.offset + hashes_ptr.length ); + let mut flags = flags + | Flags::SOURCE_TEXT_CONTIGUOUS + | Flags::HAS_SOURCE_HASHES + | Flags::HAS_BUILTIN_BYTECODE; + if bytecode_string_table_ptr.length != 0 { + let mut record = [0u8; 8]; + record[0..4].copy_from_slice(&bytecode_string_table_ptr.offset.to_le_bytes()); + record[4..8].copy_from_slice(&bytecode_string_table_ptr.length.to_le_bytes()); + let _ = string_builder.append_count(&record); + flags |= Flags::HAS_BYTECODE_STRING_TABLE; + } let offsets = Offsets { entry_point_id: entry_point_id as u32, modules_ptr, compile_exec_argv_ptr: string_builder.append_count_z(compile_exec_argv), byte_count: string_builder.len, - flags: flags - | Flags::SOURCE_TEXT_CONTIGUOUS - | Flags::HAS_SOURCE_HASHES - | Flags::HAS_BUILTIN_BYTECODE, + flags, }; // SAFETY: `Offsets` is `#[repr(C)]` POD; same `modules_as_bytes` rationale as above. From 60b62b3644ce4048278f75cc0a5b47484f8acc00 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 07:40:56 +0000 Subject: [PATCH 40/44] clippy: ptr cast --- src/jsc/CachedBytecode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index 53c7ee0b9aea..14df70ed9e26 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -23,7 +23,7 @@ impl EncoderStringTable { unsafe extern "C" fn append(ctx: *mut core::ffi::c_void, bytes: *const u8, len: usize) { // SAFETY: `ctx` is `&mut Vec`; `bytes` valid for `len`. unsafe { - (*(ctx as *mut Vec)).extend_from_slice(core::slice::from_raw_parts(bytes, len)) + (*ctx.cast::>()).extend_from_slice(core::slice::from_raw_parts(bytes, len)) }; } // SAFETY: `this` is a valid table; callback receives our `&mut out`. From 245d0ba78b371744676e6be14727f2af1d017ad7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 08:00:20 +0000 Subject: [PATCH 41/44] Review: own the EncoderStringTable with a Drop guard so an early return frees it; hoist the shared read_u32; fix split doc comment --- src/bundler/bundle_v2.rs | 30 ++++++++++++++----- .../generateChunksInParallel.rs | 6 ++-- src/jsc/VirtualMachine.rs | 3 +- src/standalone_graph/StandaloneModuleGraph.rs | 15 ++++------ 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index ebf0a8b7b993..9bfe771f5d6a 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1473,16 +1473,30 @@ pub mod bv2_impl { ) } - #[inline] - pub(crate) fn encoder_string_table_new() -> core::ptr::NonNull { - __bun_jsc_encoder_string_table_new() + /// Owns a `JSC::EncoderStringTable` for one link; `take()` serializes and frees it, `Drop` frees it on early return. + pub(crate) struct EncoderStringTableHandle(Option>); + + impl EncoderStringTableHandle { + #[inline] + pub(crate) fn new() -> Self { + Self(Some(__bun_jsc_encoder_string_table_new())) + } + #[inline] + pub(crate) fn get(&self) -> Option> { + self.0 + } + #[inline] + pub(crate) fn take(mut self) -> Box<[u8]> { + __bun_jsc_encoder_string_table_take(self.0.take().expect("taken once")) + } } - #[inline] - pub(crate) fn encoder_string_table_take( - table: core::ptr::NonNull, - ) -> Box<[u8]> { - __bun_jsc_encoder_string_table_take(table) + impl Drop for EncoderStringTableHandle { + fn drop(&mut self) { + if let Some(table) = self.0.take() { + drop(__bun_jsc_encoder_string_table_take(table)); + } + } } #[inline] diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 6dc37ab06e9a..6cf7f284a6c5 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -621,7 +621,7 @@ pub(crate) fn generate_chunks_in_parallel( let is_standalone = c.options.compile_mode.is_standalone_html(); let external_string_table = (c.options.generate_bytecode_cache && c.options.compile_mode.is_executable()) - .then(crate::bundle_v2::dispatch::encoder_string_table_new); + .then(crate::bundle_v2::dispatch::EncoderStringTableHandle::new); let more_than_one_output = !is_standalone && (c.parse_graph().additional_output_files.len() > 0 || c.options.generate_bytecode_cache @@ -1084,7 +1084,7 @@ pub(crate) fn generate_chunks_in_parallel( c.options.output_format, &code_result.buffer, &source_provider_url, - external_string_table, + external_string_table.as_ref().and_then(|table| table.get()), ) { let source_provider_url_str = source_provider_url.to_utf8(); debug!( @@ -1348,7 +1348,7 @@ pub(crate) fn generate_chunks_in_parallel( append_internal_module_bytecode(c, &mut result); } if let Some(table) = external_string_table { - let bytes = crate::bundle_v2::dispatch::encoder_string_table_take(table); + let bytes = table.take(); debug!("Bytecode external string table: {} bytes", bytes.len()); result.push(options::OutputFile::init(options::OutputFileInit { output_path: b".bytecode-strings".to_vec().into_boxed_slice(), diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 4e0ed4393010..e95e97aacbe6 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -948,7 +948,7 @@ impl VirtualMachine { self.event_loop_mut() } - /// Safe `&VM` accessor for the JSC VM owned by this Bun VM. Set once in + /// Hand the executable's shared bytecode string table (if any) to JSC as this VM's `DecoderStringTable`. fn install_bytecode_string_table( &self, graph: &'static dyn bun_resolver::StandaloneModuleGraph, @@ -964,6 +964,7 @@ impl VirtualMachine { unsafe { Bun__DecoderStringTable__install(self.jsc_vm, table.as_ptr(), table.len()) }; } + /// Safe `&VM` accessor for the JSC VM owned by this Bun VM. Set once in /// `init()` and live for the VM lifetime. #[inline(always)] pub fn jsc_vm(&self) -> &VM { diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 40af3323e895..1ec1dee56e3e 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -746,6 +746,11 @@ impl StandaloneModuleGraph { return Err(crate::Error::CorruptedModuleGraphEntryPointIDIsGreaterThanModuleListCount); } + let read_u32 = |at: usize| -> u32 { + debug_assert!(at + 4 <= raw_len); + // SAFETY: callers pass offsets of records `to_bytes` wrote inside `[0, raw_len)`; unaligned-safe read. + unsafe { core::ptr::read_unaligned(raw_const.add(at).cast::()) } + }; let mut builtin_bytecode: Vec<(u32, *mut [u8])> = Vec::new(); if offsets.flags.contains(Flags::HAS_BUILTIN_BYTECODE) { let table_offset = offsets.modules_ptr.offset as usize @@ -755,11 +760,6 @@ impl StandaloneModuleGraph { } else { 0 }; - let read_u32 = |at: usize| -> u32 { - debug_assert!(at + 4 <= raw_len); - // SAFETY: `to_bytes` wrote `u32 count` + `count` records right here; read-only, unaligned-safe read within `[0, raw_len)`. - unsafe { core::ptr::read_unaligned(raw_const.add(at).cast::()) } - }; let count = read_u32(table_offset) as usize; builtin_bytecode.reserve(count); for i in 0..count { @@ -794,11 +794,6 @@ impl StandaloneModuleGraph { } else { 0 }; - let read_u32 = |at: usize| -> u32 { - debug_assert!(at + 4 <= raw_len); - // SAFETY: `to_bytes` wrote a `StringPointer` right here; unaligned-safe read within `[0, raw_len)`. - unsafe { core::ptr::read_unaligned(raw_const.add(at).cast::()) } - }; let ptr = StringPointer { offset: read_u32(record_at), length: read_u32(record_at + 4), From 511a3f68fafca217f0f0535a989488ed6e3a6168 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 08:17:18 +0000 Subject: [PATCH 42/44] ci: retrigger (WebKit 5803c87d release is published) From c71874829167af4f62dcfc7785282506534780c1 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 02:20:40 -0700 Subject: [PATCH 43/44] Internal-module bytecode shares the build's EncoderStringTable; bump WebKit to 44aa80e07418 encodeBuiltinFunction now takes the same session table, so node:*/bun:*/internal/* modules and every user chunk write their >=4-char strings into one shared table. No-Verification-Needed: compile + smoke decode; per maintainer direction --- scripts/build/deps/webkit.ts | 2 +- src/bundler/bundle_v2.rs | 4 +++- .../generateChunksInParallel.rs | 20 ++++++++++++++----- src/jsc/CachedBytecode.rs | 3 +++ src/jsc/bindings/InternalModuleRegistry.cpp | 4 ++-- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index dbc2831d6fbb..27d968fdff4f 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "5803c87d881b2c09bcbe17394869a91a8acfe988"; +export const WEBKIT_VERSION = "44aa80e07418a688ce545df05567fb0d18fd71fe"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 9bfe771f5d6a..d0f11093d9dd 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1420,6 +1420,7 @@ pub mod bv2_impl { safe fn __bun_jsc_generate_internal_module_bytecode( specifiers: &[&[u8]], depth: u32, + external_strings: Option>, ) -> Vec<(u32, Box<[u8]>)>; safe fn __bun_jsc_encoder_string_table_new() -> core::ptr::NonNull; @@ -1503,8 +1504,9 @@ pub mod bv2_impl { pub(crate) fn generate_internal_module_bytecode( specifiers: &[&[u8]], depth: u32, + external_strings: Option>, ) -> Vec<(u32, Box<[u8]>)> { - __bun_jsc_generate_internal_module_bytecode(specifiers, depth) + __bun_jsc_generate_internal_module_bytecode(specifiers, depth, external_strings) } /// CYCLEBREAK GENUINE: `JSBundleCompletionTask` — the diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 6cf7f284a6c5..27edcbc3d879 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -1345,7 +1345,11 @@ pub(crate) fn generate_chunks_in_parallel( let mut result = output_files.take(); if c.options.generate_internal_module_bytecode && c.options.compile_mode.is_executable() { - append_internal_module_bytecode(c, &mut result); + append_internal_module_bytecode( + c, + &mut result, + external_string_table.as_ref().and_then(|t| t.get()), + ); } if let Some(table) = external_string_table { let bytes = table.take(); @@ -1372,7 +1376,11 @@ pub(crate) fn generate_chunks_in_parallel( /// `--compile --bytecode`: the executable also carries ahead-of-time bytecode for the internal modules (node:fs, …) the /// bundle imports, so their first `require` decodes instead of parsing. One `OutputKind::BuiltinBytecode` per module; /// StandaloneModuleGraph::to_bytes lays them out and InternalModuleRegistry picks them up by id. -fn append_internal_module_bytecode(c: &LinkerContext, output_files: &mut Vec) { +fn append_internal_module_bytecode( + c: &LinkerContext, + output_files: &mut Vec, + external_strings: Option>, +) { let import_records = c.graph.ast.items_import_records(); let mut specifiers: Vec<&[u8]> = Vec::new(); for source_index in &c.graph.reachable_files { @@ -1400,9 +1408,11 @@ fn append_internal_module_bytecode(c: &LinkerContext, output_files: &mut Vec>, output_byte_code_size: *mut usize, cached_bytecode: *mut Option>, + external_strings: Option>, ) -> bool; /// InternalModuleRegistry.cpp: the internal JS modules `id` statically requires. fn Bun__internalModuleDependencies(id: u32, out: *mut *const u16) -> usize; @@ -182,6 +183,7 @@ pub(crate) fn __bun_jsc_encoder_string_table_new() -> NonNull>, ) -> Vec<(u32, Box<[u8]>)> { crate::virtual_machine::IS_BUNDLER_THREAD_FOR_BYTECODE_CACHE.set(true); crate::initialize(crate::InitializeOptions::default()); @@ -235,6 +237,7 @@ pub(crate) fn __bun_jsc_generate_internal_module_bytecode( &raw mut bytes, &raw mut size, &raw mut handle, + external_strings, ) } { continue; diff --git a/src/jsc/bindings/InternalModuleRegistry.cpp b/src/jsc/bindings/InternalModuleRegistry.cpp index e630d7a8ca62..61f8b071dedc 100644 --- a/src/jsc/bindings/InternalModuleRegistry.cpp +++ b/src/jsc/bindings/InternalModuleRegistry.cpp @@ -247,7 +247,7 @@ JSC::VM& vmForBytecodeCache(); // bun build --compile: bytecode for internal JS module `id` (an index below BUN_NATIVE_MODULE_START_INDEX), generated the // way generateModule() will consume it. The caller owns *handle and releases it with CachedBytecode__deref. // `depth`: how many levels of nested functions get code blocks too (UINT32_MAX = all; 0 = only the module wrapper's own). -extern "C" bool Bun__generateInternalModuleBytecode(uint32_t id, uint32_t depth, const uint8_t** bytes, size_t* size, JSC::CachedBytecode** handle) +extern "C" bool Bun__generateInternalModuleBytecode(uint32_t id, uint32_t depth, const uint8_t** bytes, size_t* size, JSC::CachedBytecode** handle, JSC::EncoderStringTable* externalStrings) { using namespace Bun; if (id >= std::size(internalJSModules)) @@ -266,7 +266,7 @@ extern "C" bool Bun__generateInternalModuleBytecode(uint32_t id, uint32_t depth, JSC::recursivelyGenerateUnlinkedCodeBlocksForFunction(vm, executable, source, error, depth); if (error.isValid()) return false; - RefPtr result = JSC::encodeBuiltinFunction(vm, executable, source.length(), InternalModuleRegistryConstants::sourceStamp); + RefPtr result = JSC::encodeBuiltinFunction(vm, executable, source.length(), InternalModuleRegistryConstants::sourceStamp, externalStrings); if (!result) return false; result->ref(); From e739e5d26f34ac34ee5e08675b3ba288c1769c4c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 02:27:40 -0700 Subject: [PATCH 44/44] Bump WebKit to c148a12dd82b (skip per-region checksums for persistent payloads) No-Verification-Needed: version bump only --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 27d968fdff4f..b7b11f222c9b 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "44aa80e07418a688ce545df05567fb0d18fd71fe"; +export const WEBKIT_VERSION = "c148a12dd82b9d88ea81d9d93840194f56490a61"; /** * WebKit (JavaScriptCore) — the JS engine.