Repository navigation
Conversation
…ying a bundle
`print_decls` printed `a = o.x, b = o.y` as `{ x: a, y: b } = o` for every
caller. Where the text runs in place of a file, a nullish `o` then threw
"Cannot destructure property" from the line of the second declarator, and
`toString()` showed the pattern.
The fold is a syntax minification, so it now needs `minify_syntax` and
`bundling`, as the other size-only rewrites do. The runtime transpiler,
`Bun.Transpiler`, `bun build --no-bundle`, the REPL, the dev server and a
bundle that is not minified print the declarators as they are written. A
minified bundle prints the same bytes as before.
The AOT compiler no longer models the printed pattern, and the transpiler
cache version is 52. The tests of the rules of the fold are `itBundled`
cases now: `Bun.Transpiler` does not fold, so there they would pass with
or without the rules.
|
Status: ready for review. The diff is green in CI. The red tests of the build are red on main too. How to reproduce on main (save as function load(props) {
const first = props.first,
second = props.second;
return [first, second];
}
try { load(undefined); } catch (e) { console.log(e.message, "|", e.stack.split("\n")[1].trim()); }
CI, build 137326 (finished):
Pull request: #47870 |
|
No actionable comments were generated in the recent review. WalkthroughThe JavaScript printer now emits same-target destructuring only when syntax minification and bundling are enabled. AOT lowering no longer tracks joined declarators or emits their associated nullish checks. Tests cover output, runtime errors, IR, cache versioning, and source positions. ChangesMember-read printing and lowering
Suggested reviewers: Merge Risk:
|
Related to #17153
Problem
const a = o.x, b = o.ywith a nullishothrowsCannot destructure property 'x' from null or undefined valueon the line ofo.y. Two statements and aforinitializer do the same.toString()shows a pattern.print_decls(src/js_printer/lib.rs:2196) folds such a run into{ x: a, y: b } = ofor every caller.Fix
minify_syntaxandbundling, like the other rewrites that only save bytes. Minified bundles keep their bytes (51 of 51 files).--compile --aot) drops its copy of the rule. The transpiler cache version is 52.test/js/bun/sourcemap/source-positions.test.ts(main fails 37 tests). Also the transpiler, bundler, REPL, dev-server,bun pm diffand AOT suites (Notes).Background
print_declsprints the declarators of onevar,letorconststatement. At run time the parser first merges adjacent statements.minify_syntaxfor every file and never bundles. So such rewrites test both flags (fix: statement joining and tmpdir on windows #8659, Collapse single-return arrow bodies to expression bodies when minifying #31723).bundlingalone: a defaultbun build --compilekeeps the pattern. Considered the fold forBun.Transpilerwithminify: it changes what a script computes.Downsides
Bun.Transpiler,bun build --no-bundleand bundles without--minify-syntaxprint another text (rxjs.umd.js: +0.43%).bun pm diffreports it as a change.Notes
Question for a maintainer. Should a bundle built without
--minify-syntaxkeep this fold? This PR says no: the fold is a syntax minification, and with it a defaultbun build --compilethrows anotherTypeErrorthanbun runof the same file. If such bundles must keep their bytes, the gate isbundlingalone, and the dev server then needs its own term. That variant was built first: every bundle was byte for byte the same (45 of 45 files).What each lane prints. Release builds of main (3bf1a06) and of this branch, same profile.
bun run,bun test,bun -e, REPLbun test --coverage,--inspect(they turnminify_syntaxoff)forheadBun.Transpiler,bun build --no-bundle, with and without minifyminify_syntaxis off in development)bun build,bun build --compilewithout--minify-syntaxforheadbun build --minifyor--minify-syntax, Bun's built-in modulesWithout
minify_syntaxthe parser does not merge two statements. So in those lanes main folds only the one-statement list and theforhead.one.mjs(a one-statement and a two-statement function, called withundefined): main reportsCannot destructure property 'first' from null or undefined valueat3:14and9:18. This PR reportsundefined is not an object (evaluating 'props.first')at2:17and8:17. As written (// @bunfirst line) the lines are the same, at column 22. Node reports2:23and8:23.bun build --compileofone.mjsreports3:14with the pattern message on main, and2:17with the member message here. With--minifyit is unchanged.toString()of the two-statement function still shows one statement,const first = props.first, second = props.second;. The parser merges adjacent statements at run time. That is not part of this PR.Wrong values, not only wrong messages. A pattern reads its object once. Two programs that show it, both fixed here:
The second one is why
Bun.Transpilerwithminify: { syntax: true }does not keep the fold. Those lanes never applied the other bundle-only rewrites either.Who reported it. No issue reports this in a user's own file. #17913, #23556 and #24374 show the message of this fold in
checkServerIdentityof the built-innode:tls, which release builds still fold. #27135 fixed that caller. The case for this PR is the repro and the precedent: #8659 ("Joining statements with commas will only happen when bundling, making runtime error stack traces more readable"), #22492 and #31723.Printer
Optionsliterals.bundling: truewith the user'sminify_syntax:LinkerContext.rs:2382,postProcessJSChunk.rs:199. The entry point tail atpostProcessJSChunk.rs:1256now saysbundling: truetoo. It moves no byte: each local it prints has one declarator.bundling: false:transpiler.rs:2386and:2463(runtime transpiler,Bun.Transpiler,--no-bundle),repl.rs,aot/services.rs(what the AOT compiler prints asbun rundoes),CodeCoverage.rs,pm_diff_normalize.rs.SAME_TARGET_BECOMES_DESTRUCTURINGwas a constanttruewith this one reader. It is removed.Cost, measured. User-mode instructions come from
perf_event_open, the minimum of 5 interleaved runs. Noise is two series of one build.function f(o){const a=o.x,b=o.y;return a+b}: 14 to 9 bytecode instructions, 472 to 435 machine instructions per call in the interpreter, 95 to 86 with the JIT.var: 10 to 7 bytecode instructions..jsand.map. That is 8 flag sets (--minifyor--minify-syntax, with--target=bun,--format=cjs,--splittingand three kinds of source map) over a two-entry fixture, the repro, typescript.js and rxjs.umd.js.--minify-0.014% (noise 0.07%).--minify-syntax: typescript.js is the same, rxjs.umd.js 234,111 to 235,129 bytes.size): text 140,303,961 to 140,298,841 bytes. The five copies ofprint_decls: 15,058 to 13,790 bytes.bun build --no-bundle --minify-syntax --target=bun typescript.js: 8,194,709 to 8,195,291 bytes.perf,valgrind,bloaty,strace.AOT compiler (
bun build --compile --aot). It predicts what plain Bun prints and reports, so it held a copy of the rule of the fold. That copy is gone (163 lines).test/aot/parallel: 7,168 pass, 12 skip themselves, 4 fail. The same 4 fail on main.test/aot/tools/same-output.ts, main against this branch: identical on the 289 benchmark programs.cargo test -p bun_aot --test ir_tests: 2,268 of 2,270 files. The other 2 hit a nesting limit of the unoptimized test binary, on main too. Two IR tests lose theNotDestructurabletest of a joined run.test/bundler/compile-aot-size.test.tsskips itself on a local build. In CI (Linux x64), main against this branch: the compiler takes 23,663,481 to 23,663,546 bytes, a hello world maps 68,134,014 to 68,125,822 bytes, and with--trim25,961,598 to 25,965,694 bytes (one page more). Its bound of 25,880,000 is exceeded on main too.Tests.
declarations.ts: two statements, one statement, aforinitializer and a third member whose getter throws. A probe can name another text for the minified bundle, where the fold stays.transpiler.test.jsheld 36 expectations and 2 programs about the rules of the fold, throughBun.Transpiler. They would pass there with or without the rules now. The same shapes areitBundledcases inbundler_minify.test.ts, withwithand the mappedargumentsobject in a CommonJS bundle.bundler_bytecode_portable.test.ts: the 5 bundles without--minifythat hold such declarators have new hashes and a smaller payload. The 3 minified ones are unchanged.--inspectand a Worker. On main both report the pattern message for the one-statement function, here the member message.Left for later.
const list = o.list; return list.items;is printedreturn o.list.items;and reports line 2 for a read on line 3 (Stack traces missing when creating errors #24789, An arrow function name is erased if it's not used directly. #20398).Other open pull requests. This is the state on 2026-11-03 at main 675ee5b. It comes from the head of each open PR and from test merges with this branch (10c7d9a).
declarations.tscase to the same corpus. The one that lands second takes the next version and merges the case. Map the frame tokens that start a line #47769 says: "A separate pull request gates that fold on bundling." This is that pull request.call/callee.rs,transpiled.rsandtranspiled/first_recorded_position.rs. Both change code that uses the AOT compiler's copy of the rule of the fold, and this PR removes that copy.EXPECTED_VERSIONto 52: jsx: preserve JSX syntax #20435, js_parser: reject parenthesized arrow parameters #41624, js_parser: don't inline a const read that crossed awithscope #42524, js_parser: do not fold{ f: () => {} }.fto the bare function #42585, Refuse a macro import that is used outside of a call #46239, Keep the namespace import beside a default import that a macro remap replaces #46241, Stop printing NUL for non-UTF-8 bytes: one WTF-8 step for the string printers and line counters #46269, js_parser: fold[n],.charCodeAt,.length,<and unary+of an 8-bit string per UTF-16 code unit #46915, js_parser: register the closure argument of a TypeScript namespace block #46938, js_parser: fix string folds that read non-ASCII 8-bit text by byte or rewrite a write or delete target #47655, transpiler: print a parameter list that holds a comment as it is written #47678, bun test: keep an inline snapshot matcher's frame by name only in a file that uses the test API #47684, js_parser: keep a dropped?.in the chain state until the visit pass #47688, transpiler: never printnew Error(x)as the callError(x)#47693, js_parser: bindimport X = A.Bto the enum or namespace it names #47708, Map stack traces through a loaded file's own source map with --enable-source-maps #47713, js_printer: print a space between a number and the.of a private name #47722, bun test: follow-ups to Vitest compatibility #47727 and js_parser: keepmodule.require()as written in awithbody when not bundling #47805. Each conflicts with this PR insrc/jsc/RuntimeTranspilerCache.rsand in no other file. Each PR that lands after the first takes the next version (js_parser: print a read of one member of an object or array define as that member #47712, stacked on js_parser: fix string folds that read non-ASCII 8-bit text by byte or rewrite a write or delete target #47655, sets 53).src/bundler/linker_context/postProcessJSChunk.rs. This PR addsbundling: true,as the first line ofprint_optionsingenerate_entry_point_tail_js, and bundler: call the wrapped entry point in iife output #37843 removes the comment on the next line. The merge keeps the new line and removes the comment.test/js/bun/repl/repl.test.ts. Upgrade WebKit to 1d4bed41aca8 #47811 (a draft WebKit upgrade) changes the hashes inbundler_bytecode_portable.test.tsthat this PR changes, so the one that lands second generates them again.bun runfolds #47646 makes the AOT compiler model other folds thatbun runapplies. This PR removes a model, because the rewrite behind it only saves bytes and changes what an error says.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/sourcemap/source-positions.test.ts, test/js/bun/repl/repl.test.ts, test/cli/install/bun-pm-diff.test.ts, test/bundler/bundler_bytecode_portable.test.ts