Repository navigation
js_parser: store string enum members flat so folds over them stay linear and cannot corrupt them - #41973
Conversation
…append to them An enum member initialized with a concatenation was stored as a rope, and every inlined use of it copied only the rope's root node. Template::fold pushes the following template text onto whatever it inlined, which walked to the end of the shared chain and appended there, so the member's value changed in its declaration and in every other use. Using the member in a second template literal then hit a chain node without an end pointer (unwrap panic), and using it twice in one literal linked the chain onto itself (infinite loop when resolving). Resolve the rope when the enum is visited, so the stored string is a single node and copies of it share nothing. This makes the inlined-enum cloning in fold_string_addition redundant, so it is removed, and wrap_inlined_enum asserts the invariant in debug builds.
Covers the fuzz finding that `S.A + "k" + S.A + "k" + ...` over an inlined string enum member used quadratic memory while folding, plus the ways a folded enum member can meet another `+` or template literal, including a member name that contains `*/`.
The transpiled output of a file that uses a folded string enum member inside a template literal changes with the previous commits (it was wrong before), and the cache is not keyed on the bun version, so a stale entry would keep serving the old output after an upgrade.
|
Warning Review limit reached
On-demand reviews are free for the next 12 days. After that, they cost $0.25 per reviewed file. Or wait 6 minutes for your next included review. View limit detailsLimit details: You’ve used all 10 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (8)
Comment |
|
Status: ready for review. CI on ffcb911 is green for this diff: 180 of 181 jobs passed, and the one red lane (debian x64-asan) is Reproduced on bun 1.4.3 with the fuzz input ( While reducing it I hit two more symptoms of the same shared rope, both on 1.4.3: |
|
Updated 7:12 AM PT - Sep 8th, 2026
❌ @robobun, your commit ffcb911 has 1 failures in 🧪 To try this PR locally: bunx bun-pr 41973That installs a local version of the PR into your bun-41973 --bun |
Problem
S.A + "k" + S.A + "k" + ...over an inlined string enum member folds in quadratic memory: 8 192 terms (49 KB) take 1.4 GB inbun run. EveryA.Breference shares the member's rope, sojoin_strings(src/ast/fold_string_addition.rs) deep-cloned both ropes when either operand was anE::InlinedEnum: each enum term re-cloned the accumulator.Template::fold(src/ast/e.rs) and members named with*/(left unwrapped bywrap_inlined_enum) had no guard and appended onto the shared rope.enum A { B = "1" + "2", T = `t${B}t` }makesA.Bprint"12t". A second use panics on 1.4.3:called Option::unwrap() on a None value,Crashed while visiting.Fix
s_enumflattens the member's string (resolve_rope_if_needed) before it stores theEnumString, the only place one is built. A shared string is then never a rope.join_stringsloseshas_inlined_enum_poisonandclone_rope_nodesand links in O(1). 4 096 pairs: 1 545 MB → 9 MB.test/js/bun/transpiler/transpiler-enum-concat-chain-oom.test.ts(new, fails on 1.4.3), the js_parser: store string enum members flat so template folding cannot append to them #38998 tests, the enum suites. Self-reviewed: 2 concerns raised, 2 addressed.Background
"a" + "b"links the rightEStringnode onto the left throughnext/end(a rope).EString::pushwrites to the rope's last node, so a rope needs one owner.A.Binto anE::InlinedEnumaround a copy of the member's root node. Later folds see a string literal.Notes
bun build --minify-syntaxbehaves the same. Not a regression: 1.3.14 behaves the same, and the Zig code this was ported from had the same shape.*/case on 1.4.3:enum A { "*/" = "s" + "t" }; console.log(A["*/"] + "u", A["*/"] + "v")panics the same way. With members stored flat the unwrapped value is a single node, so it is safe too.bun runof a 16 384-pair file peaks 8 MB over an empty script.Template::fold, the*/names and the bundler's cross-module substitution at the one place the sharing starts, and costs one O(len) copy per rope-valued member. esbuild stores enum string values flat too.resolve_rope_if_neededleavesrope_len(still the length) and a staleendbehind.endis only read from a node whosenextis set, and a push onto a copy of a flat node assigns both.EXPECTED_VERSIONgoes to 30.+chain of template literals that each have a substitution (`a${x}b` + `a${x}b` + ...) is also quadratic under syntax minification (4 096 terms: 526 MB). That isconcat_partsre-copying the accumulatedpartsarray at each step, a different path that this change does not touch.bundler_edgecase.test.ts,bundler_minify.test.ts,bundler_string.test.ts,esbuild/ts.test.ts,esbuild/default.test.ts(enum/template/string filter),transpiler/transpiler.test.js,transpiler-comma-chain-oom.test.ts,cli/run/transpiler-cache.test.ts.cargo clippyonbun_astandbun_js_parseris clean.no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/transpiler/transpiler-enum-concat-chain-oom.test.ts