Skip to content

feat: improve function return value tracking - #6065

Merged
lukastaegert merged 4 commits into
rollup:masterfrom
cyyynthia:function-return-values
Aug 25, 2026
Merged

feat: improve function return value tracking#6065
lukastaegert merged 4 commits into
rollup:masterfrom
cyyynthia:function-return-values

Conversation

@cyyynthia

@cyyynthia cyyynthia commented Aug 9, 2025

Copy link
Copy Markdown
Contributor

This PR contains:

  • bugfix
  • feature
  • refactor
  • documentation
  • other

Are tests included?

  • yes (bugfixes and features will not be merged without tests)
  • no

Breaking Changes?

  • yes (breaking changes will not be merged unless absolutely necessary)
  • no

List any relevant issue numbers:

Description

This PR improves the way return values are resolved inside function calls. It does so via 4 changes:

  • The implicit return is now an undefined expression rather than an unknown expression, enabling more optimisations to be performed.
  • When a function has multiple return values, they are all combined using the existing MultiExpression node. It has been expanded to allow resolving literal values if all paths agree on a specific value, trying to resolve its truthiness if there are different values being returned.
  • MultiExpression factors whether a return statement has been reached, which solves tree-shaking will not execute recursively. #5063 since the resolved literal now depends on the treeshaking status.
  • Some deoptimisations aren't as pessimistic as before: instead of making the node immediately give up trying to resolve a value, deoptimizeCache only clears the cached value, allowing it to be re-computed. Not all nodes have been tuned to this less pessimistic approach; only the low hanging fruits relevant for this PR.

@vercel

vercel Bot commented Aug 9, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rollup Ready Ready Preview Aug 25, 2026 4:05am

Request Review

@codecov

codecov Bot commented Aug 9, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.96907% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 98.76%. Comparing base (21528bb) to head (529371a).

Files with missing lines Patch % Lines
src/ast/nodes/BlockStatement.ts 92.85% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6065      +/-   ##
==========================================
- Coverage   98.78%   98.76%   -0.02%     
==========================================
  Files         275      276       +1     
  Lines       10823    10885      +62     
  Branches     2887     2908      +21     
==========================================
+ Hits        10691    10751      +60     
- Misses         89       91       +2     
  Partials       43       43              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 27, 2025

Copy link
Copy Markdown

Thank you for your contribution! ❤️

You can try out this pull request locally by installing Rollup via

npm install cyyynthia/rollup#function-return-values

Notice: Ensure you have installed the latest nightly Rust toolchain. If you haven't installed it yet, please see https://www.rust-lang.org/tools/install to learn how to download Rustup and install Rust.

or load it into the REPL:
https://rollup-mktm8ybza-rollup-js.vercel.app/repl/?pr=6065

@github-actions

github-actions Bot commented Aug 27, 2025

Copy link
Copy Markdown

Performance report

  • BUILD: 6959ms, 837 MB
    • initialize: 0ms, 25.1 MB
    • generate module graph: 2596ms, 632 MB
      • generate ast: 1359ms, 620 MB
    • sort and bind modules: 410ms (+11ms, +2.7%), 692 MB
    • mark included statements: 3953ms, 837 MB
      • treeshaking pass 1: 2330ms, 832 MB
      • treeshaking pass 2: 461ms, 824 MB
      • treeshaking pass 3: 394ms, 838 MB
      • treeshaking pass 4: 383ms, 818 MB
      • treeshaking pass 5: 379ms, 837 MB
  • GENERATE: 706ms (-229ms, -24.5%), 923 MB
    • initialize render: 0ms, 847 MB (-11%)
    • generate chunks: 212ms (+113ms, +113.0%), 838 MB (-10%)
      • optimize chunks: 0ms, 825 MB (-10%)
    • render chunks: 627ms (-219ms, -25.9%), 898 MB
    • transform chunks: 20ms, 923 MB
    • generate bundle: 0ms, 923 MB

@lukastaegert lukastaegert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This PR does a lot of things, and the concept of locallyReachable makes me a little uneasy. Also, I do not fully understand this concept, is this meant as a "there is a chance this node might be included at some point"?
If, for instance, we would just take the literal value of all included return statements into account and then deoptimize if another return statement is included, would this deoptimize too often because the value is queried too early before any return statement is included?

Another point is that so far, I have avoided to track multiple return expressions out of fear of a performance regression. Below is a (completely artificial) example that demonstrates this. At least for me, this will basically make the browser hang for a long time. Removing the pr=6065& from the URL, i.e. using production Rollup, will make it load quickly:

https://rollup-c7a3llkmm-rollup-js.vercel.app/repl/?pr=6065&shareable=JTdCJTIyZXhhbXBsZSUyMiUzQW51bGwlMkMlMjJtb2R1bGVzJTIyJTNBJTVCJTdCJTIyY29kZSUyMiUzQSUyMmZ1bmN0aW9uJTIwYSgpJTIwJTdCJTVDbiUyMCUyMGNvbnNvbGUubG9nKCdhJyklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBiKDEpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwYigyKSU1Q24lMjAlMjByZXR1cm4lMjBiKDMpJTVDbiU3RCU1Q24lNUNuZnVuY3Rpb24lMjBiKHgpJTIwJTdCJTVDbiUyMCUyMGNvbnNvbGUubG9nKCdiJyUyQyUyMHgpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwYygxKSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMGMoMiklNUNuJTIwJTIwcmV0dXJuJTIwYygzKSU1Q24lN0QlNUNuJTVDbmZ1bmN0aW9uJTIwYyh4KSUyMCU3QiU1Q24lMjAlMjBjb25zb2xlLmxvZygnYyclMkMlMjB4KSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMGQoMSklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBkKDIpJTVDbiUyMCUyMHJldHVybiUyMGQoMyklNUNuJTdEJTVDbiU1Q25mdW5jdGlvbiUyMGQoeCklMjAlN0IlNUNuJTIwJTIwY29uc29sZS5sb2coJ2QnJTJDJTIweCklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBlKDEpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwZSgyKSU1Q24lMjAlMjByZXR1cm4lMjBlKDMpJTVDbiU3RCU1Q24lNUNuZnVuY3Rpb24lMjBlKHgpJTIwJTdCJTVDbiUyMCUyMGNvbnNvbGUubG9nKCdlJyUyQyUyMHgpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwZigxKSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMGYoMiklNUNuJTIwJTIwcmV0dXJuJTIwZigzKSU1Q24lN0QlNUNuJTVDbmZ1bmN0aW9uJTIwZih4KSUyMCU3QiU1Q24lMjAlMjBjb25zb2xlLmxvZygnZiclMkMlMjB4KSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMGcoMSklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBnKDIpJTVDbiUyMCUyMHJldHVybiUyMGcoMyklNUNuJTdEJTVDbiU1Q25mdW5jdGlvbiUyMGcoeCklMjAlN0IlNUNuJTIwJTIwY29uc29sZS5sb2coJ2cnJTJDJTIweCklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBoKDEpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwaCgyKSU1Q24lMjAlMjByZXR1cm4lMjBoKDMpJTVDbiU3RCU1Q24lNUNuZnVuY3Rpb24lMjBoKHgpJTIwJTdCJTVDbiUyMCUyMGNvbnNvbGUubG9nKCdoJyUyQyUyMHgpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwaSgxKSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMGkoMiklNUNuJTIwJTIwcmV0dXJuJTIwaSgzKSU1Q24lN0QlNUNuJTVDbmZ1bmN0aW9uJTIwaSh4KSUyMCU3QiU1Q24lMjAlMjBjb25zb2xlLmxvZygnaSclMkMlMjB4KSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMGooMSklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBqKDIpJTVDbiUyMCUyMHJldHVybiUyMGooMyklNUNuJTdEJTVDbiU1Q25mdW5jdGlvbiUyMGooeCklMjAlN0IlNUNuJTIwJTIwY29uc29sZS5sb2coJ2onJTJDJTIweCklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBrKDEpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwaygyKSU1Q24lMjAlMjByZXR1cm4lMjBrKDMpJTVDbiU3RCU1Q24lNUNuZnVuY3Rpb24lMjBrKHgpJTIwJTdCJTVDbiUyMCUyMGNvbnNvbGUubG9nKCdrJyUyQyUyMHgpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwbCgxKSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMGwoMiklNUNuJTIwJTIwcmV0dXJuJTIwbCgzKSU1Q24lN0QlNUNuJTVDbmZ1bmN0aW9uJTIwbCh4KSUyMCU3QiU1Q24lMjAlMjBjb25zb2xlLmxvZygnbCclMkMlMjB4KSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMG0oMSklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBtKDIpJTVDbiUyMCUyMHJldHVybiUyMG0oMyklNUNuJTdEJTVDbiU1Q25mdW5jdGlvbiUyMG0oeCklMjAlN0IlNUNuJTIwJTIwY29uc29sZS5sb2coJ20nJTJDJTIweCklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBuKDEpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwbigyKSU1Q24lMjAlMjByZXR1cm4lMjBuKDMpJTVDbiU3RCU1Q24lNUNuZnVuY3Rpb24lMjBuKHgpJTIwJTdCJTVDbiUyMCUyMGNvbnNvbGUubG9nKCduJyUyQyUyMHgpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwbygxKSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMG8oMiklNUNuJTIwJTIwcmV0dXJuJTIwbygzKSU1Q24lN0QlNUNuJTVDbmZ1bmN0aW9uJTIwbyh4KSUyMCU3QiU1Q24lMjAlMjBjb25zb2xlLmxvZygnbyclMkMlMjB4KSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMHAoMSklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBwKDIpJTVDbiUyMCUyMHJldHVybiUyMHAoMyklNUNuJTdEJTVDbiU1Q25mdW5jdGlvbiUyMHAoeCklMjAlN0IlNUNuJTIwJTIwY29uc29sZS5sb2coJ3AnJTJDJTIweCklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBxKDEpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwcSgyKSU1Q24lMjAlMjByZXR1cm4lMjBxKDMpJTVDbiU3RCU1Q24lNUNuZnVuY3Rpb24lMjBxKHgpJTIwJTdCJTVDbiUyMCUyMGNvbnNvbGUubG9nKCdxJyUyQyUyMHgpJTVDbiUyMCUyMGlmJTIwKE1hdGgucmFuZG9tKCklMjAlM0UlMjAwLjUpJTIwcmV0dXJuJTIwcigxKSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMHIoMiklNUNuJTIwJTIwcmV0dXJuJTIwcigzKSU1Q24lN0QlNUNuJTVDbmZ1bmN0aW9uJTIwcih4KSUyMCU3QiU1Q24lMjAlMjBjb25zb2xlLmxvZygnciclMkMlMjB4KSU1Q24lMjAlMjBpZiUyMChNYXRoLnJhbmRvbSgpJTIwJTNFJTIwMC41KSUyMHJldHVybiUyMHMoMSklNUNuJTIwJTIwaWYlMjAoTWF0aC5yYW5kb20oKSUyMCUzRSUyMDAuNSklMjByZXR1cm4lMjBzKDIpJTVDbiUyMCUyMHJldHVybiUyMHMoMyklNUNuJTdEJTVDbiU1Q25mdW5jdGlvbiUyMHMoeCklMjAlN0IlNUNuJTIwJTIwY29uc29sZS5sb2coJ3MnJTJDJTIweCklNUNuJTIwJTIwcmV0dXJuJTIwdHJ1ZSUzQiU1Q24lN0QlNUNuJTVDbmlmJTIwKGEoKSklMjBjb25zb2xlLmxvZygnT0snKSUzQiU1Q24lNUNuJTIyJTJDJTIyaXNFbnRyeSUyMiUzQXRydWUlMkMlMjJuYW1lJTIyJTNBJTIybWFpbi5qcyUyMiU3RCU1RCUyQyUyMm9wdGlvbnMlMjIlM0ElN0IlN0QlN0Q=

As this is an artificial example, many code-bases will not have this issue. But some will, or at least they will be noticeably slowed, especially if they have some functions with a lot of return statements. To that end, I would make this feature opt-in, e.g. as treeshake.trackAllReturnValues, but add this to the treeshake.preset: "smallest" preset.

So in general, this is how I would propose to proceed

  • Look into aborting binding in a separate PR.
  • Tracking multiple return values shop be opt-in via parameter
  • Can we implement tracking multiple return statements without the additional "isLocallyReachable" static analysis? While the logic is not too complicated, I am worried that it is another separate layer of executed code path analysis that is only used for this one feature, where bugs may be noticed very late, and which will not benefit from improvements in other areas. Another way to do this could be to start tracking return statements the first time either hasEffects, include or includePath are called on a return statement, as that is equivalent to "we know this return statement is in an executed code path". This is also the semantic we use for applyDeoptimizations and actually, this could just become another "deoptimization" to perform on return statements I their applyDeoptimizations method, i.e. add a return value. Now the important thing would be to make sure that when a return statement is added and a consumer already queried the literal return value of the function (tracked via the origin parameter), then that consumer is deoptimized. However, I hope this is not a wild goose chase.

What do you think?

Comment thread src/ast/nodes/BlockStatement.ts Outdated
Comment thread src/ast/nodes/BlockStatement.ts Outdated

@cyyynthia cyyynthia left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hm, you're definitely right that this kind of performance regression is unacceptable (regardless of the exotic/synthetic nature of the scenario). I convinced myself along the way that it shouldn't be too much of a performance issue but I overlooked the exponential growth of return values in deep recursive function call chains that can definitely show up in the real world.

Having full-depth search opt-in seems a good idea. I'm wondering if depth control would allow for a good balance between performance and output quality (by default have a depth of only 1 or 2, and in the smallest preset have a depth of Infinity; i.e. exhaustive return value search.

It's very likely that performance can be improved for some scenarios through caching and smarter exploration, perhaps beyond just this use-case if getLiteralValue* gets smarter. I'll explore this further :)

Comment thread src/ast/nodes/BlockStatement.ts Outdated
Comment thread src/ast/nodes/BlockStatement.ts Outdated
@lukastaegert

Copy link
Copy Markdown
Member

Hi, I am somewhat I slightly lost track of this. What is the current state here, is this currently in a releasable state from your side? Then I would focus on finishing the review next.

@cyyynthia

Copy link
Copy Markdown
Contributor Author

Hi! The state of things hasn't moved much since your last review and the concerns you raised still are valid. I tried a bunch of things locally, with more often than not more issues created than solved... And then I ran out of free time to mess with this PR :(

The main issue remains that the information I need for the reachability check is not known prior to the inclusion process being in progress, at which point it is too late to react (since Rollup works via an additive only strategy). This is the key to solve #5063 robustly, and is quite important to make deep function return evaluation worth the effort.

The PR can likely be split in 2 separate ones; one adding the deep function return value evaluation capabilities, the other making reachability analysis smarter such that treeshaking can have a "recursive" effect without resorting to treeshaking over and over until we have a stable result which would be prohibitively expensive >:)

There are some low hanging fruits that can be cherry picked from the PR and improves some situations too, such as making blocks return undefined (instead of UnknownValue) through their implicit return statement.

@lukastaegert

Copy link
Copy Markdown
Member

Thanks for the quick response! I think I will leave it like this for a little longer from my side and focus on getting Rollup 5 ready, there is a lot still to do. After happy I am happy to invest more time here. But feel free to drive this forward in the meantime yourself.

@cyyynthia
cyyynthia force-pushed the function-return-values branch from e0d5ce0 to 622020f Compare July 14, 2026 19:30
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

@cyyynthia is attempting to deploy a commit to the rollup-js Team on Vercel.

A member of the Team first needs to authorize it.

@cyyynthia

Copy link
Copy Markdown
Contributor Author

I'm back at this! 😼

I've removed the code that was re-implementing treeshaking to mitigate dead code elimination not affecting the outcome of const folding, which was one of the major blockers for this PR to go through.

The second blocker was performance of exhaustively checking return values. I don't fully remember the specifics on the performance impact this PR has. I improved it a bit since but it'd definitely need to be checked again, to know if this needs to be made opt-in or if having it by default is viable.

@cyyynthia
cyyynthia force-pushed the function-return-values branch from 88c072b to 3bc835c Compare July 19, 2026 11:44
@cyyynthia
cyyynthia force-pushed the function-return-values branch 3 times, most recently from ab5f6a6 to d49ca8e Compare July 21, 2026 19:28
@cyyynthia
cyyynthia force-pushed the function-return-values branch from d49ca8e to e2b60b3 Compare July 24, 2026 21:13
@cyyynthia

Copy link
Copy Markdown
Contributor Author

I had an "eureka" moment; as I'm a lot more familiar with the inner workings of Rollup and its internal dance, I've successfully implemented the fix for #5063 without introducing a weird secondary path for detecting dead code. I'm a lot more confident this approach is the right one and can be merged!! :D

@cyyynthia
cyyynthia force-pushed the function-return-values branch from e2b60b3 to f89042b Compare July 24, 2026 22:37
@lukastaegert

Copy link
Copy Markdown
Member

Hi! Great seeing you back, and sorry for my lack of response so far. I had very limited time in the last months. Also, I will be off keyboard for another 2-3 weeks, then reviewing your PRs has high priority!

@lukastaegert lukastaegert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you! I no longer found any obvious flaws here, and would merge this now. Thank you for coming back here! Let's keep an eye on new issues after this is released.

@lukastaegert
lukastaegert enabled auto-merge August 25, 2026 04:21
@lukastaegert
lukastaegert added this pull request to the merge queue Aug 25, 2026
Merged via the queue into rollup:master with commit 456b237 Aug 25, 2026
46 of 47 checks passed
@cyyynthia
cyyynthia deleted the function-return-values branch August 25, 2026 07:19
@github-actions

Copy link
Copy Markdown

This PR has been released as part of rollup@4.63.0. You can test it via npm install rollup.

@lukastaegert

Copy link
Copy Markdown
Member

Just a note, we have one report of an OOM after the release. It might be benign, and there is no reproduction yet, but just a heads-up here: #6487

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tree-shaking will not execute recursively.

2 participants