Skip to content

resolver: split the port's module wrapper into files; type the extern-Rust pointers#30880

Merged
Jarred-Sumner merged 2 commits into
mainfrom
claude/resolver-typed-modules
May 16, 2026
Merged

resolver: split the port's module wrapper into files; type the extern-Rust pointers#30880
Jarred-Sumner merged 2 commits into
mainfrom
claude/resolver-typed-modules

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

What

Module split

The Zig→Rust port wrapped the entire resolver implementation in a single 7,664-line pub mod __phase_a_body { ... } inside src/resolver/lib.rs (lines 2609–10,273) — a port artifact ("this is the mechanically-translated block"). There's no name for a module that wraps a crate's whole body that isn't redundant, which is the tell the wrapper shouldn't exist. Split it into sibling files following the crate's existing convention (data_url.rs / dir_info.rs / package_json.rs):

File Lines Holds
options.rs 357 BundleOptions, Packages, ExternalModules, Framework, ExtOrder, …
result.rs 578 Result, MatchResult, PathPair, DebugLogs, PendingResolution, LoadResult, …
resolver.rs 6,640 Resolver struct + impl, threadlocal Bufs, local shim modules
standalone_module_graph.rs 30 the StandaloneModuleGraph trait
allocators.rs 6 re-exports referenced cross-file from dir_info.rs

lib.rs shrinks 10,273 → 2,615 lines. Public API surface is byte-identical — bun_resolver::Resolver, ::Result, ::options, … all resolve as before.

Typed extern-Rust pointers

Un-erase the extern "Rust" link-time pointers where the declaring crate already names the type. The port applied "type-erase across the crate boundary" uniformly to every #[no_mangle] upward call, but extern "Rust" carries full Rust types — both crates can name the parameters. Where visible, use the typed pointer with the Zig pointer shape (NonNull<T> for *T, Option<NonNull<T>> for ?*T):

  • __bun_resolver_init_package_manager: log: *mut LogNonNull<Log>, install: *const ()Option<NonNull<BunInstall>>, env: *mut c_voidNonNull<Loader<'static>>
  • BundleOptions.install: *const ()Option<NonNull<BunInstall>>
  • Resolver.log: *mut LogNonNull<Log>
  • __bun_jsc_enable_hot_module_reloading_for_bundler: *mut ()NonNull<BundleV2<'static>>

The implementation-side cast::<T>() calls — the tell that the erasure was unnecessary — are removed.

Sites where the type is genuinely not visible (bun_event_loopbun_jsc::VirtualMachine, bun_js_parserbun_bundler::Transpiler) are left as-is — that's the real layering boundary the pattern exists for.

Verification

  • cargo check --workspace clean
  • cargo fmt --check clean
  • bun run rust:check-all (all 6 target triples)
  • bun bd links and runs
  • grep -rn resolver_body src/ returns nothing

The Zig->Rust port wrapped the entire resolver implementation (Resolver,
Result, MatchResult, options, allocators, ...) in a single 7,664-line
pub mod __phase_a_body { ... } inside src/resolver/lib.rs. There is no
name for a module that wraps a crate's whole body that isn't redundant,
which is the tell that the wrapper shouldn't exist. Split it into
sibling files following the crate's existing convention
(data_url.rs / dir_info.rs / package_json.rs):

  - options.rs (357 lines): BundleOptions, Packages, ExternalModules,
    Framework, ExtOrder, ...
  - result.rs (578 lines): Result, MatchResult, PathPair, DebugLogs,
    PendingResolution, LoadResult, ...
  - resolver.rs (6,640 lines): Resolver struct + impl, threadlocal
    Bufs scratch, the local shim modules
  - allocators.rs (6 lines): re-exports referenced cross-file
  - standalone_module_graph.rs (30 lines): the StandaloneModuleGraph trait

lib.rs shrinks 10,273 -> 2,615 lines. Public API surface is byte-identical
(bun_resolver::Resolver, ::Result, ::options, ... all unchanged).

Also un-erase the extern "Rust" link-time pointers where the declaring
crate already names the type. The port applied 'type-erase across the
crate boundary' uniformly to every #[no_mangle] upward call, but
extern "Rust" carries full Rust types and most declaring crates already
depend on the crate that defines the parameter types. Where visible,
use the typed pointer with the Zig pointer shape (NonNull<T> for *T,
Option<NonNull<T>> for ?*T):

  - __bun_resolver_init_package_manager: log *mut Log -> NonNull<Log>,
    install *const () -> Option<NonNull<BunInstall>>,
    env *mut c_void -> NonNull<Loader<'static>>
  - BundleOptions.install: *const () -> Option<NonNull<BunInstall>>
  - Resolver.log: *mut Log -> NonNull<Log>
  - __bun_jsc_enable_hot_module_reloading_for_bundler:
    *mut () -> NonNull<BundleV2<'static>>

The implementation-side cast::<T>() calls -- the tell that the erasure
was unnecessary -- are removed.

Sites where the type is genuinely not visible (bun_event_loop ->
bun_jsc::VirtualMachine, bun_js_parser -> bun_bundler::Transpiler) are
left as-is: those are the real layering boundary the pattern is for.
@robobun

robobun commented May 16, 2026

Copy link
Copy Markdown
Collaborator
Updated 1:07 AM PT - May 16th, 2026

@Jarred-Sumner, your commit 93be703 has 1 failures in Build #55108 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30880

That installs a local version of the PR into your bun-30880 executable, so you can run:

bun-30880 --bun

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Rate limit exceeded

@Jarred-Sumner has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 10 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c35a9f3-4759-49b2-956f-e18f12f75819

📥 Commits

Reviewing files that changed from the base of the PR and between ff835a3 and 93be703.

📒 Files selected for processing (5)
  • src/bundler/bundle_v2.rs
  • src/resolver/dir_info.rs
  • src/resolver/lib.rs
  • src/resolver/resolver.rs
  • src/resolver/result.rs

Walkthrough

This PR modernizes pointer handling across the resolver and bundler stack by introducing typed NonNull pointers at FFI boundaries, defining resolver-tier configuration and result schemas, and updating internal log threading to eliminate raw pointer casts. Key additions include the resolver options model (BundleOptions, ExtOrder), resolver output types (Result, MatchResult), and comprehensive pointer standardization throughout bundler, JSC, and runtime modules. Phase-specific documentation is cleaned up.

Changes

Resolver options, results, and pointer standardization

Layer / File(s) Summary
Resolver options schema
src/resolver/options.rs
Defines resolver-tier canonical BundleOptions struct with extension ordering, conditions, external modules, main-field selection, and bundler-projected state fields; introduces ExtOrder and ExtensionOrder for extension-resolution ordering; adds Packages, ExternalModules, WildcardPattern, Conditions for module resolution configuration; and provides TargetMainFields with per-target default main-field ordering and pointer-identity detection.
Resolver result and output types
src/resolver/result.rs
Introduces resolver output model: Result with path info, module-type, side-effects, flags, and FDs; MatchResult and MatchResultUnion for match outcomes; PendingResolution and PendingResolutionTag for in-flight resolutions; LoadResult for loaded files; supporting types for side-effects, path iteration, queue items; and debug logging with indent-aware notes.
Resolver module infrastructure
src/resolver/allocators.rs, src/resolver/dir_info.rs, src/resolver/standalone_module_graph.rs
Re-exports bun_alloc::allocators and defines Status alias; updates HashMapExt method signatures to use crate::allocators::Result instead of phase-specific path; introduces StandaloneModuleGraph trait for embedded module lookup and artifact introspection.
FFI boundary NonNull standardization
src/install/auto_installer.rs, src/jsc/hot_reloader.rs
__bun_resolver_init_package_manager and __bun_jsc_enable_hot_module_reloading_for_bundler signatures updated from raw pointers to typed NonNull<T> and Option<NonNull<T>> for explicit non-null contracts at module boundaries.
Internal log pointer NonNull migration
src/bundler/bundle_v2.rs, src/bundler/transpiler.rs, src/jsc/AsyncModule.rs, src/jsc/VirtualMachine.rs, src/runtime/jsc_hooks.rs
Replaces raw pointer handling with NonNull in log threading: Transpiler methods use NonNull::new(log).expect(...); module logging swaps use NonNull::from(&mut log) and restore from captured non-null values; HMR dispatch updated to receive and validate non-null pointer directly.
Install option wiring updates
src/bundler/transpiler.rs, src/runtime/api/filesystem_router.rs, src/runtime/bake/production.rs, src/runtime/cli/repl_command.rs, src/runtime/cli/run_command.rs
resolver_bundle_options_subset assigns install directly instead of casting; runtime entry points assign b.resolver.opts.install directly from Option<NonNull<_>> instead of converting to nullable/erased pointers; FileSystemRouter calls updated to pass NonNull::from(&mut log).
Test harness and comment cleanup
src/router/lib.rs, src/bundler/bundle_v2.rs, src/bundler/options.rs, src/bundler/transpiler.rs, src/install/PackageManager.rs, src/install/PackageManager/PackageManagerOptions.rs, src/jsc/AsyncModule.rs, src/jsc/VirtualMachine.rs, src/jsc/hot_reloader.rs, src/runtime/api/filesystem_router.rs, src/runtime/bake/production.rs, src/runtime/cli/repl_command.rs, src/runtime/cli/run_command.rs, src/runtime/jsc_hooks.rs
Router test harness updated to pass NonNull::from(&mut log) to Resolver::init1; large "B-2 un-gate" and "Phase-A" explanatory comment blocks removed; inline comments updated throughout to reflect arena-erasure conventions, lifetime threading patterns, and current porting state.

Suggested reviewers

  • alii
  • dylan-conway
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the primary changes: splitting a large module wrapper into separate files and typing extern-Rust pointers, which matches the changeset's main objectives.
Description check ✅ Passed The PR description comprehensively covers both main changes (module split and pointer typing) with clear examples, a detailed file breakdown table, and explicit verification steps, meeting all template requirements.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (2)
src/jsc/hot_reloader.rs (1)

1284-1298: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix the reconstructed path bounds here.

This copy starts changed_name at the same index where Line 1282 wrote SEP, so the separator is overwritten, and path_slice then includes one extra byte past the copied filename. The resulting path/hash is malformed on this branch.

Suggested fix
-                                            _on_file_update_path_buf
-                                                [file_path_without_trailing_slash.len()
-                                                    ..file_path_without_trailing_slash.len()
-                                                        + changed_name.len()]
-                                                .copy_from_slice(changed_name);
-                                            let path_slice = &_on_file_update_path_buf[0
-                                                ..file_path_without_trailing_slash.len()
-                                                    + changed_name.len()
-                                                    + 1];
+                                            let dst_start =
+                                                file_path_without_trailing_slash.len() + 1;
+                                            _on_file_update_path_buf
+                                                [dst_start..dst_start + changed_name.len()]
+                                                .copy_from_slice(changed_name);
+                                            let path_slice =
+                                                &_on_file_update_path_buf[..dst_start + changed_name.len()];
🤖 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 `@src/jsc/hot_reloader.rs` around lines 1284 - 1298, The reconstructed path
overwrites the separator and includes one extra byte; fix by copying
changed_name starting after the separator and slicing to include exactly
separator + filename: use
_on_file_update_path_buf[file_path_without_trailing_slash.len() + 1 ..
file_path_without_trailing_slash.len() + 1 +
changed_name.len()].copy_from_slice(changed_name) and set path_slice =
&_on_file_update_path_buf[0 .. file_path_without_trailing_slash.len() + 1 +
changed_name.len()] so the SEP (written earlier) is preserved and no extra byte
is included; update the ranges around _on_file_update_path_buf,
file_path_without_trailing_slash, and changed_name accordingly.
src/bundler/transpiler.rs (1)

166-173: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-thread options.log in set_log.

set_log() updates self.log, linker.log, and resolver.log, but leaves self.options.log pointing at the previous logger. Line 2848 already consults self.options.log().level, so a later log swap can split diagnostics and log-level checks across two different Log instances. wire_after_move() already maintains this invariant; set_log() should too.

Suggested fix
 pub fn set_log(&mut self, log: *mut bun_ast::Log) {
     self.log = log;
+    self.options.log = log;
     self.linker.log = log;
     // SAFETY: caller (`ThreadPool::Worker::create`) passes the per-worker
     // arena-allocated `Log`, which outlives this `Transpiler<'a>`. Zig
     // aliased the same `*Log` into `resolver.log`.
     self.resolver.log = core::ptr::NonNull::new(log).expect("set_log: log is non-null");
 }
🤖 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 `@src/bundler/transpiler.rs` around lines 166 - 173, set_log currently updates
self.log, self.linker.log, and self.resolver.log but fails to update the logger
stored in self.options, which leaves self.options.log() pointing at the old Log
and can split diagnostics and level checks; update set_log to also re-thread
self.options.log to the new log pointer (mirror what wire_after_move does) so
that self.options.log(), self.log, self.linker.log, and self.resolver.log all
reference the same Log instance after calling set_log.
🤖 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 `@src/bundler/bundle_v2.rs`:
- Around line 1477-1493: The watcher is installed too early in BundleV2::init
such that enable_hot_module_reloading_for_bundler (and the NonNull bv2 pointer)
can outlive the Box<BundleV2> when generate_from_cli returns Err before
mem::forget(this); fix by moving the watcher installation to after the bundle is
guaranteed retained (i.e., after the point where generate_from_cli would call
mem::forget or otherwise leak/retain the Box), or alternatively ensure every
watch-mode error exit explicitly retains/leaks the Box<BundleV2> (call
mem::forget or convert to a leaked pointer) before returning so the bv2 passed
to enable_hot_module_reloading_for_bundler is truly 'static; update
BundleV2::init and generate_from_cli call-sites accordingly.

In `@src/jsc/AsyncModule.rs`:
- Around line 1266-1280: The temporary log replacement misses swapping
transpiler.options.log, so calls that go through self.options.log() still use
the old VM log; update the unsafe block to also set
(*jsc_vm).transpiler.options.log = log_ptr (alongside transpiler.linker.log,
transpiler.log, transpiler.resolver.log, package_manager().log) and in the
scopeguard restore closure restore (*jsc_vm).transpiler.options.log =
old_log_ptr (alongside the other restores) so the options log is correctly
overridden and restored for the duration of the scope.

In `@src/resolver/result.rs`:
- Around line 447-450: The decrease_indent method unconditionally subtracts 1
from self.indent.list.len(), which will underflow and panic if the list is
empty; update decrease_indent (in src/resolver/result.rs) to check whether
self.indent.list is empty before removing an element (e.g., use an if
!self.indent.list.is_empty() guard and then truncate or call pop) so the method
becomes a no-op on empty indent instead of panicking.
- Around line 527-529: The initializer is using the package sentinel for a
dependency field—change the root_dependency_id assignment to use
Install::INVALID_DEPENDENCY_ID instead of Install::INVALID_PACKAGE_ID; update
the code where root_dependency_id (a DependencyID) is set so it references
Install::INVALID_DEPENDENCY_ID to keep the correct sentinel type and semantic
clarity for the DependencyID field.

---

Outside diff comments:
In `@src/bundler/transpiler.rs`:
- Around line 166-173: set_log currently updates self.log, self.linker.log, and
self.resolver.log but fails to update the logger stored in self.options, which
leaves self.options.log() pointing at the old Log and can split diagnostics and
level checks; update set_log to also re-thread self.options.log to the new log
pointer (mirror what wire_after_move does) so that self.options.log(), self.log,
self.linker.log, and self.resolver.log all reference the same Log instance after
calling set_log.

In `@src/jsc/hot_reloader.rs`:
- Around line 1284-1298: The reconstructed path overwrites the separator and
includes one extra byte; fix by copying changed_name starting after the
separator and slicing to include exactly separator + filename: use
_on_file_update_path_buf[file_path_without_trailing_slash.len() + 1 ..
file_path_without_trailing_slash.len() + 1 +
changed_name.len()].copy_from_slice(changed_name) and set path_slice =
&_on_file_update_path_buf[0 .. file_path_without_trailing_slash.len() + 1 +
changed_name.len()] so the SEP (written earlier) is preserved and no extra byte
is included; update the ranges around _on_file_update_path_buf,
file_path_without_trailing_slash, and changed_name accordingly.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5479db7c-401c-4a11-878c-b645dbb0f9b2

📥 Commits

Reviewing files that changed from the base of the PR and between 2a3d0e7 and ff835a3.

📒 Files selected for processing (22)
  • src/bundler/bundle_v2.rs
  • src/bundler/options.rs
  • src/bundler/transpiler.rs
  • src/install/PackageManager.rs
  • src/install/PackageManager/PackageManagerOptions.rs
  • src/install/auto_installer.rs
  • src/jsc/AsyncModule.rs
  • src/jsc/VirtualMachine.rs
  • src/jsc/hot_reloader.rs
  • src/resolver/allocators.rs
  • src/resolver/dir_info.rs
  • src/resolver/lib.rs
  • src/resolver/options.rs
  • src/resolver/resolver.rs
  • src/resolver/result.rs
  • src/resolver/standalone_module_graph.rs
  • src/router/lib.rs
  • src/runtime/api/filesystem_router.rs
  • src/runtime/bake/production.rs
  • src/runtime/cli/repl_command.rs
  • src/runtime/cli/run_command.rs
  • src/runtime/jsc_hooks.rs

Comment thread src/bundler/bundle_v2.rs Outdated
Comment on lines +1477 to +1493
/// `'static` matches the impl side: the only caller (`bun build
/// --watch`) leaks the CLI arena, so the pointee is process-lifetime.
fn __bun_jsc_enable_hot_module_reloading_for_bundler(
bv2: core::ptr::NonNull<super::BundleV2<'static>>,
);
}

/// `Watcher.enableHotModuleReloading(this, null)` for `bun build --watch`.
#[inline]
pub fn enable_hot_module_reloading_for_bundler(bv2: *mut super::BundleV2<'_>) {
// SAFETY: link-time-resolved Rust-ABI fn in `bun_jsc::hot_reloader`.
// Not `safe fn`: the callee re-types the erased `*mut ()` as
// `*mut BundleV2<'static>` and dereferences it, so `bv2` must point to
// a live `BundleV2` whose backing allocation outlives the watcher
// (sole caller is `BundleV2::init` with the leaked CLI arena).
unsafe { __bun_jsc_enable_hot_module_reloading_for_bundler(bv2.cast()) }
// Not `safe fn`: the callee dereferences `bv2`, so it must point to a
// live `BundleV2` whose backing allocation outlives the watcher (sole
// caller is `BundleV2::init` with the leaked CLI arena — `'static`).
let bv2 = core::ptr::NonNull::new(bv2.cast::<super::BundleV2<'static>>())
.expect("BundleV2 watcher: bv2 is non-null");
unsafe { __bun_jsc_enable_hot_module_reloading_for_bundler(bv2) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

The new 'static HMR contract is not upheld on watch-mode error exits.

Line 1477 says this pointer is process-lifetime because the CLI bundle is leaked, but BundleV2::init() installs the watcher before generate_from_cli() reaches its final mem::forget(this). In watch mode, any earlier return Err(...) drops the Box<BundleV2> while the reloader still owns the pointer, so the next file event can dereference freed memory. Please move watcher installation to the point where the bundle is definitely retained, or retain/leak the bundle on every watch-mode exit once HMR is enabled.

🤖 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 `@src/bundler/bundle_v2.rs` around lines 1477 - 1493, The watcher is installed
too early in BundleV2::init such that enable_hot_module_reloading_for_bundler
(and the NonNull bv2 pointer) can outlive the Box<BundleV2> when
generate_from_cli returns Err before mem::forget(this); fix by moving the
watcher installation to after the bundle is guaranteed retained (i.e., after the
point where generate_from_cli would call mem::forget or otherwise leak/retain
the Box), or alternatively ensure every watch-mode error exit explicitly
retains/leaks the Box<BundleV2> (call mem::forget or convert to a leaked
pointer) before returning so the bv2 passed to
enable_hot_module_reloading_for_bundler is truly 'static; update BundleV2::init
and generate_from_cli call-sites accordingly.

Comment thread src/jsc/AsyncModule.rs
Comment on lines 1266 to 1280
unsafe {
(*jsc_vm).transpiler.linker.log = log_ptr;
(*jsc_vm).transpiler.log = log_ptr;
(*jsc_vm).transpiler.resolver.log = log_ptr;
(*jsc_vm).transpiler.resolver.log = log_nn;
(*jsc_vm).package_manager().log = log_ptr;
}
let _restore = scopeguard::guard((jsc_vm, old_log), |(jsc_vm, old_log)| {
// SAFETY: same per-thread VM; restoring the original `*mut Log`
// values stored above.
// SAFETY: same per-thread VM; restoring the original log pointers
// stored above.
unsafe {
let old_log_ptr = old_log.map(|p| p.as_ptr()).unwrap_or(core::ptr::null_mut());
let old_log_ptr = old_log.as_ptr();
(*jsc_vm).transpiler.linker.log = old_log_ptr;
(*jsc_vm).transpiler.log = old_log_ptr;
(*jsc_vm).transpiler.resolver.log = old_log_ptr;
(*jsc_vm).transpiler.resolver.log = old_log;
(*jsc_vm).package_manager().log = old_log_ptr;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Swap transpiler.options.log with the other temporary log aliases.

This scope overrides transpiler.log, linker.log, resolver.log, and package_manager.log, but leaves transpiler.options.log on the VM log. Anything on the link() / print_with_source_map() path that calls self.options.log() will still write to the stale buffer, so async-module diagnostics can bypass the local log.

Suggested fix
         unsafe {
+            (*jsc_vm).transpiler.options.log = log_ptr;
             (*jsc_vm).transpiler.linker.log = log_ptr;
             (*jsc_vm).transpiler.log = log_ptr;
             (*jsc_vm).transpiler.resolver.log = log_nn;
             (*jsc_vm).package_manager().log = log_ptr;
         }
@@
             unsafe {
                 let old_log_ptr = old_log.as_ptr();
+                (*jsc_vm).transpiler.options.log = old_log_ptr;
                 (*jsc_vm).transpiler.linker.log = old_log_ptr;
                 (*jsc_vm).transpiler.log = old_log_ptr;
                 (*jsc_vm).transpiler.resolver.log = old_log;
                 (*jsc_vm).package_manager().log = old_log_ptr;
             }
🤖 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 `@src/jsc/AsyncModule.rs` around lines 1266 - 1280, The temporary log
replacement misses swapping transpiler.options.log, so calls that go through
self.options.log() still use the old VM log; update the unsafe block to also set
(*jsc_vm).transpiler.options.log = log_ptr (alongside transpiler.linker.log,
transpiler.log, transpiler.resolver.log, package_manager().log) and in the
scopeguard restore closure restore (*jsc_vm).transpiler.options.log =
old_log_ptr (alongside the other restores) so the options log is correctly
overridden and restored for the duration of the scope.

Comment thread src/resolver/result.rs
Comment on lines +447 to +450
pub fn decrease_indent(&mut self) {
let new_len = self.indent.list.len() - 1;
self.indent.list.truncate(new_len);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against empty-indent underflow in decrease_indent.

Line 448 subtracts 1 unconditionally; calling this on an empty indent buffer panics.

Proposed fix
 pub fn decrease_indent(&mut self) {
-    let new_len = self.indent.list.len() - 1;
-    self.indent.list.truncate(new_len);
+    if let Some(new_len) = self.indent.list.len().checked_sub(1) {
+        self.indent.list.truncate(new_len);
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn decrease_indent(&mut self) {
let new_len = self.indent.list.len() - 1;
self.indent.list.truncate(new_len);
}
pub fn decrease_indent(&mut self) {
if let Some(new_len) = self.indent.list.len().checked_sub(1) {
self.indent.list.truncate(new_len);
}
}
🤖 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 `@src/resolver/result.rs` around lines 447 - 450, The decrease_indent method
unconditionally subtracts 1 from self.indent.list.len(), which will underflow
and panic if the list is empty; update decrease_indent (in
src/resolver/result.rs) to check whether self.indent.list is empty before
removing an element (e.g., use an if !self.indent.list.is_empty() guard and then
truncate or call pop) so the method becomes a no-op on empty indent instead of
panicking.

Comment thread src/resolver/result.rs
Comment on lines +527 to +529
resolution_id: Install::INVALID_PACKAGE_ID,
root_dependency_id: Install::INVALID_PACKAGE_ID,
import_record_id: u32::MAX,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the canonical invalid constants/types in resolver hooks.
rg -n --type rust "type\s+PackageID|type\s+DependencyID|INVALID_[A-Z_]*PACKAGE|INVALID_[A-Z_]*DEPENDENCY"

# Verify all root_dependency_id initializations/usages.
rg -n --type rust "root_dependency_id\s*[:=]"

Repository: oven-sh/bun

Length of output: 8877


Use Install::INVALID_DEPENDENCY_ID for the root_dependency_id field.

Line 528 initializes a DependencyID field with Install::INVALID_PACKAGE_ID. Although both sentinels currently have the same value (u32::MAX), they represent different types. Using the correct sentinel constant ensures semantic clarity and prevents bugs if the invalid constants ever diverge.

Proposed fix
-            root_dependency_id: Install::INVALID_PACKAGE_ID,
+            root_dependency_id: Install::INVALID_DEPENDENCY_ID,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resolution_id: Install::INVALID_PACKAGE_ID,
root_dependency_id: Install::INVALID_PACKAGE_ID,
import_record_id: u32::MAX,
resolution_id: Install::INVALID_PACKAGE_ID,
root_dependency_id: Install::INVALID_DEPENDENCY_ID,
import_record_id: u32::MAX,
🤖 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 `@src/resolver/result.rs` around lines 527 - 529, The initializer is using the
package sentinel for a dependency field—change the root_dependency_id assignment
to use Install::INVALID_DEPENDENCY_ID instead of Install::INVALID_PACKAGE_ID;
update the code where root_dependency_id (a DependencyID) is set so it
references Install::INVALID_DEPENDENCY_ID to keep the correct sentinel type and
semantic clarity for the DependencyID field.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

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 is a large refactor (~8k lines moved out of resolver/lib.rs into new files) that also retypes several extern "Rust" link-time signatures and adds new .expect() panic paths on vm.log — worth a human pass to confirm the move is byte-faithful and both sides of each #[no_mangle] boundary agree.

Extended reasoning...

Overview

Splits the 7,664-line __phase_a_body wrapper module in src/resolver/lib.rs into five sibling files (options.rs, result.rs, resolver.rs, standalone_module_graph.rs, allocators.rs), and retypes several cross-crate extern "Rust" pointers from erased *mut () / *mut c_void / *mut Log to NonNull<T> / Option<NonNull<T>>. Ripples through 22 files across bundler, install, jsc, resolver, router, and runtime. Also scrubs a large number of "Phase A/B" port-artifact comments.

Security risks

None identified — no auth, crypto, or untrusted-input parsing is touched. The extern "Rust" signature changes are an ABI concern (both declaration and definition must agree exactly or it's UB), but that's a correctness risk rather than a security one, and cargo check --workspace / bun bd linking would catch a mismatch.

Level of scrutiny

High. The resolver is on the hot path for every import/require in Bun, and this PR moves ~6,640 lines into a new resolver.rs that the diff tool can't render. The claim is "public API surface byte-identical," but verifying that requires comparing the moved bodies, not just the re-export list. The NonNull<Log> retyping of Resolver.log introduces .expect("vm.log set in init") at several restore sites (AsyncModule.rs, jsc_hooks.rs::resolve_hook) where the old code tolerated None via .unwrap_or(null_mut()) — the PR asserts these are infallible, which is probably true, but it's a behavioral tightening worth a second pair of eyes.

Other factors

The bug-hunting system found nothing, and the verification section lists cargo check, cargo fmt, all-triple rust:check-all, and a successful bun bd link+run. That's good coverage for the mechanical parts. Still, the combination of (a) a multi-thousand-line code move that can't be diffed inline, (b) link-time ABI retyping across four #[no_mangle] boundaries, and (c) touching nearly every major subsystem puts this well outside "simple/mechanical" territory.

Comment thread src/resolver/allocators.rs Outdated
@@ -0,0 +1,6 @@
//! ── bun_alloc::allocators re-export ──────────────────────────────────────

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Delete this slop file. Do not re-export.

@Jarred-Sumner Jarred-Sumner merged commit 8438ff7 into main May 16, 2026
76 of 77 checks passed
@Jarred-Sumner Jarred-Sumner deleted the claude/resolver-typed-modules branch May 16, 2026 08:42
robjtede pushed a commit to robjtede/bun that referenced this pull request May 16, 2026
…-Rust pointers (oven-sh#30880)

## What

### Module split

The Zig→Rust port wrapped the entire resolver implementation in a single
7,664-line `pub mod __phase_a_body { ... }` inside `src/resolver/lib.rs`
(lines 2609–10,273) — a port artifact ("this is the
mechanically-translated block"). There's no name for a module that wraps
a crate's whole body that isn't redundant, which is the tell the wrapper
shouldn't exist. Split it into sibling files following the crate's
existing convention (`data_url.rs` / `dir_info.rs` / `package_json.rs`):

| File | Lines | Holds |
|---|---|---|
| `options.rs` | 357 | `BundleOptions`, `Packages`, `ExternalModules`,
`Framework`, `ExtOrder`, … |
| `result.rs` | 578 | `Result`, `MatchResult`, `PathPair`, `DebugLogs`,
`PendingResolution`, `LoadResult`, … |
| `resolver.rs` | 6,640 | `Resolver` struct + impl, threadlocal `Bufs`,
local shim modules |
| `standalone_module_graph.rs` | 30 | the `StandaloneModuleGraph` trait
|
| `allocators.rs` | 6 | re-exports referenced cross-file from
`dir_info.rs` |

`lib.rs` shrinks 10,273 → 2,615 lines. Public API surface is
byte-identical — `bun_resolver::Resolver`, `::Result`, `::options`, …
all resolve as before.

### Typed extern-Rust pointers

Un-erase the `extern "Rust"` link-time pointers where the declaring
crate already names the type. The port applied "type-erase across the
crate boundary" uniformly to every `#[no_mangle]` upward call, but
`extern "Rust"` carries full Rust types — both crates can name the
parameters. Where visible, use the typed pointer with the Zig pointer
shape (`NonNull<T>` for `*T`, `Option<NonNull<T>>` for `?*T`):

- `__bun_resolver_init_package_manager`: `log: *mut Log` →
`NonNull<Log>`, `install: *const ()` → `Option<NonNull<BunInstall>>`,
`env: *mut c_void` → `NonNull<Loader<'static>>`
- `BundleOptions.install`: `*const ()` → `Option<NonNull<BunInstall>>`
- `Resolver.log`: `*mut Log` → `NonNull<Log>`
- `__bun_jsc_enable_hot_module_reloading_for_bundler`: `*mut ()` →
`NonNull<BundleV2<'static>>`

The implementation-side `cast::<T>()` calls — the tell that the erasure
was unnecessary — are removed.

Sites where the type is genuinely not visible (`bun_event_loop` →
`bun_jsc::VirtualMachine`, `bun_js_parser` → `bun_bundler::Transpiler`)
are left as-is — that's the real layering boundary the pattern exists
for.

## Verification

- `cargo check --workspace` clean
- `cargo fmt --check` clean
- `bun run rust:check-all` (all 6 target triples)
- `bun bd` links and runs
- `grep -rn resolver_body src/` returns nothing
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.

2 participants