Skip to content

js_parser: resolve a module-level var that a nested block declares again to one symbol - #46284

Draft
robobun wants to merge 7 commits into
mainfrom
robobun/26db196b/follow-merged-var-uses
Draft

robobun wants to merge 7 commits into
mainfrom
robobun/26db196b/follow-merged-var-uses

Conversation

@robobun

@robobun robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Related to #2814

Problem

  • bun build drops a live declaration: var x = { v: 1 }; if (c) { var x; console.log(x.v); } loses its first statement and throws TypeError: Cannot read properties of undefined (reading 'v').
  • hoist_symbols (src/js_parser/p.rs:3586) links the block's x to the top-level x, but the block keeps its own symbol, which the linker does not look up (doStep5.rs:394).
  • No user reported it, and 0 of 1,217 package bundles change. esbuild 0.28.2 emits the same bundle.

Fix

  • At a module-level merge the block gives up its scope member, so a lookup there finds the module's symbol.
  • Each var merge sets REDECLARED_IN_NESTED_SCOPE, which macro constants and the unwrapped require() fold check.
  • A bundle of a file with function f() {} beside var f is built as before, until js_parser: print a var merged with a top-level function declaration as an assignment #44001 lands.
  • Verified: test/bundler/esbuild/dce.test.ts (17 new cases fail on main) and 7 more files. Self-reviewed: 29 concerns raised, 27 addressed.

Background

Downsides

  • 2,000 top-level loops over one var depend on each other: 27 to 97 ms, 20 to 129 MB. Same-scope var statements cost that on main.
  • var R = require("react") with a nested var R holds the namespace object: 884 to 7,789 bytes, minified. Release binary: +4,608 bytes.
  • A lazily loaded ES module can now read a var too early (the __esm lift): 58 of 11,994 generated cells.
Notes

Why this is a draft

Context

Also fixed by the same change

  • var { a } = await import("./m.js"); { var a; console.log(a); } at module level printed undefined: the read did not count for the local, and its export was dropped.
  • A macro result and an unwrapped require() of a var that a block inside a function declares again: function f(c) { var x = macro(); if (c) { var x = 7; } return x; } returned the macro result.
  • The dev server moved the value of export var x = 1 out of the module when only a block that declares x again read it (test/bake/dev/esm.test.ts).
  • A with body keeps a read of a module-level var that it declares again: var x; with (obj) { var x; x; } runs a getter of obj. This changes bun run output, so the runtime transpiler cache version is 44.
  • Decorator metadata keeps the declaration of a type that is such a var (decorator_metadata/TypeIsVarRedeclaredInNestedBlock). The parse pass resolves the type before hoisting, so this reader follows the link.

Not fixed here

Why the block's member is removed and not overwritten

  • A direct eval pins every member of the scope that it is in. An overwritten member is the top-level symbol, so the pin reached a flat ES module's top-level var, and two files could end up with one variable. dce/VarRedeclaredInNestedBlockWithDirectEval reads both variables after both files ran. It passes on main.
  • A direct eval in the block reads the name as written. A flat ES module does not reserve that name, as on main for a var that no block declares again. dce/VarRedeclaredInNestedBlockReadByDirectEval is the single-file case, which fails on main.
  • The character-frequency pass and the slot assignment list each member of each scope. With the removal, the top-level symbol is listed once.
  • The removal stays at module level: the parser relocates a nested var only there (src/js_parser/fold.rs:75), and inside a function it would only rename minified locals.

Measurements (release builds of main 2d6d445 and of this branch on that base, x64 Linux)

  • Release .text: +4,608 bytes. hoist_symbols: +288 B in each of 2 copies (nm -S).
  • hoist_symbols per hoisted var with no merge: -2.5 instructions. Per merge inside a function: +20. Per merge at module level: +205 (gdb stepi, 200-statement synthetic files). typescript.js, lodash.js and react-dom.development.js have 0 merges of a var onto a var (gdb breakpoint on the flag store).
  • SymbolFlags is 16 bits wide, and Symbol stays 48 bytes. Per call (gdb stepi): declare_symbol 158 to 160. visit_decl for var v = 2 30 to 30, for an inlined const c = 1 115 to 118. s_local for var v = 1 392 to 395, for const c = 1 443 to 449.
  • Hot functions (find_symbol_with_record_usage, record_usage_impl, handle_identifier, take_symbol_uses, declare_symbol, do_step5, needs_wrapper_ref, generate_code_for_file_in_chunk_js): 7 of 17 copies have the same instructions as main. The other 10 differ because the fields of Symbol moved: 12,419 to 12,417 instructions in the 17 copies (objdump).
  • K top-level for (var i = 0; ...) loops, time and peak RSS of bun build: K=500 28 ms / 17 MB to 21 ms / 24 MB. K=2,000 27 ms / 20 MB to 97 ms / 129 MB. K=5,000 33 ms / 26 MB to 530 ms / 793 MB. K=20,000 0.07 s / 57 MB to 8.6 s / 11.4 GB. The same-scope form (var t = f(); use(t); x 20,000) takes about 10 s and 8.2 GB on main. 50 loops make 2,500 step-5 dependency edges (main: 50).
  • var React = require("react"); if (c) { var React; } console.log(React.version); with react 18.3.1 and --minify: 884 to 7,789 bytes. var a = { v: "kept" }; if (c) { var a; a = { v: "assigned" }; }: 82 to 112 bytes, because the plain store is a use of the top-level symbol, as it is without the block's var.

Right on main stays right (the same two builds, bundles run under node v26.3.0)

  • 164 hand-written cells (macro, import(), unwrapped require() with lifted and wrapped targets, module / exports, catch, parameter, in esm, cjs, iife, with and without --minify-syntax, and unbundled). 32 iife cells print nothing on both builds and check nothing. Of the other 132: 107 match node on main, 121 here, 0 match on main and differ here.
  • 15,000 generated modules from 5 seeds (a var declared at the top level and again in if, block, for, switch, try, catch, label, do and with bodies, with reads, stores, closures, eval, and top-level functions of the same name. Flat, behind require() and behind import(). In esm, cjs, --minify-syntax and --minify-identifiers): 47,837 cells. 43,870 match node on main, 44,623 here.
    • Flat files and lazily loaded CommonJS files: 35,843 cells. 678 become right, 0 become wrong.
    • Lazily loaded ES modules: 11,994 cells. 133 become right, 58 become wrong, in 16 modules. Each of them reads a lifted var before its declaration runs (the __esm lift above).
  • 1,500 generated CommonJS modules with module and exports as the names: 4,560 cells. 13 become right, 0 become wrong.
  • 4,500 generated ES modules whose importer reads the exports and calls exported functions (the same shapes, plus bodies without braces and for-in / for-of heads, behind import(), require() and a static import): 15,749 cells. Every cell prints the same as on main.
  • Output on a corpus (test/node_modules): --no-bundle of 32,082 files: 0 differ. Bundles of 1,217 package entries in esm, cjs, iife, --minify-identifiers and --minify: 0 differ. Each of 7,792 ES module files behind await import() (5,885 get an __esm wrapper): 0 differ.

Review history

  • A first cut re-pointed every merged var, inside functions too, and guarded module / exports with the flag. It broke programs that main bundles correctly. This PR is the re-cut.
  • The first push had a guard in the linker that kept the value of a flagged var inside the __esm wrapper. It is removed: the store then depends on the wrapper, and a statement that keeps its own var there takes the store. The guard changed 0 of 5,885 wrapped corpus files.
  • The first push read has_top_level_function_merged_with_var in the hoist merge, which hoist_symbols also sets part of the way through its walk. The rule for such a file now runs after the walk.
  • Not taken from the last self-review: a rebase on js_parser: print a var merged with a top-level function declaration as an assignment #44001 (it is not merged, and its branch is a month behind main), and an upstream esbuild issue.

Suites

  • On main these new cases fail: 17 in esbuild/dce, 2 in bundler_cjs2esm, and 1 each in bundler_dynamic_import_dce, bundler_decorator_metadata, transpiler/macro-test, transpiler/runtime-transpiler, transpiler/transpiler and bake/dev/esm.
  • Debug build, with main 449877f merged in: esbuild/dce (115 pass), bundler_cjs2esm (117), bundler_dynamic_import_dce (334), bundler_decorator_metadata (3), transpiler/macro-test (35), transpiler/runtime-transpiler (16), transpiler/transpiler (260), bake/dev/esm (19).
  • Release build: all 98 files of test/bundler, test/bundler/esbuild, test/bundler/transpiler, run-cjs and bake/dev/esm: 7,607 pass. bundler_bytecode_portable (2 cases) fails on main too in this container.

no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bake/dev/esm.test.ts

…again to one symbol

A `var` in a nested block at the top level of a module is the same
variable as a top-level `var` of that name. Hoisting linked the block's
symbol to the top-level one, but the block's scope kept its own symbol,
so a read in the block was a use of a symbol that declares nothing.

Tree shaking then removed `var x = {...}` from
`var x = {...}; if (c) { var x; x.v }`, and the bundle threw. A
destructured `import()` local that only such a block read lost its export
the same way.

The merge in `hoist_symbols` now removes the block's member for the name,
so a lookup in the block goes up to the module scope's symbol. That
symbol gets a REDECLARED_IN_NESTED_SCOPE mark. Three places that take one
declaration as the only source of a variable's value check it: a macro
result is not inlined into such a `var`, an unwrapped `require()` does
not make it the import namespace, and the `__esm` wrapper keeps its
initializer inside.

Left as they are: `module` and `exports` in a CommonJS file, a file where
a top-level function and a `var` share a name, and a `var` inside a
function.
@robobun

robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:47 PM PT - Oct 25th, 2026

❌ @robobun, your commit 695abf9 has 3 failures in Build #132936 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 46284

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

bun-46284 --bun

@robobun

robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduction: save this as d.js, then run bun build d.js --outfile out.mjs && node out.mjs.

var x = { v: 1 };
if (globalThis.process) {
  var x;
  console.log(x.v);
}
  • node d.js and bun d.js print 1.
  • Bun 1.4.3 and main (9b4284f) emit a bundle without var x = { v: 1 }. It throws TypeError: Cannot read properties of undefined (reading 'v').
  • This branch prints 1.

The fix is in this pull request (#46284).

CI on 695abf9 (build 132936)

  • No failure is in code or tests that this PR changes. The PR changes the parser and eight bundler and transpiler test files.
  • One job failed: darwin any aarch64 - test-bun on the agent darwin-aarch64-26.6.2-1. It timed out at test 234 of 3,080.
  • Three network test files failed there on each of 4 attempts: test/js/bun/http/serve.test.ts (timeout), test/js/bun/net/socket.test.ts (socket closed before the payload was read) and test/js/bun/s3/s3.test.ts (timeout).
  • The same agent failed three other network test files in build 132928, which belongs to another PR. The second shard of this lane passed on the agent darwin-aarch64-26.6.1-1.
  • The other 180 jobs passed.

The PR is a draft. It waits for a decision on the order with #44001 (see "Why this is a draft" in the description).

@coderabbitai

coderabbitai Bot commented Oct 21, 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: 269bd613-0118-4132-929b-6877beb0e22a
📥 Commits

Reviewing files that changed from the base of the PR and between 71bc818 and 04a8a44.

📒 Files selected for processing: 9
  • src/ast/scope.rs
  • src/ast/symbol.rs
  • src/bundler/linker_context/generateCodeForFileInChunkJS.rs
  • src/js_parser/p.rs
  • src/js_parser/visit/mod.rs
  • test/bundler/bundler_cjs2esm.test.ts
  • test/bundler/bundler_dynamic_import_dce.test.ts
  • test/bundler/esbuild/dce.test.ts
  • test/bundler/transpiler/macro-test.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 var redeclarations in nested scopes. Bundler hoisting, CommonJS namespace handling, and macro constant tracking use that state. Regression tests cover nested redeclarations across bundling and dead-code elimination scenarios.

Changes

Nested-scope var redeclarations

Layer / File(s) Summary
Track redeclarations during hoisting
src/ast/scope.rs, src/ast/symbol.rs, src/js_parser/p.rs
Scope members can be removed by hash and name. The parser marks qualifying merged symbols as redeclared in nested scopes and records when wrapper references are needed.
Use redeclaration state in bundler transforms
src/bundler/linker_context/generateCodeForFileInChunkJS.rs, src/js_parser/visit/mod.rs
Export hoisting excludes affected identifiers. Require namespace handling and macro constant tracking also account for nested-scope redeclarations.
Test nested redeclaration behavior
test/bundler/bundler_cjs2esm.test.ts, test/bundler/bundler_dynamic_import_dce.test.ts, test/bundler/esbuild/dce.test.ts, test/bundler/transpiler/macro-test.test.ts
Tests cover CommonJS bindings, dead-code elimination, imports, code splitting, and macros. Some regression cases remain marked todo.

Suggested reviewers: jarred-sumner

Priority: Normal

Merge Risk: Minimal · up to 04a8a

This change makes nested var redeclarations at module level resolve to the same symbol, which fixes dropped initializers during bundling. No merge-blocking issue was found in the supplied material. Some listed cases remain unfixed and are marked todo in the tests.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly summarizes the primary change: resolving a module-level var redeclared in a nested block to one symbol.
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.
Description check Passed The description is complete and relevant. It explains the problem, fix, scope, known limitations, performance impact, and verification results. It does not use the exact template headings, but it prov…
Full details: Description check

Explanation

The description is complete and relevant. It explains the problem, fix, scope, known limitations, performance impact, and verification results. It does not use the exact template headings, but it provides the required information in equivalent sections.

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

Beyond the inline findings, I also checked the new scope.members.remove_hashed(hash.unwrap(), name) in hoist_symbols (src/js_parser/p.rs:3628): hash is unconditionally filled at p.rs:3549-3550 before any merge path, matching the sibling get_or_put_member_with_hash sites, so the unwrap cannot panic; and the removal mutates scope.members while the loop walks the separate member_snapshot copy, so iteration is not invalidated. The needs_wrapper_ref early return (p.rs:9525) was also examined and ruled out as a correctness issue (it only widens when a file gets a wrapper symbol).

Extended reasoning...

The change touches the parser's symbol hoisting (src/js_parser/p.rs), scope member storage (src/ast/scope.rs), a new SymbolFlags bit, three single-declaration optimizations in visit/mod.rs, and the __esm lift guard in the linker; it touches no security-sensitive surface. Five confirmed findings (an __esm for-head regression, order-dependent application of the function/var exclusion, quadratic linker cost on many top-level loops, and the require() namespace size growth) are posted inline, so a human needs to weigh them. This note records only the additional concerns examined and ruled out.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/js_parser/p.rs Resolved
Comment thread src/js_parser/p.rs Outdated
Comment on lines +3617 to +3628
if _scope_ptr == self.module_scope
&& value.ref_ == original_member_ref
&& self.symbols[symbol_idx].kind
== js_ast::symbol::Kind::Hoisted
&& existing_kind == js_ast::symbol::Kind::Hoisted
&& !self.has_top_level_function_merged_with_var
&& (self.has_es_module_syntax
|| (name != b"module" && name != b"exports"))
{
self.symbols[existing_idx].set_redeclared_in_nested_scope(true);
self.has_var_redeclared_in_nested_scope = true;
scope.members.remove_hashed(hash.unwrap(), name);

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.

🟡 (optional) Users bundling a module with thousands of top-level statements that re-declare one var (typically for (var i = 0; …) loops) now get a build that takes seconds and gigabytes instead of milliseconds. Once the nested member is removed at src/js_parser/p.rs:3628 every read of i resolves to the one module symbol, so in the linker each loop part depends on every other loop part (K² edges). Fix: keep one-symbol resolution but bound the linker cost, e.g. record a dependency on the symbol's declaring-part list once per use site in step 5 instead of one edge per (use, declaring part) pair, so the edge count stays O(K).

Why this was flagged

Input: one module-level file with K top-level for (var i = 0; i < n; i++) {...} loops (generated fixtures, benchmark or concatenated legacy scripts), bundled with bun build. hoist_symbols at src/js_parser/p.rs:3617-3628 flags the module-level i and removes each loop scope's own member, so every loop's uses of i become uses of the single module symbol. The linker's step-5 dependency pass then makes each loop part depend on every part that declares i, i.e. K parts, giving K² edges; the PR's own measurement is 50 loops: 50 edges on base, 2,500 after, and K=20,000: 0.07 s / 56 MB on base versus 11.8 s / 11.3 GB after, K=5,000: 40 ms / 26 MB versus 521 ms / 774 MB. On the base branch each loop's i kept its own linked symbol so each part depended only on itself. No safeguard bounds the edge count; the author notes the same-scope var t = f() form was already quadratic, but that form is far rarer at module level than repeated for (var i …) loops, so the change widens the population that hits it.

Verification: In src/js_parser/p.rs:3617-3628, when a nested scope's Hoisted var merges into an existing module-scope Hoisted var, the nested scope's own member is dropped. Then src/bundler/linker_context/doStep5.rs:376-410 iterates part.symbol_uses.keys() and pushes one Dependency per listed part. On head, every part uses i_1 and so pushes K Dependency entries, giving K² edges.

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.

No change here. Step 5 adds one dependency for each part that declares the symbol. K same-scope var statements pay the same on main (var t = f(); use(t); x 20,000: about 10 s and 8.2 GB). A bounded form changes what each reader of a part's dependencies sees, so it is a linker change of its own, and it helps both forms. The PR body has the numbers for this form: 500 loops cost 7 MB more, 2,000 loops cost 70 ms and 109 MB more.

Comment on lines +405 to +414
if self.is_redeclared_in_nested_scope(ref_) {
// The other declaration can assign the variable, so
// it holds the namespace, as `x = require()` does.
decl.value = Some(Expr::init_identifier(
self.imports_to_convert_from_require
[unwrapped_id.get_usize()]
.namespace
.ref_,
value.loc,
));

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.

🟡 (optional) Users bundling a CommonJS file that does var R = require("react") and later has any nested var R; now ship the whole package namespace instead of the handful of import items they got on the base branch (the author's probe grows 133 to 7,334 bytes per file). At visit/mod.rs:405-414 the flagged decl is rewritten to var R = <namespace ref>, so R.useState is no longer an import-item access and nothing in react can be tree-shaken. Fix: only fall back to the namespace object when the nested declaration can store to the variable (a var R = ... or an assignment in the block); a bare var R; re-declaration does not change the value and should keep the base import-item conversion.

Why this was flagged

Trigger: unwrap_commonjs_to_esm is on for the package (src/js_parser/p.rs:1652-1660, true for the default unwrap list, e.g. react) and the file has var R = require("react") at module level plus any var R; in a nested block, which hoist_symbols flags at src/js_parser/p.rs:3626 regardless of whether the block stores. At src/js_parser/visit/mod.rs:405 the guard is_redeclared_in_nested_scope(ref_) takes the new branch and replaces the ERequireString with an identifier of the generated namespace (lines 408-414), and skips the continue 'outer that on the base made R itself the namespace with an import_items_for_namespace entry (lines 416-422). Every R.x in the file is then a property read on a materialized namespace object, so the linker cannot drop unused exports; the author measures 133 to 7,334 bytes for one probe. No store analysis is performed before paying that cost. Population: every bundled CJS file hitting that shape, with output size for the consumer, not a correctness break.

Verification: src/js_parser/p.rs:3617-3628 sets REDECLARED_IN_NESTED_SCOPE whenever a nested-block var merges into a module-scope var, never testing whether the inner declaration is assigned. src/js_parser/visit/mod.rs:405-414 then replaces the ERequireString with the namespace ref instead of the base-branch import_items_for_namespace path (lines 416-422), so the linker retains every 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.

No change here. The fold is decided at the declaration, where the parser does not know what the block does with the variable. A store does not need an initializer: var R = require("react"); if (c) { var R; R = other; } prints the package's export from main's bundle and other from this one. A bare var R; can be told from that only after the visit pass, and #45343 asks where the fold is decided. The size cost is in Downsides: 884 to 7,789 bytes for a minified probe with react 18.3.1.

Comment thread src/js_parser/visit/mod.rs Resolved
…nction/var flag in the hoist merge

The linker again moves the value of a `var` that a nested block declares
again above the `__esm` wrapper, as it does for every other `var`. The
guard that kept that store inside the wrapper is removed. A `for` head
without initializer and a body without braces keep their own `var` inside
the wrapper, so the store did not reach the exported variable, and the
importer read `undefined`.

`hoist_symbols` sets `has_top_level_function_merged_with_var` part of the
way through its walk. A block after a `function f` / `{ var f }` pair
therefore kept its own symbol, and a block before the pair did not. The
merge now reads the value that the parse pass left.
@robobun
robobun marked this pull request as draft October 21, 2026 23:51
@robobun

robobun commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

I moved this PR to draft. The pushed commit has two regressions.

1. The guard in the linker (the finding above). It keeps the store of a flagged var inside the __esm wrapper. A for head without initializer keeps its own var there, and so does a body without braces (while (c) var x = ...), so the store does not reach the exported variable. The guard is removed in my local branch, and the lift is as on main again.

2. A function and a var of one name. My own review of that follow-up found this one:

function f() { return 1; }
if (globalThis.process) { var f; }
var x = f;
if (globalThis.process) { var x = 5; console.log(x); }

Node and a bundle from main print 5. With this change the read in the last block keeps var x = f, so function f stays next to the relocated var f;, and the ESM bundle does not load: SyntaxError: Identifier 'f' has already been declared. The pushed commit does this when the x block comes before the f block. The carve-out for such a file reads a flag that hoisting sets part of the way through its walk.

#44001 changes how that pair is printed, so the clean order is #44001 first and this PR without the carve-out after it. My next push makes this branch correct without #44001 too, and answers the review threads. The PR stays a draft until the order is decided.

… a file with a function and a `var` of one name

A `var`-onto-`var` merge sets `REDECLARED_IN_NESTED_SCOPE` at every scope
level, so a macro constant and an unwrapped `require()` of a `var` that a
block inside a function declares again are right too. The member removal
stays at module level. `SymbolFlags` is 16 bits wide and the flag is bit 9.

A bundle of a file with `function f() {}` beside `var f` loads only while
tree shaking drops the pair, because module code rejects it. The read of
a redeclared `var` can keep a statement that uses `f`. After the hoist
walk, an ESM or CommonJS bundle of such a file gets the removed block
members back. The earlier check read a flag that the walk itself sets.

A `with` body now keeps a read of a module-level `var` that it declares
again, so the runtime transpiler cache version is 39. Decorator metadata
follows the link of a type that the parse pass resolved before hoisting.
…ow-merged-var-uses

# Conflicts:
#	src/jsc/RuntimeTranspilerCache.rs
Comment thread src/ast/symbol.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
Comment thread src/js_parser/p.rs Resolved
Comment thread src/js_parser/visit/mod.rs Resolved
Comment thread src/js_parser/visit/mod.rs Resolved
Comment thread src/jsc/RuntimeTranspilerCache.rs Resolved
@robobun

robobun commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed f3ac595. The PR stays a draft: #44001 should land first (see "Why this is a draft" in the description).

Changes since the review:

  • The guard in the linker is removed. The __esm lift is as on main.
  • An ESM or CommonJS bundle of a file with function f() {} beside var f keeps its block symbols, in any block order. The rule runs after the hoist walk. This covers the second regression from my comment above.
  • Each var-onto-var merge sets the flag, inside functions too. The macro and require() cases inside a function are fixed.
  • SymbolFlags is 16 bits wide and the flag is bit 9, so it does not collide with js_parser: print a var merged with a top-level function declaration as an assignment #44001 (bit 0).
  • A with body keeps a read of a module-level var that it declares again. That changes bun run output, so the runtime transpiler cache version is 44.
  • Decorator metadata keeps the declaration of such a var.
  • New tests: the dev server, the runtime with read, decorator metadata, direct eval, blocks with 3, 6 and 12 names, and the function and var pair in three formats.
  • Each new source comment is one line.

Two cases that main has too have no tracking issue:

  • A CommonJS file with var module; if (globalThis.process) { var module; console.log(module.require("./dep.cjs").name); }: an ESM bundle throws TypeError: Cannot read properties of undefined (reading 'require'). The wrapper gets no module argument.
  • with (obj) { var x; x; } where x has no other declaration: the read is removed, so a getter of obj does not run. bun file.cjs does this too.

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.

1 participant