Skip to content

fix(minifier): guard reordered identifier reads#24698

Merged
graphite-app[bot] merged 1 commit into
mainfrom
codex/improve-existing-tdz-guard
Jul 20, 2026
Merged

fix(minifier): guard reordered identifier reads#24698
graphite-app[bot] merged 1 commit into
mainfrom
codex/improve-existing-tdz-guard

Conversation

@Dunqing

@Dunqing Dunqing commented Jul 20, 2026

Copy link
Copy Markdown
Member

A locally read-only identifier is not necessarily stable across a side-effecting call. ESM imports are live bindings, Script-root bindings can be changed by another script, and computed assignment keys bypassed the existing closed-over-TDZ guard. Single-use substitution could therefore move a binding read before a call and change program behavior.

The same incomplete stability query affected logical-assignment folding. A live imported member base could be cached before a call that reassigns the export, making ||= or ??= update the old object instead of the current binding.

Builds on the shared reorder predicates from #24699. Imports, Script-root bindings, unresolved references, syntactically mutated symbols, and closed-over block-scoped reads are treated conservatively across ordinary reads, assignment-target objects, computed keys, and member-base folding. Stable module bindings and pure replacements remain optimizable.

Example

var key = "x";
function init(obj) {
  let value = mutate();
  obj[key] = value;
}

The previous substitution could produce:

var key = "x";
function init(obj) {
  obj[key] = mutate();
}

That captures key before mutate() runs, although another script can change the global binding during the call. The declaration is now retained so the read remains after mutate().

For imported member bases, main can rewrite x.y || (mutate(), x.y = 3) to x.y ||= (mutate(), 3). When mutate reassigns the exported x, the original two-module runtime repro produces 3, while the rewritten program produces 0. The shared stability guard now prevents that fold.

Plain computed assignments still inline stable identifiers and literals. ECMAScript evaluates the key expression and GetValue before the right-hand side, but defers ToPropertyKey until the final PutValue, so arbitrary key expressions remain conservative without losing safe simple-key substitutions.

This improves the existing reorder guard; it does not add a general TDZ model. Sloppy mapped arguments mutation remains a pre-existing unsupported case and is outside this stack.

Verified with the full oxc_minifier suite (622 passed, 42 ignored), clippy with warnings denied, formatting, minsize, allocation snapshots, runtime ordering probes, and repeat-compression checks.

Exact guard ablations attribute the total minsize change of +18 raw and +34 gzip bytes to two fixtures, with no iteration-count changes. TypeScript contributes +9 raw/+32 gzip because retaining nonExports preserves the Script-global __spreadArray lookup after a potentially side-effecting ts.filter call; shifted mangled names amplify its gzip delta. ECharts contributes +9 raw/+2 gzip because the computed-key guard conservatively retains dataIdx when the loop key has writes. Recovering that ECharts case would require more precise write-location or closure analysis rather than weakening the correctness guard. The import guard changes no minsize fixture because the corpus is parsed as Script. System-allocation metrics are unchanged; arena changes are dominated by retained code with minor fold-path shifts.

Stack

  1. refactor(minifier): centralize reorder stability checks #24699 — centralize reorder stability checks (merged)
  2. fix(minifier): guard reordered identifier reads #24698 — guard reordered identifier reads

AI assistance: Codex and Claude were used for implementation, adversarial review, and verification. The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.

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

codspeed-hq Bot commented Jul 20, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 57 untouched benchmarks
⏩ 19 skipped benchmarks1


Comparing codex/improve-existing-tdz-guard (dbc74be) with codex/refactor-reorder-stability-checks (5c65a14)

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 changed the base branch from main to graphite-base/24698 July 20, 2026 05:12
@Dunqing
Dunqing force-pushed the codex/improve-existing-tdz-guard branch from 0261adf to 1237b61 Compare July 20, 2026 05:12
@Dunqing
Dunqing changed the base branch from graphite-base/24698 to codex/refactor-reorder-stability-checks July 20, 2026 05:12

Dunqing commented Jul 20, 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 force-pushed the codex/improve-existing-tdz-guard branch from 1237b61 to a265579 Compare July 20, 2026 05:15
@Dunqing
Dunqing force-pushed the codex/refactor-reorder-stability-checks branch from 3247c84 to 24cc5c4 Compare July 20, 2026 05:25
@Dunqing
Dunqing force-pushed the codex/improve-existing-tdz-guard branch 2 times, most recently from 26ab1f1 to 6613e6b Compare July 20, 2026 06:51
@Dunqing
Dunqing force-pushed the codex/refactor-reorder-stability-checks branch from 5ef3e63 to 5c65a14 Compare July 20, 2026 06:59
@Dunqing
Dunqing force-pushed the codex/improve-existing-tdz-guard branch from 6613e6b to dbc74be Compare July 20, 2026 06:59
@Dunqing
Dunqing marked this pull request as ready for review July 20, 2026 09:06
@Dunqing
Dunqing requested a review from overlookmotel as a code owner July 20, 2026 09:06
@Dunqing Dunqing added the 0-merge Merge with Graphite Merge Queue label Jul 20, 2026

Dunqing commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

Merge activity

A locally read-only identifier is not necessarily stable across a side-effecting call. ESM imports are live bindings, Script-root bindings can be changed by another script, and computed assignment keys bypassed the existing closed-over-TDZ guard. Single-use substitution could therefore move a binding read before a call and change program behavior.

The same incomplete stability query affected logical-assignment folding. A live imported member base could be cached before a call that reassigns the export, making `||=` or `??=` update the old object instead of the current binding.

Build on the shared reorder predicates from #24699. Imports, Script-root bindings, unresolved references, syntactically mutated symbols, and closed-over block-scoped reads are treated conservatively across ordinary reads, assignment-target objects, computed keys, and member-base folding. Stable module bindings and pure replacements remain optimizable.

## Example

```js
var key = "x";
function init(obj) {
  let value = mutate();
  obj[key] = value;
}
```

The previous substitution could produce:

```js
var key = "x";
function init(obj) {
  obj[key] = mutate();
}
```

That captures `key` before `mutate()` runs, although another script can change the global binding during the call. The declaration is now retained so the read remains after `mutate()`.

For imported member bases, main can rewrite `x.y || (mutate(), x.y = 3)` to `x.y ||= (mutate(), 3)`. When `mutate` reassigns the exported `x`, the original two-module runtime repro produces `3`, while the rewritten program produces `0`. The shared stability guard now prevents that fold.

Plain computed assignments still inline stable identifiers and literals. ECMAScript evaluates the key expression and `GetValue` before the right-hand side, but defers `ToPropertyKey` until the final `PutValue`, so arbitrary key expressions remain conservative without losing safe simple-key substitutions.

This improves the existing reorder guard; it does not add a general TDZ model. Sloppy mapped `arguments` mutation remains a pre-existing unsupported case and is outside this stack.

Verified with the full `oxc_minifier` suite (622 passed, 42 ignored), clippy with warnings denied, formatting, minsize, allocation snapshots, runtime ordering probes, and repeat-compression checks. `cargo minsize` measured 18 additional raw bytes across `echarts.js` and `typescript.js`, with no iteration-count changes. System-allocation metrics are unchanged; arena changes are dominated by retained code with minor fold-path shifts.

## Stack

1. #24699 — centralize reorder stability checks
2. **#24698 — guard reordered identifier reads**

AI assistance: Codex and Claude were used for implementation, adversarial review, and verification. The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
graphite-app Bot pushed a commit that referenced this pull request Jul 20, 2026
Single-use substitution checked the same reorder-safety decision in separate forms for ordinary identifier reads and member assignment targets. That duplication made future stability rules easy to apply to one consumer but miss in another.

Route both consumers through shared resolved-symbol stability and closed-over block-scoped-read predicates. Each identifier reference is resolved once, after which the symbol-level checks share the result. The refactor preserves three non-obvious boundaries: unresolved references remain blocked through `is_none_or`, non-simple member objects remain conservative through the `_ => true` arm, and the inverted ordinary-read condition is equivalent to the previous `resolved && !mutated && !closed_over` check. The closed-over predicate is named for what it structurally proves rather than implying that every such read is currently in the TDZ. TDZ fixtures use parameter callees so future import handling cannot make them pass vacuously.

There is no behavior or output change. Verified with 621 passing `oxc_minifier` tests and 42 ignored tests, clippy with warnings denied, formatting, `just minsize`, and allocation regeneration; minsize and allocation snapshots are byte-identical.

## Stack

1. **#24699 — centralize reorder stability checks**
2. #24698 — guard reordered identifier reads

AI assistance: Codex and Claude were used for implementation, adversarial review, and verification. The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
@graphite-app
graphite-app Bot force-pushed the codex/refactor-reorder-stability-checks branch from 5c65a14 to 6e92cc0 Compare July 20, 2026 09:07
@graphite-app
graphite-app Bot force-pushed the codex/improve-existing-tdz-guard branch from dbc74be to f145d73 Compare July 20, 2026 09:08
Base automatically changed from codex/refactor-reorder-stability-checks to main July 20, 2026 09:12
@graphite-app
graphite-app Bot merged commit f145d73 into main Jul 20, 2026
31 checks passed
@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label Jul 20, 2026
@graphite-app
graphite-app Bot deleted the codex/improve-existing-tdz-guard branch July 20, 2026 09:13
camc314 added a commit that referenced this pull request Jul 21, 2026
### 💥 BREAKING CHANGES

- 54cc121 ast: [**BREAKING**] Split `MetaProperty` into `ImportMeta` and
`NewTarget` (#24557) (camc314)

### 🚀 Features

- 4c71560 parser: More friendly error for spread element in dynamic
imports (#24705) (sapphi-red)
- 7b045cd minfier: Drop last break from last switch case (#24673)
(Armano)
- 7d3c178 minifier: Remove unreachable recursive functions (#24125)
(Dunqing)
- 94f99b3 ast: Allow `NONE` to be passed to AST builder methods where
`Option<ArenaVec>` is expected (#24629) (overlookmotel)
- 77230c5 ast: Accept arrays for `ArenaVec` params of AST builder
methods (#24621) (overlookmotel)
- f08b152 allocator: Implement `FromIn` for array to `Vec` conversion
(#24620) (overlookmotel)
- 2338c13 track-memory-allocations: Track heap deallocs, alloc bytes,
and peak growth (#24619) (Boshen)
- 7aa4739 syntax,transformer: Move JSX entity decoder to `oxc_syntax`
(#24617) (camc314)
- 2b097c4 str: Export `Str` as `ArenaStr` (#24604) (overlookmotel)
- 3acf4c1 minifier: Expand switch optimiation to remove empty cases
(#24520) (Armano)
- 129b759 parser: Improve diagnostics for unparenthesized LHS on
exponential expr (#24569) (camc314)
- 4d0c601 minifier: Fold arithmetic over undefined and null operands
(#24485) (Dunqing)
- 91541dd minifier: Drop empty switch statements  (#24527) (Armano)
- d05224d ast_visit: Generate VisitJs visitor that skips TypeScript
type-space (#24499) (Boshen)
- 3d22307 parser: Add `ParseOptions::enable_ident_hashes` (#24491)
(Boshen)

### 🐛 Bug Fixes

- 64c2241 minifier: Align class heritage removal with assumptions
(#24533) (Dunqing)
- 48b59f4 parser: Span ambient generator diagnostics (#24711) (camc314)
- e750a82 ecmascript: Fix false negative for may_have_side_effects on
dynamic property access (#24709) (sapphi-red)
- f145d73 minifier: Guard reordered identifier reads (#24698) (Dunqing)
- a2ef382 isolated-declarations: Reject `window.Symbol` as global symbol
reference (#24689) (camc314)
- b1bcf72 minifier: Invalidate facts for redeclared bindings (#24658)
(Dunqing)
- 921b834 minifier: Don't treat a conditionally-assigned var as
write-once (#24650) (Dunqing)
- 061af1f minifier: Avoid stale pure function summaries (#24636)
(Dunqing)
- 40c2f43 allocator: `Vec::from_array_in` do not allocate zero-length
array (#24628) (overlookmotel)
- 70994ae codegen: Preserve comments before expression operands (#24510)
(Dunqing)
- 7b4baff parser: Reject new import member access (#23459) (camc314)
- 128b385 minifier: Clippy warning with no-debug-assertions (#24547)
(camc314)
- 8421feb parser: Use first `as` span for imported name (#24537)
(leaysgur)
- c517aa0 parser: Reject invalid accessor assertions (#24504) (camc314)

### ⚡ Performance

- 884d9eb parser: Pre-size cover-grammar assignment target buffers
(#24683) (Boshen)
- d3f07a0 diagnostics: Box OxcDiagnosticInner to reduce binary size
(#24665) (Boshen)
- bcc9de0 parser: Defer diagnostic creation until parse exit (#24663)
(Boshen)
- c35d8ab allocator: Mark `ReplaceWith` panic path cold (#24515)
(camc314)
- ba65790 semantic, allocator: Branchless `clone_in` for semantic IDs
(#24564) (overlookmotel)
- 747feec parser: Build AST nodes with the AST builder instead of
cloning (#24540) (Boshen)
- ba35a0d react_compiler: Use IndexVec for dense id-keyed maps (#24549)
(Boshen)
- b685062 react_compiler: Keep small hot-path collections inline
(#24514) (Marius Schulz)
- a149e95 transformer: Outline rare expression exits (#24512) (camc314)
- 7808a6e react_compiler: Make aliasing effects cheap to intern and
clone (#24506) (Marius Schulz)
- 3a36f2a react_compiler: Store AbstractValue reasons as a u16 bitmask
(#24480) (Boshen)
- 1c96753 react_compiler: Use FxHashMap for the lookup-only aliasing
node map (#24490) (Boshen)

Co-authored-by: Cameron <[email protected]>
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.

1 participant