Skip to content

JSC: add a bytecode cache entry for UnlinkedFunctionExecutable - #270

Open
robobun wants to merge 1 commit into
mainfrom
farm/c842455a/builtin-bytecode-cache-entry
Open

JSC: add a bytecode cache entry for UnlinkedFunctionExecutable#270
robobun wants to merge 1 commit into
mainfrom
farm/c842455a/builtin-bytecode-cache-entry

Conversation

@robobun

@robobun robobun commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Needed so Bun can ship a bytecode cache for its JS builtins (node:net, node:fs, the internal/* modules, …). Those are the last big chunk of parse work left in a bun build --compile --bytecode binary's startup, and today they are uncacheable.

Why they are uncacheable

The bytecode cache has top-level entries for programs, modules and eval, because those are the top-level compilation units of a script. A builtin's top-level unit is a function: createBuiltinExecutable() produces an UnlinkedFunctionExecutable whose code block is generated lazily on first call. There is no cache entry shaped like that.

encodeFunctionCodeBlock() exists, but it emits a bare CachedFunctionCodeBlock with no GenericCacheEntry header (no version stamp, no key), and the only thing that can attach a decoded code block back onto an executable is CachedFunctionExecutable, which is private to CachedTypes.cpp.

Parsing the builtin as a program instead is not a workaround: BytecodeGenerator's ProgramNode constructor hardcodes m_isBuiltinFunction(false), so every nested function comes back as a normal executable, and the first one that needs a re-parse (a CodeForConstruct that was never cached, anything cleared by deleteAllUnlinkedCodeBlocks) hits SyntaxError: Invalid character '@'.

What this adds

  • CachedCodeBlockTag::CachedFunctionExecutableTag and FunctionExecutableCacheEntry, a GenericCacheEntry holding a SourceCodeKey plus a CachedPtr<CachedFunctionExecutable>.
  • encodeFunctionExecutable() / decodeFunctionExecutable(), mirroring encodeCodeBlock() / decodeCodeBlock().
  • SourceCodeType::FunctionType already existed but was unreachable in tagFromSourceCodeType(); it now maps to the new tag, so isCachedBytecodeStillValid() covers these entries.
  • recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable() — compiles an executable and everything nested beneath it, which is what makes the tree serializable. It's the function-shaped sibling of recursivelyGenerateUnlinkedCodeBlockForProgram().
  • sourceCodeKeyForSerializedFunctionExecutable() — builds the matching key.

Safety

Everything is additive: no existing entry changes shape, no existing code path changes behavior, and the three tag switches gain one arm each.

CachedFunctionExecutable::decode() already restores m_isBuiltinFunction, so a decoded builtin is still a builtin. An entry that is stale, version-mismatched, key-mismatched, or missing a nested code block just falls back to parsing in builtin mode, exactly as today. That's the property the Program-shaped approach can't give you.

Verification

Built jsc (Debug, JSCOnly, USE_BUN_JSC_ADDITIONS=ON) and linked it into a debug Bun. The four new symbols resolve:

T JSC::encodeFunctionExecutable(JSC::VM&, JSC::SourceCodeKey const&, JSC::UnlinkedFunctionExecutable const*, JSC::BytecodeCacheError&)
T JSC::decodeFunctionExecutable(JSC::VM&, JSC::SourceCodeKey const&, WTF::Ref<JSC::CachedBytecode, ...>)
T JSC::recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable(JSC::VM&, JSC::UnlinkedFunctionExecutable*, JSC::SourceCode const&, JSC::ParserError&)
T JSC::sourceCodeKeyForSerializedFunctionExecutable(JSC::VM&, JSC::SourceCode const&, WTF::String const&)

The round-trip (generate a builtin's cache entry, decode it into a fresh executable, run the module) is exercised from Bun's side; a Bun PR consuming this follows once this lands in a release.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Limit details: You’ve used all 5 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9cef841d-c4ca-4deb-86d7-049cbc8ad34b

📥 Commits

Reviewing files that changed from the base of the PR and between 0cbb4a1 and 02b1d0a.

📒 Files selected for processing (4)
  • Source/JavaScriptCore/runtime/CachedTypes.cpp
  • Source/JavaScriptCore/runtime/CachedTypes.h
  • Source/JavaScriptCore/runtime/CodeCache.cpp
  • Source/JavaScriptCore/runtime/CodeCache.h

Walkthrough

This PR adds a dedicated bytecode cache entry for UnlinkedFunctionExecutable trees, including cache tag dispatch, exported encode/decode APIs, source-key construction, and recursive generation of nested unlinked code blocks.

Changes

Function executable cache entry

Layer / File(s) Summary
New cache tag and dispatch routing
Source/JavaScriptCore/runtime/CachedTypes.cpp
Adds CachedFunctionExecutableTag, maps FunctionType to it, and routes decoding and validity checks through the function-executable cache entry.
FunctionExecutableCacheEntry implementation
Source/JavaScriptCore/runtime/CachedTypes.cpp
Encodes and decodes (SourceCodeKey, UnlinkedFunctionExecutable*) pairs and validates decoded keys.
Public encode/decode APIs
Source/JavaScriptCore/runtime/CachedTypes.cpp, Source/JavaScriptCore/runtime/CachedTypes.h
Adds exported functions for encoding and decoding function executables, including stale or mismatched-entry handling.
CodeCache key and recursive generation support
Source/JavaScriptCore/runtime/CodeCache.cpp, Source/JavaScriptCore/runtime/CodeCache.h
Adds serialized-function source-key construction and recursive generation of unlinked code blocks for nested functions.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: adding a bytecode cache entry for UnlinkedFunctionExecutable.
Description check ✅ Passed The description matches the changeset and explains the new cache entry, validation, and fallback behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Source/JavaScriptCore/runtime/CachedTypes.cpp (1)

2253-2274: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

New Bun-motivated cache format is unguarded by USE(BUN_JSC_ADDITIONS).

This entire feature (CachedFunctionExecutableTag, the tagFromSourceCodeType case, FunctionExecutableCacheEntry, and encodeFunctionExecutable/decodeFunctionExecutable) exists purely so Bun can cache bytecode for builtins — it has no consumer in stock JSC. Elsewhere in this same file, comparable Bun-specific behavior (CachedStringSourceProvider's source-reuse optimization) is wrapped in #if USE(BUN_JSC_ADDITIONS). This new addition doesn't follow that convention anywhere in the diff.

Consider wrapping the new tag, dispatch cases, FunctionExecutableCacheEntry, and the encodeFunctionExecutable/decodeFunctionExecutable free functions (also declared in CachedTypes.h) in the same guard for consistency and easier upstream rebasing.

As per coding guidelines, "Guard Bun-specific features with USE(BUN_JSC_ADDITIONS) and event-loop integration with USE(BUN_EVENT_LOOP)" for Source/JavaScriptCore/**/*.{cpp,h}.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Source/JavaScriptCore/runtime/CachedTypes.cpp` around lines 2253 - 2274, The
new Bun-only bytecode cache support is currently unguarded, so wrap the
`CachedFunctionExecutableTag` addition, the `tagFromSourceCodeType`
`FunctionType` dispatch, `FunctionExecutableCacheEntry`, and the
`encodeFunctionExecutable`/`decodeFunctionExecutable` declarations and
definitions in `USE(BUN_JSC_ADDITIONS)` just like the existing Bun-specific
`CachedStringSourceProvider` logic. Keep the guard consistent across
`CachedTypes.cpp` and `CachedTypes.h` so stock JSC does not compile or expose
the Bun-only cache format.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Source/JavaScriptCore/runtime/CachedTypes.cpp`:
- Around line 2253-2274: The new Bun-only bytecode cache support is currently
unguarded, so wrap the `CachedFunctionExecutableTag` addition, the
`tagFromSourceCodeType` `FunctionType` dispatch, `FunctionExecutableCacheEntry`,
and the `encodeFunctionExecutable`/`decodeFunctionExecutable` declarations and
definitions in `USE(BUN_JSC_ADDITIONS)` just like the existing Bun-specific
`CachedStringSourceProvider` logic. Keep the guard consistent across
`CachedTypes.cpp` and `CachedTypes.h` so stock JSC does not compile or expose
the Bun-only cache format.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ea70c59a-ed76-4280-939e-313c43fe7497

📥 Commits

Reviewing files that changed from the base of the PR and between 2a51dee and d87c5e8.

📒 Files selected for processing (4)
  • Source/JavaScriptCore/runtime/CachedTypes.cpp
  • Source/JavaScriptCore/runtime/CachedTypes.h
  • Source/JavaScriptCore/runtime/CodeCache.cpp
  • Source/JavaScriptCore/runtime/CodeCache.h

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I didn't find any issues and the implementation closely mirrors the existing CacheEntry/decodeCodeBlockImpl patterns, but this adds a new top-level serialization format to JSC's bytecode cache with no in-tree tests, so it's worth a human familiar with CachedTypes giving it a look.

Extended reasoning...

Overview

This PR adds a new top-level bytecode cache entry type for UnlinkedFunctionExecutable, so Bun can precompile and cache its JS builtins (whose top-level compilation unit is a builtin function rather than a program/module). It touches four files in Source/JavaScriptCore/runtime/: a new CachedFunctionExecutableTag enum value and FunctionExecutableCacheEntry class in CachedTypes.cpp, exported encodeFunctionExecutable/decodeFunctionExecutable entry points, a recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable helper in CodeCache.cpp, and a sourceCodeKeyForSerializedFunctionExecutable key builder. Roughly 130 net new lines.

Security risks

Bytecode cache decoding is inherently security-relevant since it interprets a serialized binary blob. The new decodeFunctionExecutable follows the same defensive shape as decodeCodeBlockImpl: it checks a minimum size, goes through GenericCacheEntry::isUpToDate (version stamp), verifies the tag before bit_casting to the derived type, decodes under DeferGC, and rejects on SourceCodeKey mismatch. In Bun's intended use the blobs are self-generated at build time (trusted), and a mismatch falls back to parsing from source. I don't see a new attack surface beyond what the existing program/module cache already exposes, but this is exactly the kind of code where a second pair of eyes on layout/alignment and the bit_cast dispatch is valuable.

Level of scrutiny

Medium-high. The change is additive and pattern-matched against existing CacheEntry<T> machinery (including the static_assert(alignof(...) <= alignof(std::max_align_t))), and the three tag switches each gain a well-behaved arm. However, it introduces a new persisted binary format in core JSC, relies on std::bit_cast between base/derived cache-entry pointers, and wires SourceCodeType::FunctionType into tagFromSourceCodeType where it previously fell through to ASSERT_NOT_REACHED. That's more than a mechanical or config-level change.

Other factors

  • No in-tree JSC tests exercise the new encode/decode path; verification is described as happening on the Bun side in a follow-up PR.
  • No prior reviewer comments on the PR to consider.
  • The bug-hunting pass found nothing.

Given the scope (new serialization format in a core subsystem) and lack of in-tree coverage, I'm deferring rather than auto-approving.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
02b1d0a1 autobuild-preview-pr-270-02b1d0a1 2026-08-16 20:11:38 UTC
ce6f2091 autobuild-preview-pr-270-ce6f2091 2026-08-14 22:32:26 UTC
321befdd autobuild-preview-pr-270-321befdd 2026-07-14 13:13:37 UTC
d87c5e88 autobuild-preview-pr-270-d87c5e88 2026-07-03 21:46:02 UTC

@robobun

robobun commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

This duplicates #177 by @sosukesuzuki, which I found only after opening it. Same four files, same idea, arrived at independently. I'm happy to close this in favour of that one.

Before that, the one place they diverge, in case it's worth folding back into #177.

#177's BuiltinFunctionCacheEntry validates only computeJSCBytecodeCacheVersion() and deliberately skips the SourceCodeKey: "builtin sources are fixed at build time." That's true when the bytecode is baked into the same binary that generated it, but the consumer for both of these is bun build --compile --bytecode, where the Bun doing the generating and the Bun the bytecode is embedded inside are not necessarily the same build. The bun-side PRs skip cross-compiles, but --compile-executable-path isn't covered by that check: point it at another Bun whose src/js bundle differs and a version-only check accepts the entry and runs bytecode compiled from different source.

This PR makes the entry a real GenericCacheEntry carrying a CachedSourceCodeKey, so the source hash is part of validation. A mismatch is rejected and the builtin falls back to parsing, which is the behaviour every other cache entry type already has. It also maps SourceCodeType::FunctionType in tagFromSourceCodeType() (it was declared but ASSERT_NOT_REACHED), so isCachedBytecodeStillValid() covers these entries too, and adds recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable() as the function-shaped sibling of recursivelyGenerateUnlinkedCodeBlockForProgram().

Either shape works. Whichever survives, the key check seems worth having, so that callers can't silently decode bytecode built from source they no longer have.

Consumer on the Bun side: oven-sh/bun#33316 (Rust), which is the successor to oven-sh/bun#28461 (Zig, unrebasable after the Rust rewrite).

robobun added a commit to oven-sh/bun that referenced this pull request Jul 11, 2026
A compiled binary already ships bytecode for the app's own modules, but every
builtin it touches (node:net, node:fs, the internal/* modules they require) is
still parsed from source on first use. That is the bulk of the parse work left
at startup.

Builtins are compiled with createBuiltinExecutable() into an
UnlinkedFunctionExecutable, which the bytecode cache had no entry type for;
oven-sh/WebKit#270 adds encodeFunctionExecutable()/decodeFunctionExecutable()
for exactly this. WEBKIT_VERSION points at that PR's preview build for now and
must be re-pointed at the merged main sha before landing.

Unlike the other bytecode generation steps, this one has to walk a graph the
bundler cannot see. The bundle's import records name only the builtins the app
imports directly; those then require() each other through
InternalModuleRegistry by numeric id, resolved at runtime. So codegen emits the
adjacency of those @createInternalModuleById(N) edges, and caching "net" means
caching node:net plus every internal/* module it transitively reaches.

The entries are embedded in the standalone module graph and looked up by module
id on first require. JSC validates each one against a cache version and a
SourceCodeKey over the builtin's source, and the decoded executable keeps its
m_isBuiltinFunction bit, so a rejected or missing entry just parses in builtin
mode exactly as before. Cross-compiles skip generation entirely: src/js is
specialized per platform at Bun's own build time, so the key would never match.

Measured on a debug build running require("node:net") + node:http + node:fs:
~1.3s to ~0.77s startup, +5.8MB binary.
@robobun
robobun force-pushed the farm/c842455a/builtin-bytecode-cache-entry branch from d87c5e8 to 321befd Compare July 14, 2026 12:29

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I didn't find any bugs, but this adds a new top-level bytecode-cache entry type and decode path in JSC's core serialization layer, and there's an open coordination question with #177 — worth a human look before landing.

What was reviewed:

  • FunctionExecutableCacheEntry layout/methods vs. the existing CacheEntry<T> template — same field order, same key/tag/version validation.
  • decodeFunctionExecutable() mirrors decodeCodeBlockImpl() (size guard, DeferGC, key equality check); tag mismatch cleanly returns false/nullptr.
  • New CachedFunctionExecutableTag arm added to all three GenericCacheEntry switches; tagFromSourceCodeType(FunctionType) was previously unreachable so no existing caller changes behavior.
  • recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable() reuses the existing generateUnlinkedCodeBlockForFunctions() recursion.
Extended reasoning...

Overview

This PR adds a fourth top-level bytecode-cache entry shape to CachedTypes.cpp: FunctionExecutableCacheEntry, tagged CachedFunctionExecutableTag, which serializes an UnlinkedFunctionExecutable (plus its nested code-block tree) behind a versioned GenericCacheEntry header with a CachedSourceCodeKey. It exports encodeFunctionExecutable() / decodeFunctionExecutable() and two CodeCache.cpp helpers (sourceCodeKeyForSerializedFunctionExecutable(), recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable()). Roughly 130 lines across four files, all additive.

Security risks

Bytecode-cache deserialization is inherently sensitive: a mismatched or hostile blob that passes validation is treated as trusted bytecode. The mitigations here match the existing program/module paths exactly — isUpToDate() checks the JSC cache version, m_tag is checked before bit_cast, and the decoded SourceCodeKey (which hashes the source) must equal the caller's key. decodeFunctionExecutable() also has a sizeof(FunctionExecutableCacheEntry) lower-bound guard. I don't see a new attack surface beyond what decodeCodeBlockImpl() already exposes, but this is the kind of code where a second set of eyes on the bit_cast layout assumptions and the CachedPtr<CachedFunctionExecutable> decode path is warranted.

Level of scrutiny

Medium-high. The implementation is a near-verbatim copy of CacheEntry<T> and decodeCodeBlockImpl(), which lowers risk considerably, and none of the new code is reachable from existing JSC callers (only Bun's forthcoming consumer will call the new exports). But it's still ~130 lines of hand-written serialization in the JSC runtime, not a config tweak or mechanical change.

Other factors

The author flagged that this duplicates #177 and offered to close in favour of it, while noting a design difference (this PR validates the SourceCodeKey, #177 validates only the version stamp). Which shape lands — and whether the key check should be folded into #177 instead — is a coordination decision a maintainer should make, not a bot. There are no in-tree tests for the new path; the round-trip is exercised only from the downstream Bun PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Source/JavaScriptCore/runtime/CachedTypes.cpp (1)

1-1: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Wrap the new function-executable cache feature in USE(BUN_JSC_ADDITIONS). This entire feature (CachedFunctionExecutableTag, FunctionExecutableCacheEntry, encodeFunctionExecutable/decodeFunctionExecutable, sourceCodeKeyForSerializedFunctionExecutable, recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable) exists solely so Bun can cache builtin bytecode for node:net/node:fs/internal/* — it is not a general WebKit/JSC requirement, yet none of it is conditionally compiled, unlike the existing Bun-specific CachedStringSourceProvider logic in the same file (which is wrapped in #if USE(BUN_JSC_ADDITIONS)).

  • Source/JavaScriptCore/runtime/CachedTypes.cpp#L2654-2701: wrap FunctionExecutableCacheEntry, the new tag value/tagFromSourceCodeType case, the new GenericCacheEntry decode overloads and dispatch cases, and encodeFunctionExecutable/decodeFunctionExecutable in #if USE(BUN_JSC_ADDITIONS).
  • Source/JavaScriptCore/runtime/CachedTypes.h#L131-143: guard the encodeFunctionExecutable/decodeFunctionExecutable declarations with the same macro (or provide stub/no-op declarations for non-Bun builds).
  • Source/JavaScriptCore/runtime/CodeCache.cpp#L328-349: guard sourceCodeKeyForSerializedFunctionExecutable and recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable.
  • Source/JavaScriptCore/runtime/CodeCache.h#L269-278: guard the matching declarations.

As per coding guidelines, "Guard Bun-specific features with USE(BUN_JSC_ADDITIONS) and event-loop integration with USE(BUN_EVENT_LOOP)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Source/JavaScriptCore/runtime/CachedTypes.cpp` at line 1, The new
function-executable cache feature is compiled unconditionally instead of only
for Bun. Wrap FunctionExecutableCacheEntry, its tag and GenericCacheEntry
encode/decode dispatch, and encodeFunctionExecutable/decodeFunctionExecutable in
CachedTypes.cpp; guard their declarations in CachedTypes.h. Similarly guard
sourceCodeKeyForSerializedFunctionExecutable and
recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable, plus their
declarations in CodeCache.cpp and CodeCache.h, with USE(BUN_JSC_ADDITIONS),
preserving non-Bun builds without these Bun-specific APIs.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Source/JavaScriptCore/runtime/CachedTypes.cpp`:
- Around line 2654-2701: Guard the Bun-only FunctionExecutableCacheEntry class
and its CachedFunctionExecutableTag usage with `#if` USE(BUN_JSC_ADDITIONS),
matching the existing CachedStringSourceProvider guard pattern. Ensure non-Bun
JSC builds do not compile or reference this cache entry or tag, while preserving
the current behavior when the feature is enabled.

In `@Source/JavaScriptCore/runtime/CachedTypes.h`:
- Around line 131-143: Wrap the Bun-specific declarations
encodeFunctionExecutable and decodeFunctionExecutable in CachedTypes.h with the
USE(BUN_JSC_ADDITIONS) preprocessor guard, matching the corresponding guard in
CachedTypes.cpp and preserving their existing signatures.

In `@Source/JavaScriptCore/runtime/CodeCache.cpp`:
- Around line 328-349: Wrap both Bun-only functions,
sourceCodeKeyForSerializedFunctionExecutable and
recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable, in the
USE(BUN_JSC_ADDITIONS) conditional guard, matching the guard used for the
corresponding functionality in CachedTypes.cpp. Keep their implementations
unchanged within the guarded block.

In `@Source/JavaScriptCore/runtime/CodeCache.h`:
- Around line 269-278: Wrap the Bun-specific declarations in
CodeCache.h—recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable,
serializeBytecode, and the serialized source-code key helpers—in the same `#if`
USE(BUN_JSC_ADDITIONS) guard used by CachedTypes.cpp. Keep unrelated
declarations such as writeCodeBlock outside the guard.

---

Outside diff comments:
In `@Source/JavaScriptCore/runtime/CachedTypes.cpp`:
- Line 1: The new function-executable cache feature is compiled unconditionally
instead of only for Bun. Wrap FunctionExecutableCacheEntry, its tag and
GenericCacheEntry encode/decode dispatch, and
encodeFunctionExecutable/decodeFunctionExecutable in CachedTypes.cpp; guard
their declarations in CachedTypes.h. Similarly guard
sourceCodeKeyForSerializedFunctionExecutable and
recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable, plus their
declarations in CodeCache.cpp and CodeCache.h, with USE(BUN_JSC_ADDITIONS),
preserving non-Bun builds without these Bun-specific APIs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5a0c5b02-e8db-4057-99a8-1fe0f65a1358

📥 Commits

Reviewing files that changed from the base of the PR and between d87c5e8 and 321befd.

📒 Files selected for processing (4)
  • Source/JavaScriptCore/runtime/CachedTypes.cpp
  • Source/JavaScriptCore/runtime/CachedTypes.h
  • Source/JavaScriptCore/runtime/CodeCache.cpp
  • Source/JavaScriptCore/runtime/CodeCache.h

Comment thread Source/JavaScriptCore/runtime/CachedTypes.cpp
Comment thread Source/JavaScriptCore/runtime/CachedTypes.h
Comment thread Source/JavaScriptCore/runtime/CodeCache.cpp
Comment on lines +269 to +278
// Eagerly compile `executable` and every function nested beneath it, so that the whole
// tree can be serialized with encodeFunctionExecutable(). `parentSource` is the source
// the executable was created from, not its own linked sub-range.
JS_EXPORT_PRIVATE UnlinkedFunctionCodeBlock* recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable(VM&, UnlinkedFunctionExecutable*, const SourceCode& parentSource, ParserError&);

void writeCodeBlock(const SourceCodeKey&, const SourceCodeValue&);
RefPtr<CachedBytecode> serializeBytecode(VM&, UnlinkedCodeBlock*, const SourceCode&, SourceCodeType, LexicallyScopedFeatures, JSParserScriptMode, FileSystem::FileHandle&, BytecodeCacheError&, OptionSet<CodeGenerationMode>);
SourceCodeKey sourceCodeKeyForSerializedProgram(VM&, const SourceCode&);
SourceCodeKey sourceCodeKeyForSerializedModule(VM&, const SourceCode&);
JS_EXPORT_PRIVATE SourceCodeKey sourceCodeKeyForSerializedFunctionExecutable(VM&, const SourceCode&, const String& name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Same missing Bun-feature guard as CachedTypes.cpp.

These declarations expose a Bun-only feature but aren't wrapped in #if USE(BUN_JSC_ADDITIONS). See consolidated comment for details across the affected files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Source/JavaScriptCore/runtime/CodeCache.h` around lines 269 - 278, Wrap the
Bun-specific declarations in
CodeCache.h—recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable,
serializeBytecode, and the serialized source-code key helpers—in the same `#if`
USE(BUN_JSC_ADDITIONS) guard used by CachedTypes.cpp. Keep unrelated
declarations such as writeCodeBlock outside the guard.

Source: Coding guidelines

robobun added a commit to oven-sh/bun that referenced this pull request Jul 14, 2026
A compiled binary already ships bytecode for the app's own modules, but every
builtin it touches (node:net, node:fs, the internal/* modules they require) is
still parsed from source on first use. That is the bulk of the parse work left
at startup.

Builtins are compiled with createBuiltinExecutable() into an
UnlinkedFunctionExecutable, which the bytecode cache had no entry type for;
oven-sh/WebKit#270 adds encodeFunctionExecutable()/decodeFunctionExecutable()
for exactly this. WEBKIT_VERSION points at that PR's preview build for now and
must be re-pointed at the merged main sha before landing.

Unlike the other bytecode generation steps, this one has to walk a graph the
bundler cannot see. The bundle's import records name only the builtins the app
imports directly; those then require() each other through
InternalModuleRegistry by numeric id, resolved at runtime. So codegen emits the
adjacency of those @createInternalModuleById(N) edges, and caching "net" means
caching node:net plus every internal/* module it transitively reaches.

The entries are embedded in the standalone module graph and looked up by module
id on first require. JSC validates each one against a cache version and a
SourceCodeKey over the builtin's source, and the decoded executable keeps its
m_isBuiltinFunction bit, so a rejected or missing entry just parses in builtin
mode exactly as before. Cross-compiles skip generation entirely: src/js is
specialized per platform at Bun's own build time, so the key would never match.

Measured on a debug build running require("node:net") + node:http + node:fs:
~1.3s to ~0.77s startup, +5.8MB binary.
@robobun
robobun force-pushed the farm/c842455a/builtin-bytecode-cache-entry branch from 321befd to ce6f209 Compare August 14, 2026 22:00
@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto f0f60fd2 (#432), which is the sha Bun's main currently pins, so the preview built from this branch is exactly "what Bun ships today plus this one commit". 932 commits of churn including the upstream upgrade in #352 and the BunTranspiledModule cached-types change in #405; neither touched anything this relies on, and the rebase itself was clean.

The conflict GitHub was reporting is with #420, which adds a declaration at the same spot in CodeCache.h. I moved my two declarations to sit together after sourceCodeKeyForSerializedModule() instead; a trial merge against current main (8b6b046b) is now clean.

One deliberate change while I was in here: everything is now under USE(BUN_JSC_ADDITIONS), matching how #177, #405 and #420 do it in these same files. With the flag off the four files are byte-for-byte upstream (in particular tagFromSourceCodeType() keeps upstream's break; for FunctionType), which should make future upstream merges a little easier than the unconditional version was. No functional change under the flag.

Still a duplicate of #177 in intent; the comparison in my earlier comment stands, and I'm still happy for either to be the one that lands.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I reviewed this PR and didn't find any bugs. Because it adds new bytecode-cache serialization/deserialization machinery in CachedTypes.cpp and overlaps in intent with #177, a human look would still be worthwhile — both to sanity-check the encode/decode shape and to decide which of the two PRs should land.

What was reviewed:

  • FunctionExecutableCacheEntry layout and methods against the existing CacheEntry<T> template — field order, key handling, and static_assert(alignof …) all match.
  • decodeFunctionExecutable against decodeCodeBlockImpl — same DeferGC scope, same key comparison, plus an added size guard.
  • The three tag-dispatch switches — each gains a guarded CachedFunctionExecutableTag arm; the code-block decode overload correctly returns false on tag mismatch rather than asserting.
  • All additions are under #if USE(BUN_JSC_ADDITIONS); with the flag off, tagFromSourceCodeType() keeps upstream's break; for FunctionType.
Extended reasoning...

Overview

This PR adds a fourth top-level bytecode cache entry type to JSC so that an UnlinkedFunctionExecutable (the compilation unit for a JS builtin) can be serialized and later decoded as a self-validating cache entry. It touches four files: CachedTypes.{h,cpp} (new tag, new FunctionExecutableCacheEntry class deriving from GenericCacheEntry, three new switch arms, and encodeFunctionExecutable/decodeFunctionExecutable) and CodeCache.{h,cpp} (sourceCodeKeyForSerializedFunctionExecutable and recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable). Roughly ~130 net lines, all additive and all under #if USE(BUN_JSC_ADDITIONS).

Security risks

Bytecode cache deserialization is security-relevant: a malformed or mismatched blob decoded into an executable could run arbitrary code. The new decode path reuses the existing GenericCacheEntry header (cache version + boot-session UUID) via isUpToDate(), checks the tag before bit_cast-ing to the concrete entry type, and rejects on SourceCodeKey mismatch — the same layered validation the program/module entries use. decodeFunctionExecutable also adds a cachedBytecode->size() < sizeof(FunctionExecutableCacheEntry) guard before touching the buffer. The intended input is bytecode produced by Bun itself for its own builtins, not user-supplied blobs. I don't see a new attack surface beyond what the existing code-block cache entries already expose.

Level of scrutiny

Medium-to-high. The change is almost entirely a pattern-copy of CacheEntry<T> and decodeCodeBlockImpl, and with USE(BUN_JSC_ADDITIONS) off the four files are byte-for-byte upstream, so risk to non-Bun builds is nil. But it is new serialization logic in a core JSC subsystem, has no in-repo callers or tests (the round-trip is exercised from Bun's side per the description), and there is an open design question — this PR and #177 solve the same problem with slightly different validation semantics, and the author explicitly defers to maintainers on which should land.

Other factors

The earlier CodeRabbit feedback about missing USE(BUN_JSC_ADDITIONS) guards has been addressed in ce6f209; every added line in the current diff is guarded. The one CodeRabbit inline on CodeCache.h that isn't marked resolved is in fact addressed — the two new declarations there sit inside a #if USE(BUN_JSC_ADDITIONS) block. Preview builds succeeded. Given the scope, the lack of in-repo test coverage, and the #177-vs-#270 decision that a maintainer needs to make, deferring to human review rather than auto-approving.

robobun added a commit to oven-sh/bun that referenced this pull request Aug 14, 2026
A compiled binary already ships bytecode for the app's own modules, but every
builtin it touches (node:net, node:fs, the internal/* modules they require) is
still parsed from source on first use. That is the bulk of the parse work left
at startup.

Builtins are compiled with createBuiltinExecutable() into an
UnlinkedFunctionExecutable, which the bytecode cache had no entry type for;
oven-sh/WebKit#270 adds encodeFunctionExecutable()/decodeFunctionExecutable()
for exactly this. WEBKIT_VERSION points at that PR's preview build for now and
must be re-pointed at the merged main sha before landing.

Unlike the other bytecode generation steps, this one has to walk a graph the
bundler cannot see. The bundle's import records name only the builtins the app
imports directly; those then require() each other through InternalModuleRegistry
by numeric id, resolved at runtime. bundle-modules.ts already derives that graph
from the bundled output to lay the source blob out in dependency order; it now
also emits it as an adjacency table, and caching "net" means caching node:net
plus every internal/* module it transitively reaches.

The entries are embedded in the standalone module graph and looked up by module
id on first require, behind a flag the graph sets at startup so a Bun that
embeds nothing never takes the lookup. JSC validates each entry against a cache
version and a SourceCodeKey over the builtin's source; the runtime and the
generator obtain that source and build that SourceCode through the same two
functions, so a build's entries are always keyed on what the same build parses.
A rejected or missing entry parses in builtin mode exactly as before.
Cross-compiles skip generation: src/js is specialized per platform at Bun's own
build time, so the key would never match.

The bytecode VM only registers Bun's private names (JSVMClientData::
registerBuiltinNames) rather than creating full client data: the builtin-mode
lexer needs the names, and nothing else in client data applies to a VM with no
Bun VirtualMachine behind it.

Measured on a debug build running require("node:net") + node:http + node:fs:
~1.3s to ~0.77s startup; 69 builtins served from the cache.
The bytecode cache has top-level entries for programs, modules and eval,
because those are the top-level compilation units of a script. A JS builtin's
top-level unit is a builtin function: it is compiled with
createBuiltinExecutable() into an UnlinkedFunctionExecutable, whose code block
is generated lazily and has no cache entry of its own. encodeFunctionCodeBlock()
produces a bare CachedFunctionCodeBlock with no validation header, and the only
thing that can attach one back to an executable is CachedFunctionExecutable,
which is private to the cache implementation.

Add CachedFunctionExecutableTag and a FunctionExecutableCacheEntry holding a
SourceCodeKey plus a CachedPtr<CachedFunctionExecutable>, exposed as
encodeFunctionExecutable()/decodeFunctionExecutable(). SourceCodeType::FunctionType
already existed but was unreachable in tagFromSourceCodeType(); it now maps to the
new tag, so isCachedBytecodeStillValid() works for these entries too.

Decoding restores the executable's m_isBuiltinFunction bit, so an entry that is
stale, rejected, or missing a nested code block falls back to parsing in builtin
mode exactly as before.

recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable() compiles an executable
and everything nested beneath it, which is what makes the whole tree serializable,
and sourceCodeKeyForSerializedFunctionExecutable() builds the matching key.

Everything is under USE(BUN_JSC_ADDITIONS), like the other fork additions in
these files; with it off, the files are byte-for-byte upstream. No existing cache
entry changes shape.
@robobun
robobun force-pushed the farm/c842455a/builtin-bytecode-cache-entry branch from ce6f209 to 02b1d0a Compare August 16, 2026 19:13

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I reviewed this PR and didn't find any bugs. Because it adds a new top-level bytecode-cache entry type with its own serialization/deserialization path in JSC, and because there's still an open call on whether this or #177 is the one that should land, a human look would be worthwhile.

What was reviewed:

  • New FunctionExecutableCacheEntry layout vs. GenericCacheEntry dispatch — same std::bit_cast + static_assert(alignof) pattern as CacheEntry<T>, and the tag is checked before the cast.
  • decodeFunctionExecutable() — size check, isUpToDate() version gate, tag gate, and SourceCodeKey equality all reject before returning an executable; DeferGC scope matches decodeCodeBlockImpl().
  • All additions are under USE(BUN_JSC_ADDITIONS); with the flag off, tagFromSourceCodeType() and the switch statements are byte-for-byte upstream.
Extended reasoning...

Overview

This PR adds a fourth top-level bytecode-cache entry shape to JSC's CachedTypes.cpp: a FunctionExecutableCacheEntry that serializes an UnlinkedFunctionExecutable (plus its nested code-block tree) as a self-validating GenericCacheEntry, alongside the existing program/module/eval entries. It exposes four new exported symbols — encodeFunctionExecutable(), decodeFunctionExecutable(), recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable(), and sourceCodeKeyForSerializedFunctionExecutable() — and threads a new CachedFunctionExecutableTag through tagFromSourceCodeType() and the three GenericCacheEntry dispatch switches. Everything is guarded by #if USE(BUN_JSC_ADDITIONS) (added in ce6f209 after CodeRabbit feedback), so non-Bun builds are unchanged.

Security risks

Bytecode-cache deserialization is security-adjacent: the decoder reads a byte buffer produced elsewhere, bit_casts to a concrete entry type based on a tag, and materializes GC objects from it. The new path follows the existing CacheEntry<T> pattern closely — isUpToDate() (bytecode version) is checked first, then the tag, then the SourceCodeKey (source hash) — and there is a minimum-size check before the first bit_cast. In Bun's intended use the buffer is baked into the binary at build time, not read from an untrusted filesystem. I did not spot a way for a mismatched or truncated entry to get past the guards, but this is exactly the sort of code where a second pair of eyes on the layout assumptions and CachedPtr<CachedFunctionExecutable> decode path is worth having.

Level of scrutiny

Medium-high. The change is additive and feature-flagged, and it very deliberately reuses the existing encoder/decoder machinery rather than inventing new serialization. But it is ~150 lines of new C++ in JSC's runtime that create GC cells from serialized bytes, and the round-trip is only exercised from Bun's side (oven-sh/bun#33316), not by any test in this repo. That combination — core-runtime serialization plus out-of-tree test coverage — is past the threshold where I would auto-approve.

Other factors

The author has twice noted that this duplicates #177 in intent and is happy for either to land, with the substantive difference being that this variant validates the full SourceCodeKey (source hash) while #177 validates only the JSC bytecode version. Which of the two lands, or whether the key check should be folded into #177 instead, is a maintainer call rather than a correctness call, and it should be made by a human. All prior reviewer feedback (the USE(BUN_JSC_ADDITIONS) guards) has been addressed and those threads are resolved.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant