ast: shrink Expr/Stmt/Binding 24→16 bytes#32507
Conversation
Pack the five primitives that force align-8 into the AST down to align 4:
- StoreRef<T>: NonNull<T> → {NonZeroU32 hi, u16 lo} (48-bit ptr split at
bit 16; hi is non-zero for any address ≥ 64 KiB so Option<StoreRef<T>>
keeps its niche). 8 B align 8 → 8 B align 4.
- StoreSlice<T>, StoreStr: NonNull + len → u32 + u16 + u32 (48-bit ptr +
u32 len). 16 B align 8 → 12 B align 4.
- Ref: u64 → {u32 lo, u32 hi}. 8 B align 8 → 8 B align 4.
- E::Number: f64 → [u32; 2] via to_bits/from_bits (.value field becomes
.value()/::new()). 8 B align 8 → 8 B align 4.
With every inline Data payload now align-4, expr::Data / stmt::Data / B
drop 16→12 and Expr/Stmt/Binding drop 24→16. The cascade through every
heap node that embeds an Expr/Stmt/StoreStr is large: E::Binary 56→36,
E::Dot 48→36, E::If 72→48, S::SExpr 32→20, G::Property 128→88, G::Fn
112→88, G::Class 112→88.
Also: Option<Loc> fields → Loc with the existing Loc::EMPTY sentinel
(comma_after_spread, star_name_loc, NamedImport.alias_loc); Option<Ref>
fields → Ref with the existing Ref::NONE sentinel (LocRef.ref_ 24→12,
Fn.arguments_ref, Scope.label_ref, NamedImport.namespace_ref, all 25
runtime Imports.__* slots).
User-space addresses are ≤48 bits on every supported target — same
invariant TaggedPtr already relies on. ptr→int splitting forbids
const-eval, so EString::from_static is no longer const; the 5 static
EString singletons move to LazyLock and the dead EString::CLASS/TRUE/
FALSE/NULL/UNDEFINED consts are removed.
|
Updated 2:20 AM PT - Jun 19th, 2026
❌ @Jarred-Sumner, your commit c8753a6 has 1 failures in 🧪 To try this PR locally: bunx bun-pr 32507That installs a local version of the PR into your bun-32507 --bun |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe PR restructures three core AST data types to reduce in-memory size and eliminate defensive Option wrappers. ChangesAST Layout Compaction and Sentinel Propagation
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 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/ast/nodes.rs`:
- Around line 58-80: Replace the debug_assert! calls with assert! calls in
StoreRef::pack, StoreStr::new, and StoreSlice constructors (from_raw, new,
new_mut) to ensure pointer and length bounds validation occurs in release
builds, not just debug builds. Additionally, replace the unsafe
NonZeroU32::new_unchecked call in StoreRef::pack with
NonZeroU32::new().expect(...) to safely handle the case where the pointer falls
in the low 64 KiB range, eliminating the possibility of undefined behavior from
truncation or invalid pointer packing.
In `@src/bundler/AstBuilder.rs`:
- Around line 524-526: In the non-HMR import scan path around the alias_loc
assignment, replace the value `item.name.loc` with `item.alias_loc` to make the
error location handling consistent with the HMR branch. This ensures that the
alias_loc field correctly points to the alias token regardless of the execution
path, preventing mode-dependent error locations.
In `@src/js_parser/parse/parse_entry.rs`:
- Around line 1201-1203: The code at the usage sites of
`deferred_import.namespace.ref_` (in the blocks around lines 1201-1213 and
1446-1453) directly consumes this field without validation that it is not
`Ref::NONE`, even though the default `LocRef` initializes `ref_` to `Ref::NONE`.
Add an assertion (such as `assert_ne!(deferred_import.namespace.ref_,
Ref::NONE)`) at both usage sites before consuming `namespace.ref_`, or
alternatively add a clear documentation comment on the `DeferredImportNamespace`
type definition explicitly stating that `namespace.ref_` is guaranteed to be
valid and must never be default-initialized.
In `@src/js_parser/parse/parse_fn.rs`:
- Line 62: The code contains redundant field name assignments that trigger the
clippy lint. In the struct initialization where `ref_: ref_,` appears (at line
62 in parse_fn and also at line 447), remove the redundant `ref_:` prefix and
use the shorthand syntax by changing `ref_: ref_,` to just `ref_,`. Apply this
same fix to both occurrences to resolve the clippy::redundant_field_names lint
failure.
In `@src/js_parser/parser.rs`:
- Around line 671-695: The getter methods in this code that return Option<Ref>
are using map to extract the ref_ field directly, which can return
Some(Ref::NONE) instead of properly returning None. In both the method with the
series of if statements checking byte string names (checking for b"jsx",
b"jsxDEV", b"jsxs", b"Fragment", b"createElement") and the get_with_tag method
with the match expression over JSXImport enum, replace the map patterns that
extract ref_ with a proper nullable conversion (such as calling to_nullable() on
the ref_ value) to ensure that sentinel Ref::NONE values are converted to None
before being returned through the Option<Ref> API. Check the corresponding .zig
implementation files to confirm the correct nullability semantics that should be
preserved.
In `@src/js_parser/scan/scan_imports.rs`:
- Line 400: Replace redundant field initializers with Rust's field init
shorthand syntax to satisfy clippy. In the struct initialization, replace
`namespace_ref: namespace_ref` with just `namespace_ref`. Apply this same
shorthand pattern to all similar redundant field initializers throughout the
file where a field name is being initialized with a variable of the same name.
In `@src/js_parser/visit/visit_stmt.rs`:
- Line 1436: The field initializer at line 1436 uses the redundant form ref_:
ref_, where the field name matches the variable name. Replace this with the Rust
shorthand form ref_, which is more concise and satisfies clippy conventions.
Simply remove the "ref_: " part before the variable name, leaving only ref_, to
match the field-init shorthand pattern.
🪄 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: 06067fdc-ed25-420c-844c-5c47c751e81e
📒 Files selected for processing (67)
src/ast/b.rssrc/ast/e.rssrc/ast/expr.rssrc/ast/g.rssrc/ast/known_global.rssrc/ast/lib.rssrc/ast/nodes.rssrc/ast/runtime.rssrc/ast/s.rssrc/ast/scope.rssrc/ast/stmt.rssrc/bundler/AstBuilder.rssrc/bundler/LinkerContext.rssrc/bundler/ServerComponentParseTask.rssrc/bundler/defines.rssrc/bundler/linker_context/computeCrossChunkDependencies.rssrc/bundler/linker_context/convertStmtsForChunk.rssrc/bundler/linker_context/convertStmtsForChunkForDevServer.rssrc/bundler/linker_context/generateCodeForFileInChunkJS.rssrc/bundler/linker_context/generateCodeForLazyExport.rssrc/bundler/linker_context/postProcessJSChunk.rssrc/bundler/linker_context/renameSymbolsInChunk.rssrc/bundler/transpiler.rssrc/bunfig/bunfig.rssrc/ini/lib.rssrc/install/ConfigVersion.rssrc/install/lockfile/bun.lock.rssrc/install/migration.rssrc/install/npm.rssrc/install/pnpm.rssrc/js_parser/defines_table.rssrc/js_parser/fold.rssrc/js_parser/lib.rssrc/js_parser/lower/lower_decorators.rssrc/js_parser/lower/lower_esm_exports_hmr.rssrc/js_parser/p.rssrc/js_parser/parse/mod.rssrc/js_parser/parse/parse_entry.rssrc/js_parser/parse/parse_fn.rssrc/js_parser/parse/parse_import_export.rssrc/js_parser/parse/parse_prefix.rssrc/js_parser/parse/parse_property.rssrc/js_parser/parse/parse_stmt.rssrc/js_parser/parse/parse_typescript.rssrc/js_parser/parser.rssrc/js_parser/repl_transforms.rssrc/js_parser/scan/scan_imports.rssrc/js_parser/scan/scan_side_effects.rssrc/js_parser/visit/mod.rssrc/js_parser/visit/visit_binary.rssrc/js_parser/visit/visit_expr.rssrc/js_parser/visit/visit_stmt.rssrc/js_parser_jsc/Macro.rssrc/js_parser_jsc/expr_jsc.rssrc/js_printer/lib.rssrc/parsers/json.rssrc/parsers/json5.rssrc/parsers/toml.rssrc/parsers/yaml.rssrc/runtime/api/JSON5Object.rssrc/runtime/api/JSTranspiler.rssrc/runtime/api/YAMLObject.rssrc/runtime/bake/bake_body.rssrc/runtime/cli/audit_command.rssrc/runtime/cli/pm_pkg_command.rssrc/runtime/cli/upgrade_command.rssrc/sourcemap/lib.rs
| alias: Some(item.alias), | ||
| alias_loc: Some(item.name.loc), | ||
| namespace_ref: Some(st.namespace_ref), | ||
| alias_loc: item.name.loc, | ||
| namespace_ref: st.namespace_ref, |
There was a problem hiding this comment.
Use item.alias_loc in the non-HMR import scan path.
This branch writes alias_loc: item.name.loc while the HMR branch uses item.alias_loc, which makes error locations mode-dependent and can point at the wrong token.
Suggested fix
- alias_loc: item.name.loc,
+ alias_loc: item.alias_loc,📝 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.
| alias: Some(item.alias), | |
| alias_loc: Some(item.name.loc), | |
| namespace_ref: Some(st.namespace_ref), | |
| alias_loc: item.name.loc, | |
| namespace_ref: st.namespace_ref, | |
| alias: Some(item.alias), | |
| alias_loc: item.alias_loc, | |
| namespace_ref: st.namespace_ref, |
🤖 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/AstBuilder.rs` around lines 524 - 526, In the non-HMR import scan
path around the alias_loc assignment, replace the value `item.name.loc` with
`item.alias_loc` to make the error location handling consistent with the HMR
branch. This ensures that the alias_loc field correctly points to the alias
token regardless of the execution path, preventing mode-dependent error
locations.
| deferred_import.namespace.ref_, | ||
| deferred_import.namespace.loc, | ||
| deferred_import.import_record_id, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect where imports_to_convert_from_require entries are created/updated
rg -n -C3 'imports_to_convert_from_require|namespace\.ref_|namespace_ref' src/js_parser
# Inspect the deferred import/namespace type definitions and defaults
rg -n -C3 'struct .*Deferred|namespace:|LocRef|Ref::NONE' src/js_parser src/astRepository: oven-sh/bun
Length of output: 50368
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Get the specific lines mentioned in the review
echo "=== Lines 1201-1203 in parse_entry.rs ==="
sed -n '1195,1210p' src/js_parser/parse/parse_entry.rs
echo ""
echo "=== Lines 1446-1450 in parse_entry.rs ==="
sed -n '1440,1460p' src/js_parser/parse/parse_entry.rs
echo ""
echo "=== Structure definition for deferred imports ==="
rg -n "struct.*Deferred|pub.*deferred" src/js_parser/parse/parse_entry.rs | head -20
echo ""
echo "=== Where deferred_import is pushed/added to imports_to_convert_from_require ==="
rg -n "imports_to_convert_from_require.*push\|imports_to_convert_from_require.*insert\|Deferred.*{" src/js_parser/parse/parse_entry.rs | head -30Repository: oven-sh/bun
Length of output: 2335
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Try to find struct definitions for deferred imports
echo "=== Searching for Deferred struct/type definition ==="
rg -n "struct Deferred|Deferred {" src/js_parser/parse/ --type rust
echo ""
echo "=== Type of namespace field ==="
rg -n "namespace\s*:" src/js_parser/parse/parse_entry.rs | head -5
echo ""
echo "=== Check if ns_ref is used as VecExt::append arg ==="
sed -n '1213,1220p' src/js_parser/parse/parse_entry.rs
echo ""
echo "=== Check namespace initialization in code ==="
rg -n "namespace\s*=" src/js_parser/parse/parse_entry.rs | head -20Repository: oven-sh/bun
Length of output: 243
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Try to find struct definitions for deferred imports
echo "=== Searching for Deferred struct/type definition ==="
rg -n "struct Deferred" src/js_parser/parse/ --type rust
echo ""
echo "=== Check imports_to_convert_from_require type ==="
rg -n "imports_to_convert_from_require\s*:" src/js_parser/parse/parse_entry.rs
echo ""
echo "=== Check where imports_to_convert_from_require entries are added ==="
rg -n "imports_to_convert_from_require.push\|imports_to_convert_from_require.append" src/js_parser/parse/parse_entry.rs | head -20
echo ""
echo "=== Check the context around line 1213 where ns_ref is used ==="
sed -n '1210,1230p' src/js_parser/parse/parse_entry.rs
echo ""
echo "=== Look for Namespace struct definition ==="
rg -n "pub struct Namespace" src/js_parser/ -A 5Repository: oven-sh/bun
Length of output: 109
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Get more context around the actual usage lines
echo "=== Full context around 1201-1203 ==="
sed -n '1180,1230p' src/js_parser/parse/parse_entry.rs
echo ""
echo "=== Full context around 1446-1450 ==="
sed -n '1430,1480p' src/js_parser/parse/parse_entry.rsRepository: oven-sh/bun
Length of output: 5985
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find where imports_to_convert_from_require is populated
echo "=== Where imports_to_convert_from_require is modified ==="
rg -n "imports_to_convert_from_require\[|imports_to_convert_from_require.push" src/js_parser/parse/parse_entry.rs | head -20
echo ""
echo "=== Look at visit/mod.rs lines around 344-347 where namespace.ref_ is assigned ==="
sed -n '340,355p' src/js_parser/visit/mod.rs
echo ""
echo "=== Check what type namespace is and what ref_ is ==="
rg -n "namespace.*Ref|struct.*Namespace" src/js_parser/ -A 3 | head -40Repository: oven-sh/bun
Length of output: 3431
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find LocRef definition
echo "=== LocRef struct definition ==="
rg -n "pub struct LocRef|struct LocRef" src/js_parser/ -A 3
echo ""
echo "=== How are imports_to_convert_from_require entries initially created? ==="
rg -n "DeferredImportNamespace\s*{" src/js_parser/parse/parse_entry.rs -B 5 -A 5
echo ""
echo "=== Check where imports_to_convert_from_require is first populated ==="
rg -n "imports_to_convert_from_require.push\|\.push.*DeferredImportNamespace" src/js_parser/ | head -20
echo ""
echo "=== Look for initialization of imports_to_convert_from_require ==="
rg -n "imports_to_convert_from_require\s*=" src/js_parser/parser.rs | head -10Repository: oven-sh/bun
Length of output: 88
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Try different pattern for LocRef
echo "=== Search for LocRef definition ==="
rg -n "LocRef" src/js_parser/ -A 2 | head -30
echo ""
echo "=== Where are DeferredImportNamespace entries added ==="
rg -n "DeferredImportNamespace" src/js_parser/ --type rust | head -30
echo ""
echo "=== Check parse_entry.rs to find where imports_to_convert_from_require is populated ==="
rg -n "push\|\.append" src/js_parser/parse/parse_entry.rs | grep "import\|deferred" | head -20Repository: oven-sh/bun
Length of output: 2393
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look at where DeferredImportNamespace is pushed
echo "=== Context around line 1288 in p.rs where DeferredImportNamespace is pushed ==="
sed -n '1280,1310p' src/js_parser/p.rs
echo ""
echo "=== LocRef definition in js_ast ==="
rg -n "pub struct LocRef\|pub.*LocRef.*Ref" src/js_ast/ -A 2
echo ""
echo "=== Ref::NONE definition ==="
rg -n "pub const NONE\|const NONE" src/js_ast/ | head -10Repository: oven-sh/bun
Length of output: 1576
🏁 Script executed:
#!/bin/bash
set -euoripefail
# Find LocRef in the AST files
echo "=== Finding LocRef definition ==="
fd -e rs -e zig | xargs grep -l "struct LocRef" | head -5
echo ""
echo "=== LocRef in js_ast files ==="
find src -name "*.rs" -o -name "*.zig" | xargs grep -l "LocRef" | head -10
echo ""
echo "=== Looking in ast directory ==="
ls -la src/ast/ 2>/dev/null | head -20 || echo "ast not found"
echo ""
echo "=== Check js_parser/ast if it exists ==="
ls -la src/js_parser/ast/ 2>/dev/null | head -20 || echo "ast not found in js_parser"
echo ""
echo "=== Searching for LocRef definition more broadly ==="
rg "LocRef\s*\{" src/ -A 3 | head -40Repository: oven-sh/bun
Length of output: 731
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find LocRef in the AST files
echo "=== Finding LocRef definition ==="
find src -name "*.rs" | xargs grep -l "struct LocRef" | head -5
echo ""
echo "=== Looking for LocRef with ref_ field ==="
rg "LocRef.*ref_|pub struct LocRef" src/ -B 1 -A 3 | head -50Repository: oven-sh/bun
Length of output: 2016
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Now look at Ref::NONE
echo "=== Ref::NONE definition ==="
rg -n "pub const NONE|const NONE|NONE.*Ref" src/ast/ src/js_ast/ -A 1 | head -20
echo ""
echo "=== Check if ref_ can be Ref::NONE by looking at initialization flow ==="
echo "Initial creation at p.rs line 1288 context:"
sed -n '1275,1305p' src/js_parser/p.rs
echo ""
echo "=== Check the reassignment in visit/mod.rs - is there any guard? ==="
sed -n '335,360p' src/js_parser/visit/mod.rs
echo ""
echo "=== Look at the actual usage at line 1213 in parse_entry.rs ==="
sed -n '1210,1220p' src/js_parser/parse/parse_entry.rsRepository: oven-sh/bun
Length of output: 558
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check if LocRef has a Default impl and what it defaults to
echo "=== LocRef Default implementation ==="
rg -n "impl.*Default.*LocRef|LocRef.*Default" src/ast/ -A 5
echo ""
echo "=== Check if Ref::NONE is used in any LocRef initialization ==="
rg -n "LocRef.*Ref::NONE\|Ref::NONE.*LocRef" src/js_parser/ | head -20
echo ""
echo "=== Now check the actual code flow - where is deferred_import first created ==="
echo "Looking at visit/mod.rs context more carefully:"
sed -n '330,360p' src/js_parser/visit/mod.rs
echo ""
echo "=== Check if there's a guard before using namespace.ref_ at line 1213 ==="
sed -n '1195,1225p' src/js_parser/parse/parse_entry.rsRepository: oven-sh/bun
Length of output: 427
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Key finding: LocRef default is Ref::NONE. Let's verify the initialization flow
echo "=== Full initialization at p.rs 1288 ==="
sed -n '1275,1310p' src/js_parser/p.rs
echo ""
echo "=== Full visit/mod.rs context where ref_ is reassigned ==="
sed -n '320,365p' src/js_parser/visit/mod.rs
echo ""
echo "=== Check: are there any code paths where a DeferredImportNamespace entry is created but not filled? ==="
rg -n "DeferredImportNamespace\s*{" src/js_parser/ -B 5 -A 10
echo ""
echo "=== Verify: at usage site parse_entry.rs, what guards exist? ==="
sed -n '1180,1215p' src/js_parser/parse/parse_entry.rsRepository: oven-sh/bun
Length of output: 4463
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Fix the regex
echo "=== Where DeferredImportNamespace is created ==="
rg -n "DeferredImportNamespace" src/js_parser/ --type rust -B 3 -A 3 | head -60
echo ""
echo "=== In visit/mod.rs, what is id.r#ref and can it be NONE? ==="
sed -n '338,355p' src/js_parser/visit/mod.rs
echo ""
echo "=== Check BIdentifier ref_ assignment in parser ==="
rg -n "BIdentifier.*ref_|r#ref\s*=" src/js_parser/parse/parse_stmt.rs | head -20Repository: oven-sh/bun
Length of output: 3263
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Let's check if there was prior validation that was removed
echo "=== Check git history/blame for the problematic lines ==="
git log -p --all -S "deferred_import.namespace.ref_" -- src/js_parser/parse/parse_entry.rs | head -100
echo ""
echo "=== Check if there were guards that checked Ref validity before ==="
rg -n "Ref::NONE\|if.*ref.*==" src/js_parser/parse/parse_entry.rs | head -20
echo ""
echo "=== Look for similar patterns - how are other ref_ fields validated ==="
rg -n "VecExt::append.*ref|assert.*ref|ref_.*NONE" src/js_parser/parse/parse_entry.rs | head -15Repository: oven-sh/bun
Length of output: 182
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Let's understand the complete flow - when is an entry added to imports_to_convert_from_require?
echo "=== All places where .push is called on imports_to_convert_from_require ==="
rg -n "imports_to_convert_from_require" src/js_parser/ --type rust | grep -i "push\|insert"
echo ""
echo "=== Check if any entry could have a stale/default LocRef ==="
rg -n "imports_to_convert_from_require\[" src/js_parser/ --type rust -B 2 -A 2 | head -40
echo ""
echo "=== Verify: are ALL entries created with a valid ref at initialization? ==="
echo "Looking at just p.rs line 1288:"
sed -n '1285,1302p' src/js_parser/p.rsRepository: oven-sh/bun
Length of output: 131
Add explicit assertions that namespace.ref_ is valid before consuming it, or document the invariant guaranteeing it cannot be Ref::NONE.
Based on code inspection, DeferredImportNamespace entries are created with a valid namespace.ref_ from new_symbol() in p.rs:1288. However, LocRef's Default implementation sets ref_ to Ref::NONE (ast/nodes.rs:634–639), and both usage sites at lines 1201–1213 and 1446–1453 consume namespace.ref_ directly without validating it is not Ref::NONE. If any code path instantiates an entry with a default LocRef rather than via the explicit initialization, or if the invariant is subtle, add an assertion at the point of use to prevent silent failures. Alternatively, document clearly in the DeferredImportNamespace type definition that namespace.ref_ is guaranteed to be non-NONE and must not be default-initialized.
🤖 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/js_parser/parse/parse_entry.rs` around lines 1201 - 1203, The code at the
usage sites of `deferred_import.namespace.ref_` (in the blocks around lines
1201-1213 and 1446-1453) directly consumes this field without validation that it
is not `Ref::NONE`, even though the default `LocRef` initializes `ref_` to
`Ref::NONE`. Add an assertion (such as
`assert_ne!(deferred_import.namespace.ref_, Ref::NONE)`) at both usage sites
before consuming `namespace.ref_`, or alternatively add a clear documentation
comment on the `DeferredImportNamespace` type definition explicitly stating that
`namespace.ref_` is guaranteed to be valid and must never be
default-initialized.
| return self.jsx.map(|jsx| jsx.ref_); | ||
| } | ||
| if name == b"jsxDEV" { | ||
| return self | ||
| .jsx_dev | ||
| .map(|jsx| jsx.ref_.expect("infallible: ref bound")); | ||
| return self.jsx_dev.map(|jsx| jsx.ref_); | ||
| } | ||
| if name == b"jsxs" { | ||
| return self | ||
| .jsxs | ||
| .map(|jsxs| jsxs.ref_.expect("infallible: ref bound")); | ||
| return self.jsxs.map(|jsxs| jsxs.ref_); | ||
| } | ||
| if name == b"Fragment" { | ||
| return self | ||
| .fragment | ||
| .map(|f| f.ref_.expect("infallible: ref bound")); | ||
| return self.fragment.map(|f| f.ref_); | ||
| } | ||
| if name == b"createElement" { | ||
| return self | ||
| .create_element | ||
| .map(|c| c.ref_.expect("infallible: ref bound")); | ||
| return self.create_element.map(|c| c.ref_); | ||
| } | ||
| None | ||
| } | ||
|
|
||
| pub(crate) fn get_with_tag(&self, tag: JSXImport) -> Option<Ref> { | ||
| match tag { | ||
| JSXImport::Jsx => self.jsx.map(|jsx| jsx.ref_.expect("infallible: ref bound")), | ||
| JSXImport::JsxDEV => self | ||
| .jsx_dev | ||
| .map(|jsx| jsx.ref_.expect("infallible: ref bound")), | ||
| JSXImport::Jsxs => self | ||
| .jsxs | ||
| .map(|jsxs| jsxs.ref_.expect("infallible: ref bound")), | ||
| JSXImport::Fragment => self | ||
| .fragment | ||
| .map(|f| f.ref_.expect("infallible: ref bound")), | ||
| JSXImport::CreateElement => self | ||
| .create_element | ||
| .map(|c| c.ref_.expect("infallible: ref bound")), | ||
| JSXImport::Jsx => self.jsx.map(|jsx| jsx.ref_), | ||
| JSXImport::JsxDEV => self.jsx_dev.map(|jsx| jsx.ref_), | ||
| JSXImport::Jsxs => self.jsxs.map(|jsxs| jsxs.ref_), | ||
| JSXImport::Fragment => self.fragment.map(|f| f.ref_), | ||
| JSXImport::CreateElement => self.create_element.map(|c| c.ref_), | ||
| } |
There was a problem hiding this comment.
Preserve nullable semantics when projecting sentinel-backed refs
map(|...| ...ref_) can return Some(Ref::NONE), which leaks an invalid ref through an Option<Ref> API. For these getters, convert sentinel refs to None (e.g. via to_nullable()) to preserve the prior nullability contract.
Suggested fix
pub(crate) fn get(&self, name: &[u8]) -> Option<Ref> {
if name == b"jsx" {
- return self.jsx.map(|jsx| jsx.ref_);
+ return self.jsx.and_then(|jsx| jsx.ref_.to_nullable());
}
if name == b"jsxDEV" {
- return self.jsx_dev.map(|jsx| jsx.ref_);
+ return self.jsx_dev.and_then(|jsx| jsx.ref_.to_nullable());
}
if name == b"jsxs" {
- return self.jsxs.map(|jsxs| jsxs.ref_);
+ return self.jsxs.and_then(|jsxs| jsxs.ref_.to_nullable());
}
if name == b"Fragment" {
- return self.fragment.map(|f| f.ref_);
+ return self.fragment.and_then(|f| f.ref_.to_nullable());
}
if name == b"createElement" {
- return self.create_element.map(|c| c.ref_);
+ return self.create_element.and_then(|c| c.ref_.to_nullable());
}
None
}
pub(crate) fn get_with_tag(&self, tag: JSXImport) -> Option<Ref> {
match tag {
- JSXImport::Jsx => self.jsx.map(|jsx| jsx.ref_),
- JSXImport::JsxDEV => self.jsx_dev.map(|jsx| jsx.ref_),
- JSXImport::Jsxs => self.jsxs.map(|jsxs| jsxs.ref_),
- JSXImport::Fragment => self.fragment.map(|f| f.ref_),
- JSXImport::CreateElement => self.create_element.map(|c| c.ref_),
+ JSXImport::Jsx => self.jsx.and_then(|jsx| jsx.ref_.to_nullable()),
+ JSXImport::JsxDEV => self.jsx_dev.and_then(|jsx| jsx.ref_.to_nullable()),
+ JSXImport::Jsxs => self.jsxs.and_then(|jsxs| jsxs.ref_.to_nullable()),
+ JSXImport::Fragment => self.fragment.and_then(|f| f.ref_.to_nullable()),
+ JSXImport::CreateElement => self.create_element.and_then(|c| c.ref_.to_nullable()),
}
}As per coding guidelines, when porting/fixing behavior, .zig siblings should be treated as semantic source-of-truth for nullability behavior.
🤖 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/js_parser/parser.rs` around lines 671 - 695, The getter methods in this
code that return Option<Ref> are using map to extract the ref_ field directly,
which can return Some(Ref::NONE) instead of properly returning None. In both the
method with the series of if statements checking byte string names (checking for
b"jsx", b"jsxDEV", b"jsxs", b"Fragment", b"createElement") and the get_with_tag
method with the match expression over JSXImport enum, replace the map patterns
that extract ref_ with a proper nullable conversion (such as calling
to_nullable() on the ref_ value) to ensure that sentinel Ref::NONE values are
converted to None before being returned through the Option<Ref> API. Check the
corresponding .zig implementation files to confirm the correct nullability
semantics that should be preserved.
Source: Coding guidelines
…t fixes
Ref::bits() and StoreRef::as_ptr() were doing two u32 loads + shift|or
to reconstruct the 64-bit value, which showed as a ~5% bundler regression
in the minify-renamer path (millions of HashMap<Ref> eq/hash). Both
structs are repr(C) {u32, u32} with no padding, so transmute<_, u64> is
sound and compiles to one mov — same cost as the old NonNull<T>/u64
representation. StoreRef drops its NonZeroU32 niche (Option<StoreRef<T>>
grows 8→12 at the five storage sites that use it; the Data/Expr niche
comes from spare discriminant values and is unaffected).
Also: clippy redundant-field-names autofix; refresh the stale layout
derivation comments above the b.rs/expr.rs/stmt.rs size asserts.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/ast/lib.rs (1)
249-258:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftEnforce
Refwidth limits in release builds.
pack()masksinnerafter only adebug_assert!, so a release build silently aliases refs onceinner_indexexceeds 28 bits. This is reachable from source-derived offsets/symbol counts per the comment, so validate before packing or return a recoverable error at the layer that creates the oversized ref. As per coding guidelines, size/index arithmetic on external data must be release-validated because debug assertions compile out.🤖 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/ast/lib.rs` around lines 249 - 258, The pack function relies on debug_assert to validate that inner does not exceed 28 bits, but this assertion is stripped in release builds leaving the code to silently alias refs via masking. Replace the debug_assert with a runtime validation that works in both debug and release builds. Either convert the pack function to return a Result type and propagate errors to the caller, or use a regular assert to panic on invalid input. Ensure the validation occurs before the bitwise operations on inner to prevent silent data corruption when inner_index exceeds the INNER_BITS limit in production.Source: Coding guidelines
src/ast/nodes.rs (1)
102-107:⚠️ Potential issue | 🟠 MajorMake
from_static()unsafe to encode the immutable-static mutation contract in the type system.
StoreRef::from_static()is a safe constructor that returns aStoreRef<T>capable of mutable dereference to immutable static storage. This violates memory safety: the comment documents thatDerefMuton the result is UB, but the type system does not enforce this. Convert tounsafe fnwith a# Safetysection stating that the returnedStoreRefmust never be mutably dereferenced, or return a read-only wrapper type that omits theDerefMutimpl.- pub fn from_static(r: &'static T) -> Self { + /// # Safety + /// + /// The returned `StoreRef` must never be used for mutation via `DerefMut`. + pub unsafe fn from_static(r: &'static T) -> Self { // Provenance is shared/read-only: the pointee is *never* written // through — `DerefMut` on a `StoreRef` produced here is UB and callers // must not do so (audited: only `Deref`/`get()` reads occur). Self::pack(core::ptr::from_ref(r).cast_mut()) }🤖 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/ast/nodes.rs` around lines 102 - 107, The `from_static()` method in the `StoreRef` implementation currently encodes an unsafe contract (that the returned value must never be mutably dereferenced) only in comments, but does not enforce this through the type system. Convert the `from_static` function from a safe function to an `unsafe fn`, and add a `# Safety` documentation section that explicitly states the safety requirement: callers must guarantee that the returned `StoreRef` is never mutably dereferenced, as doing so on immutable static storage would constitute undefined behavior.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 `@src/ast/lib.rs`:
- Around line 249-258: The pack function relies on debug_assert to validate that
inner does not exceed 28 bits, but this assertion is stripped in release builds
leaving the code to silently alias refs via masking. Replace the debug_assert
with a runtime validation that works in both debug and release builds. Either
convert the pack function to return a Result type and propagate errors to the
caller, or use a regular assert to panic on invalid input. Ensure the validation
occurs before the bitwise operations on inner to prevent silent data corruption
when inner_index exceeds the INNER_BITS limit in production.
In `@src/ast/nodes.rs`:
- Around line 102-107: The `from_static()` method in the `StoreRef`
implementation currently encodes an unsafe contract (that the returned value
must never be mutably dereferenced) only in comments, but does not enforce this
through the type system. Convert the `from_static` function from a safe function
to an `unsafe fn`, and add a `# Safety` documentation section that explicitly
states the safety requirement: callers must guarantee that the returned
`StoreRef` is never mutably dereferenced, as doing so on immutable static
storage would constitute undefined behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b120d960-9a95-4ab3-adb7-8e7a4280ac86
📒 Files selected for processing (13)
src/ast/b.rssrc/ast/expr.rssrc/ast/lib.rssrc/ast/nodes.rssrc/ast/stmt.rssrc/bundler/AstBuilder.rssrc/bundler/ServerComponentParseTask.rssrc/bundler/linker_context/computeCrossChunkDependencies.rssrc/bundler/transpiler.rssrc/js_parser/p.rssrc/js_parser/parse/parse_fn.rssrc/js_parser/scan/scan_imports.rssrc/js_parser/visit/visit_stmt.rs
There was a problem hiding this comment.
Thanks for the comment + clippy fixes in 208ffe3 — the bug-hunter pass on the new head came back clean. Deferring to human review given the scope: unsafe pointer-packing in StoreRef/StoreSlice/Ref plus the Option→sentinel migration across ~67 files is the kind of layout/invariant change that warrants a maintainer's eyes.
Extended reasoning...
Overview
This PR repacks the core AST primitives to align-4 so Expr/Stmt/Binding shrink 24→16 bytes: Ref becomes {u32,u32} with transmute-based bits(), StoreRef<T> stores the address as [u32;2], StoreStr/StoreSlice split a 48-bit pointer + u32 len into 12 bytes, and E::Number stores f64 as [u32;2]. It then propagates two consequences across 67 files: (a) E::Number { value } → E::Number::new()/.value() everywhere, and (b) Option<Ref>/Option<Loc> storage on LocRef, NamedImport, Scope.label_ref, Fn.arguments_ref, Import.star_name_loc, comma_after_spread, and runtime::Imports is replaced with Ref::NONE/Loc::EMPTY sentinels, with .to_nullable() bridging back to Option at read sites.
A second commit (208ffe3) addressed my earlier nit about stale layout-derivation comments in b.rs/expr.rs/stmt.rs and fixed the redundant_field_names clippy failures CodeRabbit flagged — the diff now shows updated derivations and field-init shorthand throughout.
Security risks
No direct attack-surface change (no auth/crypto/network). The memory-safety angle is the new unsafe in nodes.rs/lib.rs: transmute<[u32;2], usize> for pointer reconstruction and transmute<Ref, u64> for the hot hash key, both gated by a const cfg!(target_endian = "little") assert and the existing 48-bit-VA invariant TaggedPtr already relies on. The debug_assert!(bits >> 48 == 0) and len <= u32::MAX guards in StoreStr/StoreSlice are debug-only, which CodeRabbit called out; whether those should be release assert!s on a hot constructor is a perf/safety trade-off the maintainers should weigh.
Level of scrutiny
High. This is hot-path transpiler/bundler infrastructure — the parser, visitor, printer, linker, and every config/lockfile parser flow through these types. The Option→sentinel migration is mechanically simple per-site but spans ~100+ call sites where a missed .to_nullable() or an is_some()→!is_empty() inversion would silently change behavior (e.g. star_name_loc, comma_after_spread). The const size/align asserts catch layout regressions but not semantic ones.
Other factors
The bug-hunting system found nothing on this revision. The remaining CodeRabbit inline comments are either already fixed (clippy shorthand), pre-existing behavior unrelated to this PR (alias_loc: item.name.loc in AstBuilder), or design suggestions (release-mode asserts, to_nullable() in JSX getters where the prior code also expect-unwrapped). None block, but the overall scale and the unsafe layout work put this well outside auto-approval territory.
The {u32, u32} representation for Ref/StoreRef/StoreSlice/StoreStr was
classified as ScalarPair by rustc and passed in two registers, forcing
shl|or recombination at every call site (visible as +20% in
symbol::Map::follow and +30-60% in the lexer string-literal path).
#[repr(C, packed(4))] on the original single-scalar layouts (Ref(u64),
StoreRef(NonNull<T>), StoreSlice/Str{NonNull, u32}, Number{f64}) lowers
alignment to 4 with the same struct sizes but keeps single-register
passing, the NonNull niche, const-fn constructors, and pointer
provenance. Codegen is identical to the pre-shrink layout except for
4-aligned 8-byte loads (free on x64/arm64).
Bench vs main-base (same LTO, GOMAXPROCS=2):
transformSync 11.2 MB 136 -> 125 ms (-8%)
RSS per parse 174 -> 148 MB (-15%)
bundler three10x 328 -> 316 ms (-4%)
bundler 10k 523 -> 509 ms (-3%)
bundler 100k 3.87 -> 3.79 s (-2%)
Also reverts the LazyLock workaround for static EString (StoreStr::new
is const again).
Both fields are inline on every instance but populated rarely: - Symbol.namespace_alias (~28 B) is only set for import-clause names; >95% of symbols are locals/temps where it stays None. - Part.import_symbol_property_uses (~80 B ArrayHashMap) is only non-empty for `import * as ns; ns.foo` access patterns. Boxing them takes Symbol 68->48 (-40% vs main 80) and Part 280->200. Also removes the long-dead ImportRecord.module_id field. Bench vs main 3b24ca9 (same LTO, GOMAXPROCS=2): transformSync 11.2 MB 136 -> 120 ms (-12%) bundler three10x 327 -> 309 ms (-6%) bundler 10k 528 -> 484 ms (-8%) bundler 100k 3.87 -> 3.79 s (-2%)
The 6 Symbol bools collapse into a u8 SymbolFlags. Symbol stays at 48 B (the saved bytes were padding under the align-8 Box pointer), but merge_contents_with becomes one OR instead of six branches. Part.import_symbol_property_uses moves from Box<_> to AstBox<_> (= Box<_, AstAlloc>) so its 80 B header lives in the same spill heap as the rest of Part and is reclaimed on heap reset rather than relying on Drop. Adds bun_alloc::AstBox/ast_box() alongside AstVec.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/bundler/AstBuilder.rs (1)
385-410:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPush the rewritten local statement after clearing
is_export.Line 391 mutates the copied
st, but Line 410 pushes*stmt, so the originalexportremains in the HMR wrapper path.🐛 Proposed fix
- hmr_stmts.push(*stmt); + hmr_stmts.push(Stmt::alloc(st, stmt.loc));🤖 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/AstBuilder.rs` around lines 385 - 410, The code modifies the copied `st` struct by setting `st.is_export = false` but then pushes the original `*stmt` to `hmr_stmts` instead of using the modified version. This means the HMR wrapper path will still have the export flag set. After mutating `st` to clear the `is_export` flag, reconstruct the statement with the modified `st` data and push that reconstructed statement to `hmr_stmts` instead of pushing `*stmt` directly.
🤖 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/bun_alloc/ast_alloc.rs`:
- Around line 331-339: The public type `AstBox<T>` and function `ast_box<T>`
lack documentation about the drop-safety contract they enforce. Add doc comments
to both `AstBox<T>` and `ast_box<T>` that explicitly document the requirement
that the generic type parameter `T` must be AST-lifetime-safe and must not rely
on `Drop` for correctness, since `AstAlloc::deallocate` is intentionally a
no-op. The documentation should clarify that any required cleanup (dropping heap
allocations, refcounts, or file descriptors) must happen before the arena
resets, or the value must be explicitly dropped before that point. Reference the
coding guideline about arena-backed values in `bun_alloc::MimallocArena` to
reinforce this contract.
---
Outside diff comments:
In `@src/bundler/AstBuilder.rs`:
- Around line 385-410: The code modifies the copied `st` struct by setting
`st.is_export = false` but then pushes the original `*stmt` to `hmr_stmts`
instead of using the modified version. This means the HMR wrapper path will
still have the export flag set. After mutating `st` to clear the `is_export`
flag, reconstruct the statement with the modified `st` data and push that
reconstructed statement to `hmr_stmts` instead of pushing `*stmt` directly.
🪄 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: 6113935d-08f2-4567-a76c-f55d6028f2bf
📒 Files selected for processing (23)
src/ast/import_record.rssrc/ast/nodes.rssrc/ast/symbol.rssrc/bun_alloc/ast_alloc.rssrc/bun_alloc/lib.rssrc/bundler/AstBuilder.rssrc/bundler/HTMLScanner.rssrc/bundler/LinkerContext.rssrc/bundler/ParseTask.rssrc/bundler/linker_context/doStep5.rssrc/bundler/linker_context/postProcessJSChunk.rssrc/bundler/linker_context/prepareCssAstsForChunk.rssrc/css/css_parser.rssrc/js_parser/lower/lower_esm_exports_hmr.rssrc/js_parser/p.rssrc/js_parser/parse/parse_fn.rssrc/js_parser/scan/scan_imports.rssrc/js_parser/scan/scan_symbols.rssrc/js_parser/visit/mod.rssrc/js_parser/visit/visit_expr.rssrc/js_parser/visit/visit_stmt.rssrc/js_printer/lib.rssrc/js_printer/renamer.rs
💤 Files with no reviewable changes (5)
- src/bundler/linker_context/prepareCssAstsForChunk.rs
- src/ast/import_record.rs
- src/bundler/HTMLScanner.rs
- src/css/css_parser.rs
- src/bundler/linker_context/postProcessJSChunk.rs
Symbol lives in ArenaVec<'a, Symbol> alongside Part, so Drop is not guaranteed to run when the arena resets. Move the box header into the AstAlloc spill heap so it's reclaimed on reset like the rest of the per-file AST data. NamespaceAlias is already arena-lifetime (its alias: StoreStr points into the parse arena), so AstAlloc is the correct allocator.
repr(C, align(8)) on EString and prefer_template moved after rope_len. The StoreStr packed(4) change in #32507 let repr(Rust) place data.ptr at offset 20, so static EString::from_static initializers (5 sites in runtime/bake/bake_body.rs and bundler/defines.rs) emit ARM64_RELOC_UNSIGNED at a non-8-aligned offset, which arm64 ld rejects. repr(C) pins data (the only field needing a static relocation) at offset 0. EString is StoreRef-boxed so Expr stays 16; EString stays 40. Also: Arrow size assert 48→32 (StoreSlice shrink); two or_insert_with→or_default clippy. Claude-Session: https://claude.ai/code/session_01FShraojuNHfmfS8q2DLDZb
Mechanical: Some(x)→x, .and_then(|n| n.ref_)→.map(|n| n.ref_).filter(is_valid),
star_name_loc None→Loc::EMPTY, E::Number{value}→E::Number::new(x), n.value→.value().
Claude-Session: https://claude.ai/code/session_01FShraojuNHfmfS8q2DLDZb
Packs the align-8 AST primitives down to align 4 via
#[repr(C, packed(4))]soExpr/Stmt/Bindingdrop to 16 bytes (−33%), and boxes the rarely-populatedSymbol.namespace_alias/Part.import_symbol_property_usesso the linker hot data shrinks too.What changed
Ref/StoreRef<T>#[repr(C, packed(4))]— same single-scalar codegen, niche,const fn, provenanceStoreSlice<T>/StoreStr#[repr(C, packed(4))]+len: usize→u32E::Number#[repr(C, packed(4))];.value→.value()/::new()Option<Loc>/Option<Ref>storageLoc::EMPTY/Ref::NONEsentinelsSymbol.namespace_aliasOption<Box<NamespaceAlias>>(None for >95% of symbols)Part.import_symbol_property_usesOption<AstBox<…>>(only set forimport * as ns; ns.foo)Symbol6× boolSymbolFlags: u8bitflagsCascade
Expr/Stmt/BindingSymbolPartLocRefE::BinaryE::ArrowG::Fn/G::ClassBench (vs main 3b24ca9, same LTO, GOMAXPROCS=2)
transformSync11.2 MBthree10x10k100k