Skip to content

js_parser: roll back a removed statement's tracked uses along with its use counts - #45345

Open
robobun wants to merge 6 commits into
mainfrom
robobun/608cc83b/dead-part-revert
Open

robobun wants to merge 6 commits into
mainfrom
robobun/608cc83b/dead-part-revert

Conversation

@robobun

@robobun robobun commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Fix

  • Both counts become an AccountedUses: the total and the open part's share. revert_dead_part takes back the uses, that share, the property reads (a.x) and the "escapes" mark.
  • It no longer zeroes the symbols the statement declared (ReferenceError: ns is not defined), except the wrapper's module and exports. A removed typeof require; builds again.
  • Verified: 19 new cases in bundler_dynamic_import_dce.test.ts and bundler_cjs2esm.test.ts fail on main. test/bundler/ has no new failure.

Background

  • A part is one top-level statement. The parser drops it when the visit leaves it empty.
  • A tracked use (ns.a, !ns) counts in use_count_estimate and in a second count. The namespace escapes when the first is larger.
  • Considered a note list per part, and a count in each part-use entry (+4 bytes each).

Downsides

  • Builds that hit these shapes get larger: the exports and the wrapper stay.
  • A kept statement runs +3 instructions (JS) or -15 (TypeScript) of about 1,400. A tracked read runs +14, a module.exports rewrite +12.
  • The parser struct grows 8 bytes. .text grows 4,608 bytes.
Notes

The branch has cf414ea1e0 (the roll-back), 95583045fd (the require fold on the revisit), 65d3b8bcc0 (comments only), a merge of main at 91e7e3b140, 40864409bd (property reads, the escape mark, the module / exports exception, more tests), and a second merge of main at ea6a2c7b7c. Unless a line says otherwise, a number below compares 40864409bd with the parser sources of main at 91e7e3b140 in the same tree. After the second merge the two test files pass again (123 and 344, debug + ASAN build).

Behaviour.

  • The 19 cases fail with the parser of main and pass here, on debug + ASAN builds and on release builds. bundler_dynamic_import_dce.test.ts: 344 pass. bundler_cjs2esm.test.ts: 123 pass.
  • All 107 files of test/bundler/, release builds of both trees: the only failures here are two bytecode cache portability tests, which fail the same way with the parser of main in this container. native-plugin.test.ts hit a 5 s setup timeout on its first run and passes 20 of 20 when it runs again.
  • 3,237 small programs from the second self-review, built with no flag and with --minify-syntax: for 135 of them the bundle differs between 65d3b8bcc0 (merged with main) and the last commit. In each, the new bundle is smaller (a property read or an escape mark of a removed statement), or it equals the bundle of main again (a var module / var exports file). No program prints something that main gets right and the last commit gets wrong. In the var module family the last commit prints what main prints, also in the few programs where 65d3b8bcc0 was right and main is not.

What the last commit changes, and why.

  • A removed statement that reads a property of a name from a pattern, (function () { const { a } = require("./lib.js"); return a.x; });, kept the export a at 65d3b8bcc0. record_import_property_use moves such a read out of the part's record and leaves use_count_estimate. main has the same leftover, but it zeroed every symbol the removed statement declared, which hid it for a local declared inside the statement. The roll-back now takes the property reads back. The export also goes when the local is declared outside the removed statement, where main keeps it.
  • With --minify-syntax, (function () { const ns = require("./lib.js"); return ns; }); as a removed statement kept every export: single-use substitution marks ns as escaped, and nothing took the mark back. The mark now belongs to its part.
  • var module = require("./impl.js") in a package of the unwrap list makes the symbol of the wrapper's module an import namespace and removes the declaration. At 65d3b8bcc0 a use in a function above the declaration then read the wrapper's module, where main reads the namespace. For these two symbols the roll-back zeroes the count again, as main does. On 63 programs of this family the bundles are byte-identical to main.
  • My first fix for that family kept the declaration as a store into the wrapper's variable. It made every whole-variable read right, and it broke var exports = require("react"); typeof exports.createElement, which main gets right. It is not in this PR.

Left out on purpose.

  • The var module = require() / var exports = require() family is as on main: wrong for a read that runs before the declaration and for module.exports after it. bundler: keep the CommonJS wrapper when a file gives module or exports its own value #41635 and bundler: do not fold an unwrapped require() declaration when its variable is rebound #45343 are open for it.
  • An entry that a removed statement made in the table of CommonJS named exports keeps the wrapper: exports.a = 1; if (module.exports.b) {} with --minify-syntax. Same bytes as main. It is reported separately (no PR yet).
  • A require() or import() call inside a removed statement still counts as an import of the file. When nothing accounts for its result (var ns = require()), every export stays. Same as main.
  • A use that a kept statement folds away or removes inside itself (module; in a function body, a fold on the second visit) stays counted, as on main. With a removed module.exports read beside it, main lifted the file because the two miscounts cancelled. This change keeps the wrapper, as main does without the removed read. The output is right in both.
  • --minify names can differ from main when an earlier statement uses a symbol that a removed statement declares: the count that orders the names is no longer zero. Same size.
  • A note on the internal ref of an import() call made in a removed statement stays. Its import record is in no live part.
  • With --minify-syntax, exports.a = 1; exports.get = function () { const m = module; return m.exports.a + 1; }; imported as import { get } throws ReferenceError: $a is not defined, as on main. On the second visit the use of the __require helper is still kept, as on main: function f() { const r = require; return typeof r; } emits an unused shim. Both were reported separately.

Cost, release builds. valgrind, perf and bpftrace could not be installed where this was built. A count is the number of instructions executed in one call of append_part, entry to return, callees included, from gdb single-stepping the stripped binary. Each input repeats one statement 12 times in a bundled file. The table has the value that repeats.

per append_part call main this change
kept export const v = 1;, JS 1394 1397
kept export const v = 1;, TypeScript 1394 1379
kept console.log(ns.b);, one tracked read 5403 5417
kept exports.b = module.exports.a + 1;, one rewrite 6306 6318
removed v;, JS 1865 1258
removed v;, TypeScript 1936 1330
removed v;, file with a tracked namespace local 1882 1308
removed !ns; 2174 1653
removed (function () { return a.x; });, a property read 3978 3456
removed if (module.exports.a) {}, --minify-syntax 3446 2782
  • The first statements of a file take more (tables grow). From the third on, every sample of a row gives the same difference. Kept var v = 1; gives 1335 against 1338 (JS) and 1321 (TypeScript).
  • In a file with tracked namespace locals, the roll-back does one hash-map probe per symbol the removed statement used: 1308 against 1254 for v; in a file without one.
  • .text of the stripped binary: 81,665,765 to 81,670,373 bytes (+4,608). P (the parser struct): 4,032 to 4,040 bytes. AccountedUses: 12 bytes, so each tracked namespace local uses 8 more bytes in its map.
  • Measured at cf414ea1e0 against its merge base, and not again: calls to the mimalloc entry points per append_part call are 3 malloc and 1 free for a kept statement in both trees, and 5 and 2 against 2 and 2 for a removed v;. The later commits add no call that allocates.

Review.

  • Before this PR was opened: 3 concerns reached me, 3 addressed. That review stopped early. One of the three: a guard in fold.rs made a CommonJS file that lifts correctly keep its wrapper. Removed, and alias.cjs in the tests now pins it.
  • On this PR: the first commit changed the revisit path of the require fold, and a file with no other CommonJS sign got a wrapper. Fixed in 95583045fd. Five added comments were longer than one line. Shortened in 65d3b8bcc0.
  • Second self-review, on 65d3b8bcc0 merged with main, with a build of main beside it: 22 findings, 19 held. Two were regressions of that head, each found three times, and are fixed in the last commit (the property read, the var module read). One was the same kind of leftover in a map this PR already changes (the escape mark) and is fixed too.
  • Seven of the 19 named lines that no test pinned. New rows answer six: pair, keptThenRemoved, alias and aliasRemoved in bundler_cjs2esm.test.ts, and mixed and pair in bundler_dynamic_import_dce.test.ts. The seventh has no test: the generation bump at the end of revert_dead_part, which matters only when the removed statement is the last one of a file.
  • The rest are in "Left out on purpose" or belong to other changes: bundler: allocate wrapper_ref for CommonJS modules with no non-hoistable statements #33881 (a CommonJS file with no statement left), js_parser: fix four crashes of exports.eliminate and exports.replace #44137 (exports.eliminate moves the wrong statement), and a lifted file that reads the whole module.exports value (reported separately, no PR yet).
  • A read-only review of the last commit found the fault of my first var module fix (above) and three more lines without a test. The rows call, live, viaSwitch and the second read in outside answer them.
  • call.cjs and viaSwitch pass on main too. They pin the exports exception of the property-read loop and the early return of note() by reading, not by a build with the line removed.

Open PRs in the same code. #38352 calls clear_symbol_usages_from_dead_part on a part that is already closed. revert_dead_part only reverts the open part, so #38352 needs its own helper. #32417 also edits the dead-part branch. #45507 removes the second visit: the rows alias, aliasRemoved and the test SubstitutedRequireReadFoldsOneUse describe that visit. #41635 and #45343 change the var module = require() family: the test VarModuleIsTheUnwrappedRequireNamespace pins three reads that main gets right today. #45296, #41158, #41159, #41156, #45016 and #42402 touch the same files.


[human-review] gate passed · iteration 0 · 6 files touched

fails on main (without fix)
ASAN without fix: 19 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" "test/bundler/bundler_cjs2esm.test.ts" test/bundler/bundler_dynamic_import_dce.test.ts
bun test v1.4.4 (367d939d9)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [998.84ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [426.94ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [449.94ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [385.99ms]
(pass) bundler > cjs2esm/ExportsFunction [417.35ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [457.55ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [426.96ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [715.17ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [591.07ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [521.48ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [549.48ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [882.82ms]
(pass) bundler > cjs
... (truncated)

release without fix: 19 FAILED
bun test v1.4.4-canary.1 (40864409b)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [324.20ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [44.24ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [28.75ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [46.25ms]
(pass) bundler > cjs2esm/ExportsFunction [30.85ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [25.17ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [24.87ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [33.84ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [33.91ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [31.36ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [29.72ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [70.15ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvDevelopment [27.43ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRuntimeCondition [24.74ms]
(pass) bundler > cjs2esm/UnwrappedModuleRequireAssigned [46.84ms]
(pass) bundler > cjs2esm/Unwra
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" "test/bundler/bundler_cjs2esm.test.ts" test/bundler/bundler_dynamic_import_dce.test.ts
rustup spent 7s installing the pinned toolchain (nightly-2026-09-15); it was missing or incomplete on this machine
bun test v1.4.4 (367d939d9)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [1326.22ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [516.04ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [535.80ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [557.56ms]
(pass) bundler > cjs2esm/ExportsFunction [503.32ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [624.25ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [599.55ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [681.89ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [844.15ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [1010.67ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwo
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
rustup spent 9s installing the pinned toolchain (nightly-2026-09-15); it was missing or incomplete on this machine
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     41a593a21a
  features     lto, baseline

23 deps, 213 codegen, 1211 objects in 22328ms

ninja: Entering directory `/workspace/bun/build/release'
[1/4] fetch lolhtml
[lolhtml] up to date
[2/4] fetch rust-argon2
[rust-argon2] up to date
[2/4] cargo plan → /workspace/bun/build/release/rust-target/plan.json
268 units: 194 lib, 16 proc-macro (host), 20 custom-build (host), 16 run custom-build, 17 lib (host), 4 run custom-build (host), 1 rlib
[1/1562] rustc unicode_ident 
[2/1562] rustc unicode_xid 
[3/1562] gen ZigGeneratedClasses.{cpp,h,rs}
Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts
  - ResolveMessage (15 fields)
  - BuildMessage (10 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts
  - Archive (4 fields, 1 class fields)
Found 1 classes from /workspace/bun/src/runtime/api/BunObject.classes.ts
  - Subprocess (20 fields)

... (truncated)
diff hotspot
src/js_parser/fold.rs                           |   2 +-
 src/js_parser/p.rs                              | 153 +++++++++++-----
 src/js_parser/parse/parse_entry.rs              |   9 +-
 src/js_parser/visit/mod.rs                      |  10 +-
 test/bundler/bundler_cjs2esm.test.ts            | 209 ++++++++++++++++++++++
 test/bundler/bundler_dynamic_import_dce.test.ts | 224 ++++++++++++++++++++++++
 6 files changed, 558 insertions(+), 49 deletions(-)

gate history · 3 passed · 0 rejected · iteration 0

evidence per changed file
file                                             reads  edits  tests
src/js_parser/fold.rs                                3      3     56
src/js_parser/p.rs                                  13     11     58
src/js_parser/parse/parse_entry.rs                   5      3     56
src/js_parser/visit/mod.rs                           6      5     56
test/bundler/bundler_cjs2esm.test.ts                 4      5     42
test/bundler/bundler_dynamic_import_dce.test.ts      4      6     37

…s use counts

When the visit leaves a top-level statement empty, the parser rolls the
statement's uses back from `use_count_estimate`. Two counts that are
compared with `use_count_estimate` were not rolled back:

- the tracked uses of an import() / require() namespace local. One
  removed `!ns;` or `if (ns && process.env.NODE_ENV !== "production") ...`
  hid one use of the whole namespace, and the importee lost every export.
- the `module.exports` rewrites. One removed `if (module.exports.a) {}`
  hid one other use of `module`, and the file lost its CommonJS wrapper.

Each count now keeps the share that the open part noted. `revert_dead_part`
replaces `clear_symbol_usages_from_dead_part` and takes that share back
together with the uses.

The roll-back no longer sets the use count of a symbol the part declared
to zero. An unwrapped `var x = require()` leaves its statement empty, and
a use of `x` in a function above it was lost.

`ignore_usage_of_runtime_require` calls `ignore_usage` for `require`. A
folded `typeof require` or `require.main === module` subtracted a use
outside the part's record, so a removed statement subtracted it twice, and
unreachable code subtracted a use that was never counted.
@robobun

robobun commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:15 PM PT - Oct 20th, 2026

✅ @robobun, your commit 41a593a21af57d324f586c5f9a4b87ee6d7235f9 passed in Build #129545! 🎉


🧪   To try this PR locally:

bunx bun-pr 45345

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

bun-45345 --bun

@robobun

robobun commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. CI is green on 41a593a21a (build 129545, 181 jobs passed). The two regressions that the second review of this diff found are fixed in 40864409bd. The branch has main at ea6a2c7b7c.

  • A removed statement that reads a property of a destructured require() local, for example (function () { const { a } = ns; return a.x; });, keeps no export. The roll-back now takes the property reads back.
  • In a CommonJS file, var module = require("./impl.js") with a read of module above the declaration gives the package again. For module and exports the roll-back zeroes the use count, as main does.

The description has the new test count (19 cases fail on main), the cost numbers for the final code, and what is left out on purpose.

How this was reproduced (a 1.4.3 canary build and a release build of the merge base):

// lib.mjs
export const a = 1;
export const b = 2;
// entry.mjs
const ns = await import("./lib.mjs");
!ns;
console.log(JSON.stringify(Object.keys(ns)));

bun build entry.mjs --outdir out && bun out/entry.js prints []. With this change it prints ["a","b"].

// lib.js (CommonJS), imported with `import { a } from "./lib.js"`
exports.a = 1;
if (module.exports.a) console.debug("x");
function k(m) { globalThis.out = typeof m; }
k(module);

bun build --minify-syntax --drop=console entry.mjs --outdir out && bun out/entry.js throws ReferenceError: module_lib is not defined. With this change the file keeps its __commonJS wrapper and the program prints [1,"object"].

@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 973fe606-9e63-495b-b3e1-9a8e8f609ff7
📥 Commits

Reviewing files that changed from the base of the PR and between 65d3b8b and 41a593a.

📒 Files selected for processing: 5
  • src/js_parser/p.rs
  • src/js_parser/parse/parse_entry.rs
  • src/js_parser/visit/mod.rs
  • test/bundler/bundler_cjs2esm.test.ts
  • test/bundler/bundler_dynamic_import_dce.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

The parser now tracks usage totals and notes by part generation. It reverts tracked usage when a visited part has no statements, and uses the resulting totals in CommonJS and dynamic-import decisions. New bundler tests cover both transformation paths across CLI and API backends.

Changes

Parser usage accounting

Layer / File(s) Summary
Generation-scoped accounting and CommonJS rewrites
src/js_parser/p.rs, src/js_parser/fold.rs, src/js_parser/parse/parse_entry.rs, test/bundler/bundler_cjs2esm.test.ts
AccountedUses tracks total notes and notes for the current part generation. Dead-part handling reverts matching usage and advances the generation. CommonJS rewrite tracking uses the accounted total, with regression tests for preserved module and require uses.
Dynamic-import namespace tracking
src/js_parser/p.rs, src/js_parser/parse/parse_entry.rs, src/js_parser/visit/mod.rs, test/bundler/bundler_dynamic_import_dce.test.ts
Namespace tracking and escape detection use the accounted total. Tests cover namespace references that remain after truthiness checks or other statements are removed, including builds with minification and compile-time defines.

Possibly related PRs

  • oven-sh/bun#41186: It introduced namespace-use tracking for dynamic-import and require namespace-object elision, which this change updates and tests.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 41a59

No confirmed issue prevents merging after normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly summarizes the primary change: rolling back tracked uses and use counts for removed statements.
Description check Passed The description thoroughly explains the problem, fix, scope, trade-offs, and verification results. It does not use the template headings, but it includes the required information and is mostly complet…
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.
Full details: Description check

Explanation

The description thoroughly explains the problem, fix, scope, trade-offs, and verification results. It does not use the template headings, but it includes the required information and is mostly complete.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/js_parser/p.rs Resolved
`const r = require; return typeof r;` folds to "function" on the revisit
that follows single-use substitution. `ignore_usage` does nothing on a
revisit, so after the previous commit the use of `require` that the first
visit recorded stayed, and a file with no other CommonJS sign got a
`__commonJS` wrapper that it does not get on main.

`ignore_usage_of_runtime_require` now takes the `require` use back from
the count and from the part's record whenever control flow is live. A
file like that is plain ESM again. The same fold inside a removed
statement still takes exactly one use away.
Comment thread src/js_parser/p.rs Resolved
Comment thread src/js_parser/p.rs Resolved
Comment thread src/js_parser/p.rs Resolved
Comment thread src/js_parser/p.rs Resolved
Comment thread src/js_parser/p.rs Resolved

@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 reviewed the updated PR (including the new commit on the typeof require revisit path) and did not find any bugs; because this rewrites use-count bookkeeping that both the minifier's single-use substitution and the cjs2esm / dynamic-import DCE decisions key off, a human look would still be worthwhile.

What was reviewed:

  • revert_dead_part against the old clear_symbol_usages_from_dead_part: the part_use_slot liveness filter matches take_symbol_uses, forgotten-then-re-recorded refs make revert idempotent, and the dropped zeroing of declared symbols matches esbuild's appendPart (which never zeroed).
  • AccountedUses.note/revert generation handling across the three part_generation bump sites, the u32::MAX sentinel path in visit/mod.rs, and all three .total readers.
  • ignore_usage_of_runtime_require now takes the require_ref use back on the substitution revisit, which addresses the earlier inline note; the is_control_flow_dead guard pairs correctly with record_usage_impl skipping dead regions. __require still keeps its use on the revisit, same as the base branch.
  • New tests exercise cli/api backends, minify-syntax, --drop/--define, and production, with exact stdout and kept-export assertions.
Extended reasoning...

The change touches the Rust JS parser's part-level use accounting (src/js_parser/p.rs, fold.rs, visit/mod.rs, parse/parse_entry.rs) and adds 15 bundler test cases; it touches no security-sensitive surface. The second commit addresses the one inline finding from the prior review, and no CODEOWNER covers these files. Deferral rather than approval because the accounting feeds minifier substitution and wrapper/export-narrowing decisions across every bundled file, and the removal of declared-symbol zeroing changes behavior beyond the two reported repros even though it aligns with esbuild.

@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.

Code review completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

@robobun
robobun marked this pull request as draft October 20, 2026 06:59
…ved statement

A removed statement that reads a property of a destructured import()
or require() local left one use on that local. The roll-back zeroed
the locals the statement declared, which hid it. The roll-back now
takes the property reads back, and a read of a local declared
outside the statement keeps no export either.

The mark that single-use substitution leaves on a namespace local
goes with its statement.

`var module = require()` and `var exports = require()` of an
unwrapped package make the wrapper's variable an import namespace.
For these two symbols the roll-back still zeroes the use count, as
before this branch: a use above the declaration is a use of the
namespace.
@robobun
robobun marked this pull request as ready for review October 21, 2026 21:38

@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 reviewed the current revision and didn't find any bugs; the revisit fold now takes back the require_ref use through ignore_recorded_usage, which addresses my earlier inline note. Because this rewrites the parser's dead-part symbol accounting (use counts, namespace escape tracking, module.exports rewrite counts) and narrows the old zero-all-declared-symbols behavior, a human look from someone who owns the parser is still worthwhile.

What was reviewed:

  • revert_dead_part against take_symbol_uses/part_use: the generation|index slot check only subtracts live entries, and every ref in part_uses has a slot, so no out-of-bounds index.
  • Checked whether a dead part could carry import_symbol_property_uses without reaching the revert branch — record_import_property_use requires an existing part use, so part_uses is non-empty; skipping exports_ref is right since those entries are call records never moved out of part_uses.
  • AccountedUses escape/revert interplay: mark_escaped only fires at total 0 and revert restores 0 for the same generation, no-op otherwise; ignore_usage_of_runtime_require now leaves require_ref alone in dead control flow where it was never recorded.
  • Tests assert exact stdout and kept-export sets across cli/api backends and both minifySyntax states.
Extended reasoning...

The diff touches four parser files (src/js_parser/p.rs, fold.rs, visit/mod.rs, parse/parse_entry.rs) plus two bundler test files, replacing plain counters with a per-part AccountedUses struct and replacing clear_symbol_usages_from_dead_part with revert_dead_part that walks part_uses, tracked namespace notes, import property reads and module.exports rewrites. It touches no security-sensitive surface. The change is behaviorally subtle (it stops zeroing use counts of all symbols a removed statement declared, keeping only module_ref/exports_ref, and gates the require_ref decrement on live control flow), and correctness depends on invariants spread across the minifier's single-use substitution and the CJS-to-ESM classifier, which I could not execute here. No CODEOWNERS entry covers these files and no third-party objection is outstanding, but the complexity and scope rule out an approval.

This branch has not been deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants