Skip to content

feat(minifier): track boolean state across branches for self-value DCE#22753

Closed
fazba wants to merge 10 commits into
oxc-project:mainfrom
fazba:feat/minifier-14001-self-value-dce
Closed

feat(minifier): track boolean state across branches for self-value DCE#22753
fazba wants to merge 10 commits into
oxc-project:mainfrom
fazba:feat/minifier-14001-self-value-dce

Conversation

@fazba

@fazba fazba commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR improves minifier DCE for cases where code behavior depends on a symbol's own tracked value across statements/branches.

  • Track known boolean symbol state during statement minimization and fold if (id) / if (!id) when possible.
  • Refine call invalidation from "clear all known symbols" to "kill only symbols that the reachable function declaration may write".
  • Use a visitor-based function declaration scope collector to avoid missing nested declarations and reduce ad-hoc traversal logic.
  • Add regression tests covering:
    • reachable function-call invalidation
    • conservative unknown-call invalidation
    • nested declaration tracking
    • unreachable call-path behavior in loop blocks

Closes #14001.

Test plan

  • cargo test -p oxc_minifier dce_if_statement_symbol_tracking_with_function_calls
  • cargo test -p oxc_minifier dce_if_statement
  • cargo test -p oxc_minifier remove_constant_value

fazba and others added 3 commits May 27, 2026 14:45
Teach statement minimization to carry boolean symbol state across statements and fold `if (id)`/`if (!id)` when values are known. Make function-call invalidation precise by killing only symbols written by reachable function-declaration calls, and add regression tests for reachable, unreachable, and nested declaration scenarios.

Co-authored-by: Cursor <cursoragent@cursor.com>
Conclude the in-progress merge from main into this feature branch and keep both upstream updates and local minifier-related changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Drop snapshot temp files generated during test runs so the branch only keeps stable snapshot outputs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codspeed-hq

codspeed-hq Bot commented May 27, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 12.37%

❌ 6 regressed benchmarks
✅ 46 untouched benchmarks
⏩ 14 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation minifier[App.tsx] 12.6 ms 15.5 ms -18.87%
Simulation minifier[kitchen-sink.tsx] 63.7 ms 77.5 ms -17.8%
Simulation minifier[binder.ts] 3.7 ms 4.3 ms -14.84%
Simulation pipeline[kitchen-sink.tsx] 127.6 ms 140.9 ms -9.43%
Simulation pipeline[App.tsx] 34.2 ms 37.1 ms -7.79%
Simulation pipeline[binder.ts] 13.2 ms 13.9 ms -4.5%

Tip

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


Comparing fazba:feat/minifier-14001-self-value-dce (da59177) with main (8c93601)

Open in CodSpeed

Footnotes

  1. 14 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.

Finalize the statement-side boolean update call cleanup and commit the updated minifier allocation snapshot required by CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
@camc314 camc314 added the A-minifier Area - Minifier label May 27, 2026
fazba and others added 6 commits June 2, 2026 17:12
Avoid repeated reference scans when updating branch boolean state around call expressions.
Gate boolean tracking work when statements cannot seed tracked symbols to reduce hot-path overhead.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: fazba <32932149+fazba@users.noreply.github.com>
…lidation

Replace two-phase mutation collection/removal with in-place retain when invalidating
known boolean symbols for direct function-declaration calls, and add an early return
for empty tracking maps to reduce hot-path overhead.

Co-authored-by: Cursor <cursoragent@cursor.com>
graphite-app Bot pushed a commit that referenced this pull request Jun 23, 2026
…#23540)

## What

A `var` initialized to a falsy constant and never reassigned is always falsy. oxc already folds such a `let`/`const` everywhere, but a `var` is held back by a hoisting check (a read before the declarator runs sees the hoisted `undefined`), so a multi-read `var` flag past a non-trivial declarative prelude was never folded.

In **boolean context** that check is unnecessary: a pre-init `var` read yields `undefined`, which is indistinguishable from the falsy init inside `if (x)` / `x ? … : …` / `!x`. So fold such reads to `false` there; the existing `if (false)` dead-code pass removes the branch.

This is the bundled-flag shape behind this issue. Svelte's `var hydrating = false` is read by `if (hydrating)` throughout the runtime; once a client-only bundle tree-shakes the `set_hydrating` setter, `hydrating` is a write-once falsy `var` and the whole hydration path becomes dead and is eliminated.

## Why it is sound

- `SymbolValue::boolean_falsy` is set only for a write-once binding with a falsy constant initializer.
- It folds to `false` **only** in boolean context; value-context reads keep their hoisting-correct behavior (never folded here).
- Gated off for a script's top-level `var` (a global another script can reassign, so an in-module write count of 0 doesn't prove write-once) and for `eval` scopes.

## Verification

- `oxc_minifier` tests pass; idempotent.
- On a real client-only Svelte (Vite/Rollup) bundle, every `if (hydrating)` check is eliminated.
- `minsize`: react 23.14 → 23.12 kB, lodash 70.96 → 70.92 kB (gzip down too). `allocs_minifier.snap`: +20 sys allocs on antd (from evaluating `var` initializers; arena allocs unchanged).
- Cross-validated for soundness: on the Svelte bundle, terser `--module` and esbuild `--format=esm` independently fold the same write-once flag (16 → 0), agreeing with oxc. On a Vue 3 client bundle the runtime's flags are multi-write (e.g. `isInSSRComponentSetup` has two assignments, so it can be truthy) and oxc correctly leaves them untouched — the write-once gate is the right line.

## Scope (partial fix)

This handles the real-world **bundled** shape — a write-once falsy `var` flag read in boolean context (the Svelte/Vue hydration case, where the bundler has already tree-shaken the setter). It does **not** fold the issue's literal source snippet:

```js
let foo = false;
function bar(val) { foo = val; }
if (foo) { bar(true) }
console.log(foo);
```

There `foo` is still reassigned via `bar` (so it isn't write-once), and proving the write dead requires self-referential / flow-sensitive analysis — out of scope here (see #22753 for that direction). So this is a partial step.

Refs #14001.

Prepared with AI assistance.
@Dunqing

Dunqing commented Jul 2, 2026

Copy link
Copy Markdown
Member

Thank you for contributing! Closing this as we have adopted #23540 to solve that issue.

@Dunqing Dunqing closed this Jul 2, 2026
camc314 pushed a commit that referenced this pull request Jul 3, 2026
…#23540)

## What

A `var` initialized to a falsy constant and never reassigned is always falsy. oxc already folds such a `let`/`const` everywhere, but a `var` is held back by a hoisting check (a read before the declarator runs sees the hoisted `undefined`), so a multi-read `var` flag past a non-trivial declarative prelude was never folded.

In **boolean context** that check is unnecessary: a pre-init `var` read yields `undefined`, which is indistinguishable from the falsy init inside `if (x)` / `x ? … : …` / `!x`. So fold such reads to `false` there; the existing `if (false)` dead-code pass removes the branch.

This is the bundled-flag shape behind this issue. Svelte's `var hydrating = false` is read by `if (hydrating)` throughout the runtime; once a client-only bundle tree-shakes the `set_hydrating` setter, `hydrating` is a write-once falsy `var` and the whole hydration path becomes dead and is eliminated.

## Why it is sound

- `SymbolValue::boolean_falsy` is set only for a write-once binding with a falsy constant initializer.
- It folds to `false` **only** in boolean context; value-context reads keep their hoisting-correct behavior (never folded here).
- Gated off for a script's top-level `var` (a global another script can reassign, so an in-module write count of 0 doesn't prove write-once) and for `eval` scopes.

## Verification

- `oxc_minifier` tests pass; idempotent.
- On a real client-only Svelte (Vite/Rollup) bundle, every `if (hydrating)` check is eliminated.
- `minsize`: react 23.14 → 23.12 kB, lodash 70.96 → 70.92 kB (gzip down too). `allocs_minifier.snap`: +20 sys allocs on antd (from evaluating `var` initializers; arena allocs unchanged).
- Cross-validated for soundness: on the Svelte bundle, terser `--module` and esbuild `--format=esm` independently fold the same write-once flag (16 → 0), agreeing with oxc. On a Vue 3 client bundle the runtime's flags are multi-write (e.g. `isInSSRComponentSetup` has two assignments, so it can be truthy) and oxc correctly leaves them untouched — the write-once gate is the right line.

## Scope (partial fix)

This handles the real-world **bundled** shape — a write-once falsy `var` flag read in boolean context (the Svelte/Vue hydration case, where the bundler has already tree-shaken the setter). It does **not** fold the issue's literal source snippet:

```js
let foo = false;
function bar(val) { foo = val; }
if (foo) { bar(true) }
console.log(foo);
```

There `foo` is still reassigned via `bar` (so it isn't write-once), and proving the write dead requires self-referential / flow-sensitive analysis — out of scope here (see #22753 for that direction). So this is a partial step.

Refs #14001.

Prepared with AI assistance.
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: remove code that relies on the value of itself

3 participants