Skip to content

transpiler: preserve TDZ for top-level class declarations referenced earlier in the file - #34933

Open
robobun wants to merge 6 commits into
mainfrom
farm/a34e0399/class-decl-tdz-preserve
Open

transpiler: preserve TDZ for top-level class declarations referenced earlier in the file#34933
robobun wants to merge 6 commits into
mainfrom
farm/a34e0399/class-decl-tdz-preserve

Conversation

@robobun

@robobun robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes #25569

Problem

At module top level, bun run evaluates a class declaration as if it were hoisted, so use-before-declaration succeeds instead of throwing:

// repro.mjs
console.log(typeof K);          // bun: "function"   node: ReferenceError: Cannot access 'K' before initialization
console.log(new K().constructor.name); // bun: "K"
class K { m() {} }
$ bun repro.mjs
function
K
$ node repro.mjs
ReferenceError: Cannot access 'K' before initialization

Block/function scope, let/const, and class expressions are unaffected; only top-level class declarations lose their TDZ in the runtime transpiler. bun build output of the same file is correct when evaluated in Node, so code that works under bun run can start throwing once bundled.

Cause

src/js_parser/parse/parse_entry.rs moves top-level class and export default statements into the before part list whenever !bundle && Class::can_be_moved(), as a workaround for some cyclic-import evaluation-order issues (kysely-org/kysely#412, #1961). The move is not conditioned on whether the class binding is already referenced by an earlier statement, so the declaration is reordered above the first use and the TDZ vanishes. The original change in eec1a07 gated this on class.is_export; that guard was dropped in c3dc64d and the reorder has applied to every movable top-level class since.

Visible with --target=bun (which takes the same tree-shaking path as the runtime loader):

$ bun build --no-bundle --target=bun repro.mjs
class K {           # moved above the first use
  m() {}
}
console.log(typeof K);
console.log(new K().constructor.name);

Fix

At the point where the move is decided, every earlier top-level statement has already been visited, so the class symbol's use_count_estimate counts the textual mentions in those statements (including inside function bodies). Skip the move when that count is non-zero for the class name (and for the named class inside an export default). Classes that are not mentioned before their declaration are still hoisted, so the cyclic-import workaround is preserved for the cases it was added for. The check is intentionally conservative: a reference inside a preceding function body also suppresses the hoist, because the counter cannot tell whether that function is called before the class initializes.

Verification

New tests in test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts:

  • class K {}, export class K {}, export default class K {} and a CommonJS top-level class each referenced before their declaration now throw ReferenceError (previously printed typeof=function | constructed=K).
  • Under --target=bun, a class with no prior mention is still emitted ahead of the preceding statement, while a class mentioned inside a preceding function body stays in source order.

Existing transpiler.test.js, runtime-transpiler.test.ts, decorators.test.ts and export-default.test.js pass unchanged.

The tests live in a sibling file rather than runtime-transpiler.test.ts because the // @bun tests in that file currently fail on a second release-build run when the entry module is served from the runtime transpiler cache (#33371); touching that file makes the release gate unreliable until that lands.


[review] gate passed · iteration 1 · 2 files touched

fails on main (without fix)
ASAN without fix: 5 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts
bun test v1.4.0 (fc9f02f8e)

test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts:
63 |       `,
64 |     });
65 |     const tokens = ["class A", "unrelated = 1", "function make", "class B"];
66 |     const indexed = tokens.map(s => [s, stdout.indexOf(s)] as const);
67 |     const order = [...indexed].sort((a, b) => a[1] - b[1]).map(([s]) => s);
68 |     expect({ missing: indexed.filter(([, i]) => i < 0).map(([s]) => s), order, exitCode }).toEqual({
                                                                                                ^
error: expect(received).toEqual(expected)

  {
    "exitCode": 0,
    "missing": [],
    "order": [
      "class A",
+     "class B",
      "unrelated = 1",
      "function make",
-     "class B",
    ],
  }

- Expected  - 1
+ Received  + 1

      at <anonymous> (/workspace/bun/test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts:68:92)
(fail) top-level class declaration TDZ > still hoisted when no already-visite
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (a509074ba)

test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts:
(pass) top-level class declaration TDZ > still hoisted when no already-visited statement mentions the name [3.05ms]
(pass) top-level class declaration TDZ > preserved for a class K { m() {} } referenced before its declaration (class declaration) [12.96ms]
(pass) top-level class declaration TDZ > preserved for a export default class K { m() {} } referenced before its declaration (export default named class) [11.02ms]
(pass) top-level class declaration TDZ > preserved for a export class K { m() {} } referenced before its declaration (exported class declaration) [11.58ms]
(pass) top-level class declaration TDZ > preserved for a CommonJS top-level class declaration [12.24ms]

 5 pass
 0 fail
 5 expect() calls
Ran 5 tests across 1 file. [172.00ms]
__F:0:S:0
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/mechgate.xml" test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts
bun test v1.4.0 (fc9f02f8e)

test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts:
(pass) top-level class declaration TDZ > still hoisted when no already-visited statement mentions the name [184.34ms]
(pass) top-level class declaration TDZ > preserved for a class K { m() {} } referenced before its declaration (class declaration) [480.34ms]
(pass) top-level class declaration TDZ > preserved for a export class K { m() {} } referenced before its declaration (exported class declaration) [432.10ms]
(pass) top-level class declaration TDZ > preserved for a CommonJS top-level class declaration [430.90ms]
(pass) top-level class declaration TDZ > preserved for a export default class K { m() {} } referenced before its declaration (export default named class) [443.01ms]

 5 pass
 0 fail
 5 expect() calls
Ran 5 tests across 1 file. [2.57s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 715ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m    Blocking�[0m waiting for file lock on build directory
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_output v0.0.0 (/workspace/bun/src/output)
�[1m�[92m   Compiling�[0m bun_clap v0.0.
... (truncated)
diff hotspot
src/js_parser/parse/parse_entry.rs                 | 43 ++++++++++---
 .../runtime-transpiler-class-hoist.test.ts         | 74 ++++++++++++++++++++++
 2 files changed, 109 insertions(+), 8 deletions(-)

gate history · 2 passed · 1 rejected · iteration 1

evidence per changed file
file                                                      reads  edits  tests
src/js_parser/parse/parse_entry.rs                            5      5      0
…ndler/transpiler/runtime-transpiler-class-hoist.test.ts      2      5      0

…earlier in the file

The runtime transpiler (target=bun, tree_shaking, non-bundle) moves
top-level class declarations to the start of the module to smooth over
some cyclic-import cases. That reorder was unconditional on any class
Class::can_be_moved() accepts, so a file like

    console.log(typeof K);
    class K {}

was rewritten with class K first, and typeof K evaluated to 'function'
instead of hitting the temporal dead zone ReferenceError that Node and
bun's own bundled output produce.

At this point in part assembly, earlier top-level statements have
already been visited, so the class symbol's use_count_estimate reflects
exactly those earlier references. Skip the move when that count is
non-zero for a class (and for the named class inside an export default)
so the declaration stays where it was written and the TDZ is preserved.
Classes that are not referenced before their declaration are still
hoisted, so the cyclic-import workaround keeps working.
@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:25 AM PT - Jul 21st, 2026

@robobun, your commit fc9f02f8e8e946788afe265c2a07b497f5ef45b2 passed in Build #76895! 🎉


🧪   To try this PR locally:

bunx bun-pr 34933

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

bun-34933 --bun

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Top-level class and named default-export class hoisting now checks for earlier name references, while runtime transpiler tests cover TDZ behavior and retained hoisting.

Class hoisting TDZ preservation

Layer / File(s) Summary
TDZ-aware hoisting decisions
src/js_parser/parse/parse_entry.rs
Class declarations and named default-export classes remain in place when their names were referenced earlier; otherwise existing movable-class hoisting conditions apply.
TDZ runtime coverage
test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts
Tests cover plain, exported, default-exported, and CommonJS classes, plus emitted ordering when earlier statements do not reference class names.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The change preserves class TDZ and makes pre-initialization access throw ReferenceError, matching issue #25569’s expected behavior.
Out of Scope Changes check ✅ Passed The added test file and parser change are both directly tied to the TDZ hoisting fix, with no unrelated changes visible.
Title check ✅ Passed The title clearly and concisely summarizes the main change: preserving TDZ for top-level class declarations referenced earlier.
Description check ✅ Passed The description covers the problem, cause, fix, and verification, which satisfies the template despite using different section headings.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/js_parser/parse/parse_entry.rs`:
- Around line 1055-1063: Compress the rationale comment near the class-statement
relocation logic in src/js_parser/parse/parse_entry.rs:1055-1063 to three lines
or fewer while preserving both the cyclic-import purpose and the TDZ safeguard.
Also shorten the regression comment in
test/bundler/transpiler/runtime-transpiler.test.ts:256-260 to three lines or
fewer while retaining the behavior it protects.
🪄 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: 0f7bd678-69b7-4a05-bbfe-e276662b747d

📥 Commits

Reviewing files that changed from the base of the PR and between e550f2c and 45574d3.

📒 Files selected for processing (2)
  • src/js_parser/parse/parse_entry.rs
  • test/bundler/transpiler/runtime-transpiler.test.ts

Comment thread src/js_parser/parse/parse_entry.rs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Error for accessing classes before initialization displays empty name #25569 - Reports that accessing a class before its declaration produces a garbled error with an empty name instead of a proper ReferenceError, which is caused by the same incorrect top-level class hoisting this PR fixes

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #25569

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@test/bundler/transpiler/runtime-transpiler.test.ts`:
- Around line 256-258: Update the regression-test comments at
test/bundler/transpiler/runtime-transpiler.test.ts lines 256-258 and 304-305 to
remove the explanatory prose and leave only the relevant issue URL, with no
other test changes.
🪄 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: e3cb6442-ed1f-4c4b-b387-468495129300

📥 Commits

Reviewing files that changed from the base of the PR and between 45574d3 and 791401a.

📒 Files selected for processing (2)
  • src/js_parser/parse/parse_entry.rs
  • test/bundler/transpiler/runtime-transpiler.test.ts

Comment thread test/bundler/transpiler/runtime-transpiler.test.ts Outdated

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

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/js_parser/parse/parse_entry.rs:1064-1074use_count_estimate is a textual, source-order count — not an evaluation-time check — so this gate both over- and under-applies relative to the comment's claim. Over: export function make() { return new K(); } before class K {} disables the hoist even though the reference is inside a function body and no TDZ is at stake (narrowing the #1961/kysely#412 workaround more than the description says; the "still hoisted" test's const unrelated = 1 never mentions A, so it doesn't cover this). Under: foo(); class K {}; function foo() { return new K(); } still hoists K above foo() because use_count_estimate(K)==0 when the SClass arm runs — pre-existing, but the same bug class this PR targets. Neither should block merge (over-applying fails safe toward spec, under-applying needs look-ahead), but it'd be worth qualifying the comment/description and adding tests that pin these two shapes so the behavior is deliberate.

    Extended reasoning...

    What the gate actually checks

    The new guard reads p.symbols[class_name.ref_].use_count_estimate at the moment the top-level loop reaches the SClass statement. By that point every textually earlier top-level statement has been sent through append_partvisit_stmts_and_prepend_temp_refs, which walks the full subtree — including function/arrow bodies — and calls record_usage (p.rs:1745-1754) for every identifier encountered. So use_count_estimate is a count of textual mentions in earlier statements, not of references that execute at module-evaluation time before the class initializes. The code comment ("an earlier top-level statement already references the class name") and the PR description ("the cyclic-import workaround is preserved") both describe the second thing while the code implements the first.

    Over-application: function-before-class disables the hoist with no TDZ at stake

    Concrete trace for:

    export function make() { return new K(); }
    export class K {}
    1. Loop iteration 0: SFunction(make) falls into the default arm (line 1142) → append_partvisit_stmts_and_prepend_temp_refs. visit_func walks the body, visits new K(), and record_usage bumps K.use_count_estimate to 1.
    2. Loop iteration 1: SClass(K) arm reads use_count_estimate == 1used_before_decl = trueshould_move = false. The class stays in place.

    But make's body doesn't run at module-evaluation time — there is no TDZ to preserve here. Prior to this PR the class was hoisted; after it, it isn't. This is exactly the shape the kysely/luxon workaround targets (helper functions/factories declared above the class they construct), so "the cyclic-import workaround is preserved" only holds when nothing textually before the class mentions its name. The "still hoisted" test happens to satisfy that (const unrelated = 1; never touches A), so it can't catch this narrowing.

    The enum-preprocessing pass at lines 979-1000 has the same effect: a TS enum that appears after the class but references it is visited before the main loop, so its reference is already counted when the class is reached.

    This is not a correctness bug — being more conservative never re-erases a TDZ — but it is an untested behavioral narrowing of a workaround the PR description asserts is preserved.

    Under-application: later-declared hoisted function still lets the class jump the TDZ

    The mirror case — the gate misses references it should count:

    foo();                              // calls hoisted foo → new K() → K in TDZ per spec
    class K {}
    function foo() { return new K(); }

    Loop trace:

    1. SExpr(foo())append_part visits it → bumps use_count(foo); K untouched.
    2. SClass(K)use_count_estimate(K) == 0should_move = true → moved to before.
    3. SFunction(foo) (default arm, line 1142) → append_part visits body → bumps use_count(K), but the move already happened.

    Output order: class K {}; foo(); function foo() { return new K(); } — succeeds. Node throws ReferenceError: Cannot access 'K' before initialization. This is the same bug class the PR targets (top-level class TDZ erased by the runtime-transpiler hoist), and REVIEW.md asks that same-class variants be covered in the same PR. It is pre-existing, not a regression, and fixing it correctly needs look-ahead into later hoisted-function bodies — a materially different mechanism than the source-order use_count_estimate heuristic.

    Why existing code doesn't prevent it

    use_count_estimate is by design a coarse textual counter incremented during the visit pass; it has no notion of "executes at module-evaluation time" vs "inside a nested function body", and it is populated in source order as the top-level loop iterates. Both gaps are intrinsic to using it as the signal.

    Impact and suggested action

    Over-application fails safe toward spec (a class that could have been hoisted simply isn't), so the worst case is that a cyclic-import file that previously benefited from the workaround stops benefiting — which per the tests still passing hasn't been demonstrated for a real package. Under-application is pre-existing behavior this PR strictly improves on; code hitting it always throws in Node, so it's rare in practice.

    Given both fail non-blocking, the concrete asks are: (a) soften the code comment / PR description from "references" to something like "any earlier statement mentions the class name (including inside function bodies)" so the guarantee isn't overstated; (b) add a test with function f() { return new A(); } before the class showing it is not hoisted, so the narrowed scope is pinned as deliberate; and optionally (c) add a .todo test for the foo(); class K {}; function foo() { new K(); } shape so the remaining gap is tracked. If preserving the hoist for the function-before-class shape matters, distinguishing body references from top-level references would need a different signal than use_count_estimate — e.g. a separate counter bumped only outside fn_stmts.

Comment thread test/bundler/transpiler/runtime-transpiler.test.ts Outdated
Comment thread src/js_parser/parse/parse_entry.rs
The '// @Bun' tests in runtime-transpiler.test.ts fail on a second
release-build run when the entry module is served from the runtime
transpiler cache (the has_loaded flag is not set on that path; #33371
tracks it). Keep that file untouched and put the class-hoist TDZ tests
in a sibling file so they can be verified independently.

Also document that the use_count_estimate guard counts textual mentions
in already-visited statements (including function bodies), and extend
the 'still hoisted' test to pin that a class mentioned by a preceding
function body is left in source order while an unmentioned class is
still moved ahead.
Comment thread test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts
Comment thread test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts`:
- Around line 13-15: Strengthen the assertions in the runtime transpiler
class-hoist test’s catch blocks to validate the complete TDZ error message,
including “Cannot access 'K' before initialization,” rather than only
e.constructor.name. Apply the same assertion update to the additional catch
block around lines 39–42, preserving the existing error-capture behavior.
🪄 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: 0d937ebe-a0c0-40e1-a0be-efa6d649189b

📥 Commits

Reviewing files that changed from the base of the PR and between 791401a and f349104.

📒 Files selected for processing (2)
  • src/js_parser/parse/parse_entry.rs
  • test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts

Comment thread test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts

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

No issues found in the latest revision — all prior nits (concurrent tests, order-token presence guard, full TDZ error message, combined-object assertion) are addressed. Deferring to a human because this narrows the cyclic-import class-hoisting heuristic in the core parser path: the new guard is conservative (declining to hoist is always spec-safe), but as noted in the resolved thread on parse_entry.rs:1067, it also suppresses the hoist when the class name appears inside a preceding function body or a later top-level enum. Worth a maintainer confirming the kysely #412 / luxon #1961 repros still evaluate cleanly.

Extended reasoning...

Overview

The PR touches two files: a ~35-line change in src/js_parser/parse/parse_entry.rs gating the existing "move top-level class / export-default to before" heuristic on use_count_estimate == 0 for the class name symbol, and a new dedicated test file test/bundler/transpiler/runtime-transpiler-class-hoist.test.ts with five concurrent subprocess tests. The parser change reads p.symbols.as_slice()[ref.inner_index() as usize].use_count_estimate before append_part() visits the class statement itself, using the exact same indexing pattern already used at lines 1148/1150/1397/etc. of the same file, so there's no new panic surface.

What was checked

  • The use_count_estimate read happens before append_part() on the class statement, so it counts only references from already-visited top-level statements (plus pre-visited enums). Declining to hoist leaves statements in source order, which is always spec-correct — the change can only move behavior toward spec, never away.
  • The SExportDefault arm only inspects StmtOrExpr::Stmt(SClass); functions and expression defaults fall through with used_before_decl = false, preserving the existing hoist for those.
  • The symbol-table indexing pattern matches the established idiom in this file; class_name.ref_ is created during parse of the same source, so inner_index() is in-bounds.
  • Tests: describe.concurrent, toMatchObject on {stdout, exitCode} (no exact-empty stderr assertion), full ReferenceError: Cannot access 'K' before initialization. message asserted, and the order-check test now asserts missing: [] so a missing token fails visibly. All prior inline comments from me and coderabbit are resolved.

Security risks

None. This is a statement-reordering heuristic in the transpiler; no untrusted input parsing, no allocation sizing, no FFI.

Level of scrutiny

Medium-high. The change itself is small and mechanically sound, but it lives in the tree-shaking path that runs for every file the runtime transpiler processes, and it narrows a heuristic that was deliberately added (and later broadened in c3dc64d) to make real-world packages (kysely, luxon) evaluate under cyclic imports. The guard is broader than "earlier top-level statement references the name": append_part() walks into function bodies, and the enum pre-pass visits later enums first, so both shapes suppress the hoist. That's arguably the right call (the counter can't tell whether the function is invoked at top level), but it's a behavioral tradeoff a maintainer should sign off on — ideally by re-running the original kysely/luxon repros.

Other factors

The mechanical gate in the PR description shows the ASAN debug lane fails 5/5 without the fix and passes 5/5 with it, and the release lane passes both (release doesn't take this path the same way). Existing transpiler.test.js, runtime-transpiler.test.ts, decorators.test.ts, and export-default.test.js are stated to pass unchanged.

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Verified the two packages the hoist was added for still load under this change (release build, fc9f02f):

$ bun -e 'import { DateTime } from "luxon"; console.log(DateTime.now().toISO().slice(0,10))'
2026-07-21
$ bun -e 'import { Kysely } from "kysely"; console.log(typeof Kysely)'
function

luxon@3.4.4, kysely@0.27.3.

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.

Error for accessing classes before initialization displays empty name

1 participant