Skip to content

feat(minifier): remove unreachable recursive functions#24125

Merged
graphite-app[bot] merged 1 commit into
mainfrom
minifier-recursive-dce
Jul 18, 2026
Merged

feat(minifier): remove unreachable recursive functions#24125
graphite-app[bot] merged 1 commit into
mainfrom
minifier-recursive-dce

Conversation

@Dunqing

@Dunqing Dunqing commented Jul 3, 2026

Copy link
Copy Markdown
Member

Reference counts cannot remove unreachable recursive functions because their self- and mutual references keep the counts nonzero even when no executable code can reach them.

This change computes function reachability after scoping is flushed. Normalize registers function declarations and bindings with implicit runtime observers. References outside registered functions make their targets live; references owned by a live registered function propagate liveness to their targets.

Graph deadness is used only to remove function declarations. Other transformations continue to use ordinary reference counts together with implicit-observability checks. Direct eval disables the analysis for the whole program while present. Exports, Script-root bindings, Annex B aliases, and using disposal remain observable even when they have no resolved AST reference.

Example

Input:

function self() { self() }
function a() { b() }
function b() { a() }
export {};

Output:

export {};

Supported

  • Self-recursive and mutually recursive function declarations.
  • References nested in blocks, arrows, function expressions, classes, and parameter defaults. The nearest registered function declaration owns each reference.
  • ES modules, CommonJS, and local functions in Scripts. Script-root bindings remain visible to later scripts.
  • Full compression and DCE-only mode.
  • Reads, writes, exports, using disposal, and direct eval preserve live functions.
  • Exact self-recursive variable initializers whose value is a FunctionExpression or ArrowFunctionExpression, including classic for initializers.

Conservatively retained

  • Mutual cycles between variable declarators, such as const a = () => b(); const b = () => a().
  • Recursive class cycles and mixed function/declarator/class cycles.
  • Wrapped or arbitrary function-valued initializers, such as const f = (effect(), () => f()).
  • Recursive declarators in for-in and for-of heads.
  • Sloppy unhoisted Annex B block functions, whose runtime alias write is not represented by their block-scoped symbol.

Performance

CodSpeed's report for e71c392 measured a 3.74% regression on binder.ts, where the analysis finds no dead graph. On kitchen-sink.tsx, removing 39 function declarations covering 25.3 kB of source early improved the minifier benchmark by 21.01% and the full pipeline by 8.76%. A separate 15-run Script/CommonJS corpus measured a 1.67% median increase overall, below the 2% acceptance threshold.

Verified with all-feature workspace tests, lint, rustdoc, formatting, AST generation, minsize and allocation snapshots, and the repeat-compression corpus.

Closes #13105
Closes #17364

Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository's AI usage policy.

@github-actions github-actions Bot added the A-minifier Area - Minifier label Jul 3, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 3, 2026

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚡ 2 improved benchmarks
❌ 1 regressed benchmark
✅ 54 untouched benchmarks
⏩ 19 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation minifier[binder.ts] 3.6 ms 3.8 ms -3.87%
Simulation minifier[kitchen-sink.tsx] 52.4 ms 43.2 ms +21.23%
Simulation pipeline[kitchen-sink.tsx] 116.9 ms 107.1 ms +9.08%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing minifier-recursive-dce (e3c8830) with main (8694167)

Open in CodSpeed

Footnotes

  1. 19 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Dunqing
Dunqing force-pushed the minifier-recursive-dce branch from 90fbef6 to 16e26dd Compare July 9, 2026 15:24
@Dunqing
Dunqing force-pushed the minifier-drop-dead-call-args branch from 0ac62bc to cca73a6 Compare July 9, 2026 15:24

Dunqing commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent changes, fast-track this PR to the front of the merge queue

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@Dunqing
Dunqing changed the base branch from minifier-drop-dead-call-args to graphite-base/24125 July 9, 2026 15:32
@Dunqing
Dunqing force-pushed the graphite-base/24125 branch from cca73a6 to 8c1d74b Compare July 9, 2026 15:32
@Dunqing
Dunqing force-pushed the minifier-recursive-dce branch from 16e26dd to 54ad60c Compare July 9, 2026 15:32
@Dunqing
Dunqing changed the base branch from graphite-base/24125 to main July 9, 2026 15:32
@Dunqing Dunqing added the run-monitor-oxc Add to a PR to dispatch oxc-project/monitor-oxc CI against it label Jul 9, 2026
@oxc-guard

oxc-guard Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@oxc-guard oxc-guard Bot removed the run-monitor-oxc Add to a PR to dispatch oxc-project/monitor-oxc CI against it label Jul 9, 2026
@Dunqing Dunqing added the run-monitor-oxc Add to a PR to dispatch oxc-project/monitor-oxc CI against it label Jul 10, 2026
@oxc-guard oxc-guard Bot removed the run-monitor-oxc Add to a PR to dispatch oxc-project/monitor-oxc CI against it label Jul 10, 2026
@Dunqing
Dunqing changed the base branch from main to graphite-base/24125 July 10, 2026 09:18
@Dunqing
Dunqing force-pushed the minifier-recursive-dce branch from e7c262d to 8fb1d34 Compare July 10, 2026 09:18
@Dunqing
Dunqing changed the base branch from graphite-base/24125 to minifier-symbol-liveness July 10, 2026 09:19
@Dunqing
Dunqing force-pushed the minifier-recursive-dce branch from 8fb1d34 to 5ab2b8c Compare July 11, 2026 14:31
@Dunqing
Dunqing force-pushed the minifier-symbol-liveness branch from 7defb37 to 0f64006 Compare July 11, 2026 14:31
@graphite-app
graphite-app Bot force-pushed the minifier-symbol-liveness branch from 0f64006 to 78da987 Compare July 11, 2026 14:49
@graphite-app
graphite-app Bot force-pushed the minifier-recursive-dce branch from 5ab2b8c to c651b5b Compare July 11, 2026 14:50
@Dunqing
Dunqing force-pushed the minifier-symbol-liveness branch from 78da987 to 1d41780 Compare July 12, 2026 12:47
@Dunqing
Dunqing force-pushed the minifier-recursive-dce branch 2 times, most recently from 1931f80 to b6de9be Compare July 13, 2026 00:15
@Dunqing
Dunqing force-pushed the minifier-symbol-liveness branch from 1d41780 to 111c30d Compare July 13, 2026 00:15
@graphite-app
graphite-app Bot force-pushed the minifier-symbol-liveness branch 2 times, most recently from d2cb6e2 to 2fa0d96 Compare July 14, 2026 14:05
@Dunqing
Dunqing force-pushed the minifier-recursive-dce branch from 8ad3e07 to 84438bb Compare July 14, 2026 14:26
@Dunqing
Dunqing force-pushed the minifier-symbol-liveness branch 2 times, most recently from 86e3737 to c405a12 Compare July 14, 2026 16:03
@Dunqing
Dunqing force-pushed the minifier-recursive-dce branch from 84438bb to 5e130c9 Compare July 14, 2026 16:03
@Dunqing
Dunqing force-pushed the minifier-symbol-liveness branch from c405a12 to 5abf9a7 Compare July 15, 2026 02:00
@Dunqing
Dunqing force-pushed the minifier-recursive-dce branch from 5e130c9 to bff6562 Compare July 15, 2026 02:00
@Dunqing Dunqing added the run-monitor-oxc Add to a PR to dispatch oxc-project/monitor-oxc CI against it label Jul 15, 2026
@oxc-guard oxc-guard Bot removed the run-monitor-oxc Add to a PR to dispatch oxc-project/monitor-oxc CI against it label Jul 15, 2026
@Dunqing
Dunqing force-pushed the minifier-symbol-liveness branch from 5abf9a7 to 6dab15b Compare July 15, 2026 02:41
@Dunqing
Dunqing force-pushed the minifier-recursive-dce branch 2 times, most recently from bf11ebf to e8873c9 Compare July 15, 2026 05:21
@Dunqing
Dunqing force-pushed the minifier-symbol-liveness branch from 6dab15b to a46ba8a Compare July 15, 2026 05:21
@Dunqing Dunqing added the run-monitor-oxc Add to a PR to dispatch oxc-project/monitor-oxc CI against it label Jul 15, 2026
@oxc-guard oxc-guard Bot removed the run-monitor-oxc Add to a PR to dispatch oxc-project/monitor-oxc CI against it label Jul 15, 2026
@Dunqing Dunqing added the run-monitor-oxc Add to a PR to dispatch oxc-project/monitor-oxc CI against it label Jul 15, 2026
@Dunqing

Dunqing commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 5570745ede

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/oxc_minifier/src/symbol_liveness/mod.rs Outdated
Comment thread crates/oxc_minifier/src/symbol_liveness/mod.rs Outdated
Comment thread crates/oxc_minifier/src/compressor.rs Outdated
@Dunqing

Dunqing commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

I paired with Codex and Claude Code to review much of this PR from different angles. I think I've got a solid understanding of the algorithm and its logic. I made several follow-up commits, mostly focused on naming, simplification, and comments to make the implementation easier to follow.

How did I do it?

I worked through the parts I found difficult with both tools. I asked them to explain the reasoning, challenge proposed changes, point out why some ideas would not work, and suggest simpler alternatives. When their suggestions conflicted, I compared their reasoning and asked follow-up questions. I only made changes after I understood the context and trade-offs and agreed with the conclusion.

@Dunqing

Dunqing commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

@armano2 I am happy you are reviewing this! I made several changes locally while reviewing this PR. And now I think this PR is ready for review. Most of the changes I made after your review are making this PR into a good state for reviewing and there are no behavior changes, as I mentioned in #24125 (comment). Feel free to review again!

Comment thread crates/oxc_minifier/src/symbol_liveness/mod.rs

Dunqing commented Jul 18, 2026

Copy link
Copy Markdown
Member Author

Merge activity

Reference counts cannot remove unreachable recursive functions because their self- and mutual references keep the counts nonzero even when no executable code can reach them.

This change computes function reachability after scoping is flushed. Normalize registers function declarations and bindings with implicit runtime observers. References outside registered functions make their targets live; references owned by a live registered function propagate liveness to their targets.

Graph deadness is used only to remove function declarations. Other transformations continue to use ordinary reference counts together with implicit-observability checks. Direct `eval` disables the analysis for the whole program while present. Exports, Script-root bindings, Annex B aliases, and `using` disposal remain observable even when they have no resolved AST reference.

## Example

Input:

```js
function self() { self() }
function a() { b() }
function b() { a() }
export {};
```

Output:

```js
export {};
```

## Supported

- Self-recursive and mutually recursive function declarations.
- References nested in blocks, arrows, function expressions, classes, and parameter defaults. The nearest registered function declaration owns each reference.
- ES modules, CommonJS, and local functions in Scripts. Script-root bindings remain visible to later scripts.
- Full compression and DCE-only mode.
- Reads, writes, exports, `using` disposal, and direct `eval` preserve live functions.
- Exact self-recursive variable initializers whose value is a `FunctionExpression` or `ArrowFunctionExpression`, including classic `for` initializers.

## Conservatively retained

- Mutual cycles between variable declarators, such as `const a = () => b(); const b = () => a()`.
- Recursive class cycles and mixed function/declarator/class cycles.
- Wrapped or arbitrary function-valued initializers, such as `const f = (effect(), () => f())`.
- Recursive declarators in `for-in` and `for-of` heads.
- Sloppy unhoisted Annex B block functions, whose runtime alias write is not represented by their block-scoped symbol.

## Performance

[CodSpeed's report for `e71c392`](#24125 (comment)) measured a 3.74% regression on `binder.ts`, where the analysis finds no dead graph. On `kitchen-sink.tsx`, removing 39 function declarations covering 25.3 kB of source early improved the minifier benchmark by 21.01% and the full pipeline by 8.76%. A separate 15-run Script/CommonJS corpus measured a 1.67% median increase overall, below the 2% acceptance threshold.

Verified with all-feature workspace tests, lint, rustdoc, formatting, AST generation, minsize and allocation snapshots, and the repeat-compression corpus.

Closes #13105
Closes #17364

Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository's AI usage policy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-minifier Area - Minifier

Projects

None yet

Development

Successfully merging this pull request may close these issues.

minifier: unused circular function calls minifier: drop functions, even when used recursively

2 participants