mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
af82a0707a
`substitute_unary_plus` removes a `+` when the enclosing binary operator will do the `ToNumber` conversion anyway. The existing doc comment reasons carefully about `ToNumber` vs `ToNumeric`, but not about *when* each runs. `+a` converts `a` while evaluating `+a`, before the right operand is evaluated at all. `a - n` converts it in `ApplyStringOrNumericBinaryOperator`, after `n` has been evaluated. Dropping the `+` therefore moves the conversion across `n`, and a right operand with a side effect can change what the conversion sees: ```js var xs = []; console.log((+xs) - (xs.push(1), 0)); // 0, converted while `xs` was still empty // oxc emitted: console.log(xs - (xs.push(1), 0)); // 1, converted after `xs` grew ``` This PR requires the right operand to be free of side effects before dropping a `+` in the left position. It cannot observe the conversion either: under the "Coercion Methods Are Pure" assumption the conversion runs no user code, so there is nothing to observe. The right position (`n - +a`) is left as is: `n` is evaluated first either way, and its `ToNumeric` is a no-op because the existing guard already established that `n` is a number, so nothing runs between evaluating `a` and converting it. `cargo minsize` moves the typescript.js gzip figure down by 10 bytes; every minified size is identical. The reproduction was found by the minifier fuzzer (#25594) at seed 12397. ### Differential testing Every case below was run through oxc and then executed in Node against the unminified source, comparing the result, the mutated state, and the error constructor *and message*. | shape | cases | value worlds | runs | | --- | ---: | ---: | ---: | | operator × 37 hand-written right operands | 814 | 41 | 33,374 | | right operand reading a closure variable the conversion may write | 374 | 41 | 15,334 | | argument shape (`+a.v`, `+f()`, `+(seq(), a)`, `+(+a)`, getter) × nesting | 3,168 | 41 | 129,888 | | random expressions, 3 seeds, depth 2-3 | 7,500 | 41 | 307,500 | The worlds cover `[]`, `{ valueOf: () => 10n }`, `Symbol()`, `Object.create(null)`, objects with getters, `Proxy`, and the numeric edge cases. No difference survives, other than one that predates this pass and is discussed in the doc comment: for a `BigInt` argument both forms throw a `TypeError`, but `+a` says "Cannot convert a BigInt value to a number" while `a - 1` says "Cannot mix BigInt and other types". The error kind never changes. --- Written with AI assistance (Claude Code). Reviewed by me, and the before/after behaviour was checked against Node.js. --------- Signed-off-by: Kotaro Chikuba <miz404@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
28 lines
1.3 KiB
Plaintext
28 lines
1.3 KiB
Plaintext
| Oxc props | ESBuild | Oxc props | ESBuild |
|
|
Original | minified | minified | gzip | gzip | Iterations | File
|
|
-------------------------------------------------------------------------------------
|
|
72.14 kB | 22.48 kB | 23.70 kB | 8.20 kB | 8.54 kB | 2 | react.development.js
|
|
|
|
173.90 kB | 55.53 kB | 59.82 kB | 18.78 kB | 19.33 kB | 2 | moment.js
|
|
|
|
287.63 kB | 89.02 kB | 90.07 kB | 30.87 kB | 31.95 kB | 2 | jquery.js
|
|
|
|
342.15 kB | 114.85 kB | 118.14 kB | 42.73 kB | 44.37 kB | 2 | vue.js
|
|
|
|
544.10 kB | 69.88 kB | 72.48 kB | 25.59 kB | 26.20 kB | 2 | lodash.js
|
|
|
|
555.77 kB | 262.77 kB | 270.13 kB | 87.67 kB | 90.80 kB | 2 | d3.js
|
|
|
|
1.01 MB | 436.09 kB | 458.89 kB | 121.80 kB | 126.71 kB | 2 | bundle.min.js
|
|
|
|
1.25 MB | 634.41 kB | 646.76 kB | 158.45 kB | 163.73 kB | 2 | three.js
|
|
|
|
2.14 MB | 707.71 kB | 724.14 kB | 160.11 kB | 181.07 kB | 2 | victory.js
|
|
|
|
3.20 MB | 959.19 kB | 1.01 MB | 317.26 kB | 331.56 kB | 2 | echarts.js
|
|
|
|
6.69 MB | 2.12 MB | 2.31 MB | 453.48 kB | 488.28 kB | 5 | antd.js
|
|
|
|
10.95 MB | 3.31 MB | 3.49 MB | 850.30 kB | 915.50 kB | 4 | typescript.js
|
|
|