Repository navigation
Conversation
… initializers
The enums of a statement list are visited before its other statements
so that their member values are known everywhere in the list. Enums
nested in a namespace of that list are only visited with the namespace.
An enum that follows the namespace and references one of their members
(`enum E { A = NS.Inner.X, B }`) was visited first, so the reference
stayed a property access and the members after it became `undefined`.
When the pre-visit loop passes a namespace, compute the constant member
values of the enums inside it from the unvisited initializers, with
eval_ts_constant_expression, and store them where the visit looks them
up. A member of another block of the same enum now resolves through the
block's closure argument there too, as in handle_identifier.
|
Updated 12:14 AM PT - Sep 11th, 2026
❌ @robobun, your commit ecd1db0 has 1 failures in 🧪 To try this PR locally: bunx bun-pr 42118That installs a local version of the PR into your bun-42118 --bun |
|
Reproduced on 1.4.3 (f42e980) with: // mzq.ts
namespace M { export enum Z { Q = 3 } }
enum Direct { A = M.Z.Q, B }
console.log(JSON.stringify(Direct));
This PR is stacked on #42004 and targets its branch. It becomes mergeable into main after #42004 lands. Comment here at that point and I rebase it onto main. Blocker (checked 2026-09-23): #42004 conflicts with main in one line, the CI: the diff is green. The one red test on this branch, |
…ber refs to the walk A second block of an enum may give a member of the first block a new value, so do not reuse a value found by name. Drop the member refs after the walk: the visit adds them one at a time, so that a reference to a later member stays a property access, as in an enum that is not nested.
|
Pushed d8f3e04 for the two review notes above: each walk evaluates every initializer again instead of reusing a value found by name (a second block of the same enum can redeclare it), and the member refs it adds to |
There was a problem hiding this comment.
Both concerns from my earlier pass are addressed in d8f3e04 — the "already computed" fast path is gone so merged-block redeclarations re-evaluate, and the per-member ref_to_ts_namespace_member inserts are now removed after the walk so s_enum's own visit still emits Z.B for a forward reference (pinned by the new test). No further findings; a human look is still worthwhile given this changes transpiler output semantics and is stacked on #42004.
Checked this round: the members_so_far cleanup keeps forward refs unfolded while still letting later members in the same block see earlier ones during evaluation; the current_scope swap is restored on all paths; the twin pre-visit loop in parse_entry.rs gets the same namespace arm; cache version bump to 31 is present.
Extended reasoning...
Overview
This PR extends the TypeScript enum pre-visit in visit_stmts (and its twin in parse_entry.rs) to recursively walk namespace bodies and evaluate nested enums' constant members ahead of time via eval_ts_constant_expression, so that a later top-level enum referencing NS.Inner.X folds correctly and its auto-increment successor gets a value. A helper ts_member_data_of_symbol resolves through namespace_alias for sibling-block references. RuntimeTranspilerCache version bumps to 31. Tests: ~13 printer-output cases in transpiler.test.js and one itBundled runtime case in esbuild/ts.test.ts.
Security risks
None. This is compile-time constant folding in the JS/TS parser; no I/O, network, auth, or untrusted-length arithmetic. The recursion into nested namespaces is guarded by stack_check.is_safe_to_recurse().
Level of scrutiny
Medium-high. Transpiler output correctness is user-visible and cached (hence the version bump), and REVIEW.md calls out that for ported code the reference implementation (tsc) is the spec. The change temporarily swaps current_scope, mutates shared exported_members maps and ref_to_ts_namespace_member, and interacts with merged enum/namespace blocks — subtle enough that a maintainer familiar with the enum/namespace visitor should sign off, especially since it is stacked on unmerged #42004.
Other factors
Since my earlier review, commit d8f3e04 removed the name-keyed "already computed" shortcut (fixing the merged-block redeclare miscount I flagged) and now scopes the ref_to_ts_namespace_member inserts to the pre-walk by removing them afterward, with a new test pinning that A = B inside a namespaced enum still emits Z["A"] = Z.B while M.Z.C folds to 6 for the outer enum. Commit f809a07 collapsed the doc comments to one-liners per repo style. All threads on the timeline are author-self-resolved, but the code diff independently confirms both fixes landed. No new issues found on this pass; the .unwrap() on exported_members.get_ptr_mut(name) is on a map the parser itself populated for every enum member during parse, so it is a provable invariant rather than a user-input path.
It reads only the symbol table and the member map, so it does not need to be compiled once per P instantiation (mordant generic_body_not_generic).
|
Noted, thanks. ecd1db0 moved |
Stacked on #42004: the base branch is that PR's branch, and this reuses its
eval_ts_constant_expression.Problem
namespace M { export enum Z { Q = 3 } } enum D { A = M.Z.Q, B }emitsD[D["B"] = undefined] = "B". tsc emitsA = 3, B = 4. esbuild 0.28.2 has the same gap.visit_stmts(src/js_parser/visit/mod.rs, twin inparse_entry.rs) visits the enums of a statement list first, but not enums nested in a namespace of that list.Dis visited beforeM, whileM.Z.Qis stillEnumProperty.Fix
compute_enum_values_inside_namespaceevaluates the constant members of the enums inside it witheval_ts_constant_expressionand stores them in the member maps, where the visit looks them up. Names resolve from the enum's own scope.ts_member_data_of_symbol), ashandle_identifierdoes.test/bundler/transpiler/transpiler.test.js(13 new cases) andtest/bundler/esbuild/ts.test.ts(1 new case), both fail on the base branch. Self-reviewed: restructured onto js_parser: fold references to const variables in enum initializers #42004 as asked, 4 more concerns addressed (Notes).Background
next_numeric_valueins_enum).TSNamespaceMemberMap. A member isEnumPropertyafter parsing and becomesEnumNumberorEnumStringwhens_enumfolds its initializer.M.Z.Qis inlined by walking these maps.eval_ts_constant_expression(js_parser: fold references to const variables in enum initializers #42004) evaluates an unvisited expression by tsc's rules and binds names withfind_symbolfromcurrent_scope. The walk setscurrent_scopeto the enum's scope first.Notes
constto nested enum to later enum case works and has a test; string members computed ahead of the visit are flat (the evaluator never builds ropes) and a test reads one through two template literals; the values do not depend on whether an unrelated enum follows the namespace, because every namespace the loop passes is walked, not only those before an enum.function f() { return M.Z.Q } namespace M { export enum Z { Q = 1 } }now returns1 /* Q */. Top-level enums already behave this way (ts/EnumUseBeforeDeclare). A test pins it.consts declared inside the namespace body are not known to this early walk (they are recorded when the namespace body is visited, with its scope current). The nested enum's own output still folds them. Only an enum outside the namespace that reads such a member keeps the property access.namespace M { export enum Z { Q = 3 } } namespace M { export enum Y { P = Z.Q, R } }). bun does not register the namespace closure argument inref_to_ts_namespace_member(esbuild does), and theNamespacearm of the alias path inhandle_identifierdoes not record the usage of the argument as esbuild does, so enabling it is its own change.RuntimeTranspilerCacheversion goes to 31 on top of js_parser: fold references to const variables in enum initializers #42004's 30. main took 30 in js_parser: store string enum members flat so folds over them stay linear and cannot corrupt them #41973 since, so both numbers move up by one when js_parser: fold references to const variables in enum initializers #42004 rebases.transpiler.test.jsandesbuild/ts.test.ts,bundler_edgecase,bundler_decorator_metadata,decorator-metadata,ts-enum-redecl-panic,regression/issue/10887.cargo clippy -p bun_js_parseris clean.[human-review] gate passed · iteration 3 · 6 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 3 passed · 0 rejected · iteration 3
evidence per changed file