Skip to content

fix(ecmascript): treat update expressions as unconditionally side-effectful#21456

Merged
graphite-app[bot] merged 1 commit into
mainfrom
fix/update-expression-side-effects
Apr 15, 2026
Merged

fix(ecmascript): treat update expressions as unconditionally side-effectful#21456
graphite-app[bot] merged 1 commit into
mainfrom
fix/update-expression-side-effects

Conversation

@Dunqing

@Dunqing Dunqing commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

obj.prop++ performs an implicit GetValue + ToNumeric + PutValue. The ToNumeric coercion alone can invoke valueOf/Symbol.toPrimitive on the old value, and the read/write may trigger getters, setters, or Proxy traps.

Previously UpdateExpression::may_have_side_effects only checked property_write_side_effects() and treated the update as free when disabled — letting the minifier drop counter.value++ even when counter is externally observable.

Fix

Return true unconditionally for UpdateExpression. Terser, esbuild, Rollup, and SWC all hardcode ++/-- as unconditionally side-effectful — this matches their behavior, and mirrors how compound assignments like a.b += 1 are already handled one impl above (AssignmentExpression).

Impact

Under rolldown's tree-shake config (propertyWriteSideEffects: false), these are now correctly preserved instead of being dropped:

counter.value++;
(class { static { ++counter.count; } });
class A { [counter.another++] = 123; }

Reported

rolldown/rolldown#9094 (comment)

Dunqing commented Apr 15, 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.

@github-actions github-actions Bot added A-minifier Area - Minifier C-bug Category - Bug labels Apr 15, 2026
@codspeed-hq

codspeed-hq Bot commented Apr 15, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 48 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing fix/update-expression-side-effects (1860cc7) with main (b3ed467)2

Open in CodSpeed

Footnotes

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

  2. No successful run was found on main (f5deb55) during the generation of this report, so b3ed467 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Dunqing
Dunqing marked this pull request as ready for review April 15, 2026 04:55
@Dunqing

Dunqing commented Apr 15, 2026

Copy link
Copy Markdown
Member Author

I remember the original behavior comes from Rolldown for some tests; maybe we need a fix in Rolldown as well.

@Dunqing
Dunqing requested a review from sapphi-red April 15, 2026 04:56
@Dunqing
Dunqing force-pushed the fix/update-expression-side-effects branch from 0879511 to 1860cc7 Compare April 15, 2026 07:46
@Dunqing Dunqing added the 0-merge Merge with Graphite Merge Queue label Apr 15, 2026

Dunqing commented Apr 15, 2026

Copy link
Copy Markdown
Member Author

Merge activity

…ectful (#21456)

## Summary

`obj.prop++` performs an implicit `GetValue` + `ToNumeric` + `PutValue`. The `ToNumeric` coercion alone can invoke `valueOf`/`Symbol.toPrimitive` on the old value, and the read/write may trigger getters, setters, or Proxy traps.

Previously `UpdateExpression::may_have_side_effects` only checked `property_write_side_effects()` and treated the update as free when disabled — letting the minifier drop `counter.value++` even when `counter` is externally observable.

## Fix

Return `true` unconditionally for `UpdateExpression`. Terser, esbuild, Rollup, and SWC all hardcode `++`/`--` as unconditionally side-effectful — this matches their behavior, and mirrors how compound assignments like `a.b += 1` are already handled one impl above (`AssignmentExpression`).

## Impact

Under rolldown's tree-shake config (`propertyWriteSideEffects: false`), these are now correctly preserved instead of being dropped:

```js
counter.value++;
(class { static { ++counter.count; } });
class A { [counter.another++] = 123; }
```

## Reported

rolldown/rolldown#9094 ([comment](rolldown/rolldown#9094 (comment)))
@graphite-app
graphite-app Bot force-pushed the fix/update-expression-side-effects branch from 1860cc7 to d7a359a Compare April 15, 2026 14:13
@graphite-app
graphite-app Bot merged commit d7a359a into main Apr 15, 2026
26 checks passed
@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label Apr 15, 2026
@graphite-app
graphite-app Bot deleted the fix/update-expression-side-effects branch April 15, 2026 14:17
Dunqing added a commit that referenced this pull request Apr 16, 2026
### 💥 BREAKING CHANGES

- 24fb7eb allocator: [**BREAKING**] Rename `Box` and `Vec` methods
(#21395) (overlookmotel)

### 🚀 Features

- ce5072d parser: Support `turbopack` magic comments (#20803) (Kane
Wang)
- f5deb55 napi/transform: Expose `optimizeConstEnums` and
`optimizeEnums` options (#21388) (Dunqing)
- 24b03de data_structures: Introduce `NonNullConst` and `NonNullMut`
pointer types (#21425) (overlookmotel)

### 🐛 Bug Fixes

- d7a359a ecmascript: Treat update expressions as unconditionally
side-effectful (#21456) (Dunqing)
- 56af2f4 transformer/async-to-generator: Correct scope of inferred
named FE in async-to-generator (#21458) (Dunqing)
- b3ed467 minifier: Avoid illegal `var;` when folding unused arguments
copy loop (#21421) (fazba)
- b0e8f13 minifier: Preserve `var` inside `catch` with same-named
parameter (#21366) (Dunqing)
- 4fb73a7 transformer/typescript: Preserve execution order for accessor
with `useDefineForClassFields: false` (#21369) (Dunqing)

### ⚡ Performance

- da3cc16 parser: Refactor out `LexerContext` (#21275) (Ulrich Stark)

### 📚 Documentation

- c5b19bb allocator: Reformat comments in `Arena` (#21448)
(overlookmotel)
- 091e88e lexer: Update doc comment about perf benefit of reading
through references (#21423) (overlookmotel)
- 922cbee allocator: Remove references to "bump" from comments (#21397)
(overlookmotel)

Co-authored-by: Dunqing <[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 C-bug Category - Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants