Use `Ident` in the property mangler. This reduces the `Set::contains` operation faster as the hashing doesn't happen.
The property names are not always an "ident" so it may not make sense to use the `Ident` type. But currently the `Ident` type allows characters that are not allowed in identifiers so the code works.
I used AI to generate the code and then reviewed the code and iterated a bit.
`ParserReturn` contains a property `panicked`.
The name `panicked` is misleading. The parser did _not_ panic. When `true`, it means that the parser encountered a fatal error and is unable to parse the source, because it contains an unrecoverable syntax error.
Rename this property to `fatal_error`, to better describe what it means.
This is a breaking change, but one that's trivial to fix in downstream code - the property doesn't change its meaning or behavior, just its name.
All the changes in this PR in other crates are just reacting to this change and making everywhere use the term "fatal error" instead of "panicked", so the codebase is consistent.
add test cases for uncovered code and branches that should be tested:
verified with llvm-cov
```sh
cargo +nightly llvm-cov clean --workspace && cargo +nightly llvm-cov --branch --html -p oxc_minifier --test mod
```
i found out that our coverage is not really ideal while working on
#26264
## Summary
- Return `OutOfRange` when a `StringCharAt` position cannot fit in `usize`, avoiding a panic on 32-bit targets.
- Cover the `u32` boundary in direct evaluation and minifier constant folding, alongside existing conversion and UTF-16 edge cases.
improve negation of 1 and 0 to 0 and 1 in boolean context
previously only `!!0` and `!!1` could be inlined to `1` and `0` and now
`!0` and `!1` will also be inlined,
this change allows us to flip numeric values in-place instead of having
to first to create unary and remove it afterwards
```
!0 was converted before to 1 in some cases:
1. negate 1
2. !1 - add unary
3. 0 - remove unary
now:
1. negate 1
2. 0
```
expands leading assignment inlining into variable declarations for
`return`, `switch`, `if`, `throw`, `for`, `for in`, `for of`
```js
// return
var a; return a = b(), c;
var a = b(); return c;
// throw
var a; throw a = b(), c;
var a = b(); throw c;
// switch
var a; switch (a = b(), c) {}
var a = b(); switch (c) {}
// for
var a; for (a = b(), c; ;) d;
var a = b(); for (c; ;) d;
// if
var a; if (a = b(), c) d;
var a = b(); if (c) d;
```
fixes#25949
Expand negation to include move of `!` operator to inner children
```js
!(a == b && c == 3 && a)
a != b || c != 3 | !a
```
delta calculation is dependent on boolean context
- if we expect truthly/falsly value negation of `!a` will be converted
to `a` (-1 delta)
- in ambiguous context `!a` will be converted to `!!a` (+1 delta)
```js
a = !!(!b || !c);
// ^ ^ ^ -3
a = !(b || c)
// ^ ^ ^ +1 - if we could detect if parens can be removed it would be -1
// ^^^ ^^ -1 - if we would know that we can remove parens
```
## Summary
- Apply `ToString(undefined)` when `String.prototype.indexOf` or `lastIndexOf` is called without a search argument.
- Keep folding strings without the resulting `"undefined"` substring to `-1`.
- Add focused helper and minifier regression coverage.
```js
"undefined".indexOf(); // 0
"undefined".lastIndexOf(); // 0
"abcdef".indexOf(); // -1
"abcdef".lastIndexOf(); // -1
```
## Summary
`StringToNumber` previously removed ECMAScript whitespace only from the beginning of a string. Legal trailing whitespace therefore remained in the value passed to Rust’s numeric parsers, causing valid JavaScript numeric strings to produce `NaN`.
For example:
```js
"1 " * 2 // incorrectly folded to NaN
"0x10 " - 0 // incorrectly folded to NaN
```
This changes `trim_start_matches` to `trim_matches`, applying the existing ECMAScript whitespace predicate to both ends of the string.
After the change:
```js
"1 " * 2 // folds to 2
"0x10 " - 0 // folds to 16
```
The custom predicate remains important because ECMAScript and Rust define whitespace differently. Tests cover every ECMAScript whitespace character and verify that non-ECMAScript whitespace such as U+0085 is not accepted.
Move removal of dead code from switch stmts to remove_dead_code, this
change enables processing of switch stmts whose parent is not list of
stmts
current minification processes stmts whose parents are block stmts and
they are last stmt of set block, this change also allows us to remove
empty blocks and last break from switch cases whose parent is not block
stmt
```js
if (a) { b(); if (c) switch (d) { case 1: case 2: break; } c(); }
// ^~~~~~ due to body not being a block we where not able to process this
// Current:
if (a) { if (b(), c) switch (d) { case 1: case 2: break } c() }
// ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ this switch case is not being processed
// This branch:
a && (b(), c && d, c());
// ^ switch cases can be correctly processed
```
note: this change applies only dce, and dosn't substitute switch stmts
with if'stmts
#### changes in stack:
- https://github.com/oxc-project/oxc/pull/25405
- https://github.com/oxc-project/oxc/pull/25403👈
- `main`
```js
for (var i = 0; i++ < 1;) {
if (x) break;
else function f() {}
f = 0;
}
console.log(typeof f, f);
```
was compressed to
```js
for (var i = 0; i++ < 1 && !x;) {
function f() {}
f = 0;
}
console.log(typeof f, f);
```
But this is wrong in sloppy mode. The output changes `number 0` / `undefined undefined` to `function f() {}`.
This PR fixes that.
I used AI to generate the code and then reviewed the code and added tests.
`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>
add `boolean_context` to `minimize_not` so negation folding can happen
in-place when possible, reducing extra AST rebuilds and allocations and
`!0` / `!1` can now collapse directly to `1` / `0` in boolean context
An unused addition can be side-effect-free even when constant folding keeps it. This happens when replacing the expression with its exact constant value would make the output longer.
For example, constant folding correctly keeps `0.1 + 0.2` instead of expanding it to `0.30000000000000004`. But when the result is unused, the whole expression can be removed.
This PR checks the existing side-effect analysis before running the string-addition cleanup. It removes the whole addition when it is safe. Additions that can run code or throw still use the existing cleanup and are preserved when needed.
Tests cover both the removable numeric addition and an addition containing `foo()` that must stay.
Verified with the full `oxc_minifier` test suite (652 passed, 34 ignored), Clippy, formatting, minsize, and allocation checks. Minsize and allocation snapshots are unchanged.
AI-assisted by OpenAI Codex.
Refactors `try_minimize_if` to improve clarity:
- restructures the function around `alternate` presence rather than
`consequent` type
- removes a recursive `Self::try_minimize_if` call — the fallthrough
after branch-flip achieves the same result
- this also removes need for code duplication for inlining expressions
to `||` or `&&`
- `if (!a) {} else b();` → `a && b();`
- `if (!a) b();` → `a || b();`
- `if (a) {} else b();` → `a || b();`
- `if (a) b();` → `a && b();`
- we also do not have to always call for `wrap_to_avoid_ambiguous_else`
and we can do that after we update code (eg, after rotation, and never
when we convert it to expression)
----------------------
#### changes in stack:
- https://github.com/oxc-project/oxc/pull/25267
- https://github.com/oxc-project/oxc/pull/25178👈
- `main`
`@__KEY__` and `#__KEY__` are currently treated as normal comments. Codegen can drop them before a later property-mangling pass. The property may then be renamed while the annotated string keeps its old name.
This classifies the marker as a property-key annotation. Codegen keeps it next to string and no-substitution template literals when annotation comments are enabled, and the property mangler reads that classification instead of scanning source text. Final output still follows the normal comment options, so whitespace-minified or annotation-disabled output removes the marker.
## Example
Input:
```js
const object = { _field: 1 };
const key = /* #__KEY__ */ "_field";
object[key];
```
Before, an intermediate codegen pass could remove the marker:
```js
const object = { _field: 1 };
const key = "_field";
object[key];
```
After, the marker survives for the later property-mangling pass.
Verified with the affected crate suites, `just minsize`, workspace lint, docs, AST generation, and independent review. `just ready` is only blocked locally by existing Node-version and terminal-color snapshot differences.
Implemented with AI assistance (OpenAI Codex). I reviewed and understand the changes and remain responsible for this contribution under the repository AI usage policy.
Oxc can shorten variable names and private class names, but it cannot shorten property names such as `obj._field`. This change adds optional property mangling. It replaces #23772 and gives bundlers a way to use the same property names across several files.
### Feature
- `include` selects names to mangle. `exclude` and `reserved` keep names unchanged.
- The cache can give a property a fixed output name or keep it unchanged.
- The mangler supports member access, object and class keys, destructuring, optional chains, the left side of `in`, and JSX property names.
- `mangle_quoted` also handles strings and template literals used directly as property keys. `/* @__KEY__ */` and `/* #__KEY__ */` can mark one other string or template literal as a property key.
- Short names are assigned in a stable order. More common properties get shorter names. Existing and reserved names are avoided.
- Bundlers can collect names from several programs, create one shared map, and apply it to every program.
- Numeric keys, `__proto__`, `constructor`, `prototype`, TypeScript-only keys, and private names are never changed by this pass.
### Design
Property names are part of JavaScript syntax, not local scope. For this reason, the code lives in `oxc_minifier`. It only reuses the short-name generator from `oxc_mangler`.
For one program, the order is:
```text
collect names → assign names → rewrite once → compress → mangle variables and private names
```
Property names are rewritten before compression because compression can remove information about quotes and comments.
For several programs, a bundler collects names from each program, merges the results, assigns one shared map, and rewrites each program once. Collection can run in parallel. All programs must use the same options.
The shared map keeps property names consistent inside one build. The cache has a different job: it keeps fixed names and reservations across separate minify calls.
### Example
With `include: "^_"` and `mangle_quoted: true`:
```js
obj[`_field`] = 1;
obj._field = 2;
```
becomes:
```js
obj[`e`] = 1;
obj.e = 2;
```
### Limitation
- Property mangling is unsafe when other code still uses the old property name. Public APIs and names shared with external code, host APIs, imported namespace objects, or separate builds must be excluded or reserved.
- Quote handling is per use. Without `mangle_quoted`, `obj._field` may change while `obj["_field"]` does not.
- The mangler cannot find names built at runtime or stored in arbitrary strings. Code that uses `eval`, `Function`, or `with` must reserve any affected names.
- Compression may create a matching property after this pass has finished. For example, `'f' + 'oo_'` may later become `foo_` and will not be mangled.
- TypeScript enum reverse mappings are not supported because the transform creates quoted strings.
- Multi-program support is a caller API. The caller must collect every program that shares mangled properties before assigning names.
The public `oxc-minify` API is stacked in #24741. The downstream multi-chunk integration is shown in rolldown/rolldown#10374.
Supersedes #23772.
AI assistance: OpenAI Codex and Claude were used for research, implementation, review, and testing. The contributor is responsible for reviewing and understanding the changes before submission.
An uninitialized module-level `var` is `undefined` until it is assigned. After Rolldown removed an unreachable setter, Oxc did not keep this fact, so a dead optional call could remain in the output.
This PR records safe uninitialized module `var` bindings as known `undefined`. Existing minifier passes can then remove optional chains and simplify other reads.
The optimization is limited to module scope. It is disabled for resolved writes, redeclarations, and direct `eval`. Script globals and function-local variables are not changed.
Direct aliases keep the same known value without replacing every read with `void 0`. This avoids larger output and lets the existing alias cleanup work normally.
Generic optimizer tests now use function parameters when they need an unknown value. Focused tests cover the new behavior for uninitialized module variables.
## Example
Input:
```js
var slot
function make(value) {
slot?.(value)
return value
}
console.log(make(1))
```
Before:
```js
var slot;
function make(value) {
slot?.(value);
return value;
}
console.log(make(1));
```
After:
```js
function make(value) {
return value;
}
console.log(make(1));
```
Verified with the full `oxc_minifier` test suite (649 passed, 34 ignored), Clippy, formatting, minsize, and allocation checks. Minsize and allocation snapshots are unchanged.
Fixesrolldown/rolldown#10659
AI-assisted by OpenAI Codex.
`f64::powf` implements IEEE 754 `pow`, which returns `1` for a base of
`1` whatever the exponent.
[`Number::exponentiate`](https://tc39.es/ecma262/#sec-numeric-types-number-exponentiate)
instead returns `NaN` when the exponent is `NaN` (step 1), and when the
base has magnitude `1` and the exponent is infinite (steps 9-10).
Constant folding called `powf` directly, so:
```js
1 ** Infinity // spec: NaN oxc folded to: 1
(-1) ** Infinity // spec: NaN oxc folded to: 1
1 ** -Infinity // spec: NaN oxc folded to: 1
1 ** NaN // spec: NaN oxc folded to: 1
```
`1` is truthy and `NaN` is not, so an `if` guarded by such an expression
takes the wrong branch.
The one case the two agree on — an exponent of `0` returning `1`
whatever the base, including `NaN ** 0` and `Infinity ** 0` — is
unchanged, since `powf` already gets it right and the spec checks the
exponent before the base.
`cargo minsize` shows no snapshot change.
Found by the minifier fuzzer (#25594).
---
Written with AI assistance (Claude Code). Reviewed by me, and every
expected value in the added test was checked against Node.js.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Keep the variable declaration when that is initialized with class and keepNames is enabled:
```js
var MyStore = class { static { console.log(this.name) } };
```
fixes https://github.com/oxc-project/oxc/issues/25060
I used AI to generate the code and then reviewed the code and added the tests for `keepNames` disabled cases.
Found by the semantic fuzzer added in #25594 (seed `29024`).
## What
`try_fold_left_child_op` rewrites `(a op b) op right` (`&`, `|`, `^`)
into `x op constant` by folding `right` together with whichever of
`a`/`b` evaluates, discarding both. It never checked the discarded
operands for side effects.
Being constant-evaluable does not imply being side-effect free: `[(y =
9), 1].length` evaluates to `2` while still assigning `y`.
```js
var y = 2, c = 0;
(3 ^ c) ^ [(y = 9), 1].length;
console.log(y); // 9, minified to 2 — the assignment is dropped
```
The minified output is `c ^ 1`: the array literal, and the assignment
inside it, are gone.
## How
Bail out when the discarded right operand — or the left-child operand
folded with it — may have side effects.
The shape check (`left.operator != op`) now runs before
`may_have_side_effects`, which walks the subtree, so the common
non-matching case stays as cheap as before.
Note `fold_static_member_expr` already guards its `.length` fold with
the same check; this restores the symmetry.
## Testing
- New regression test `test_fold_left_child_op_keeps_side_effects`,
covering `^`, `|`, `&` and both operand positions, plus a positive case
asserting the fold still happens when nothing has side effects.
- `cargo test -p oxc_minifier -p oxc_ecmascript`, `cargo clippy
--all-targets`, `cargo fmt --check` all pass.
- `just minsize`: no snapshot change, so no size regression.
## AI disclosure
This change was produced with AI assistance (Claude Code). I have
reviewed and understand every line; the root cause and the minimal
reproduction were verified by hand against Node.js.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
Fix private-name mangling for `get #x()` / `set #x(value)` accessor pairs.
JavaScript treats an accessor pair as one private name, but the mangler previously assigned a candidate per element before collecting into a name-keyed map. The setter overwrote the getter’s mapping, while still consuming an extra candidate.
### Before
```js
class Outer {
get #x() {}
set #x(value) {}
#longName;
nested() {
return class {
#nestedName;
};
}
}
```
```js
class Outer {
get #t() {}
set #t(e) {}
#n;
nested() {
return class {
#r;
};
}
}
```
### After
```js
class Outer {
get #e() {}
set #e(e) {}
#t;
nested() {
return class {
#n;
};
}
}
```
## Implementation
- Deduplicate private names during map construction with `HashMap::entry`.
- Derive nested-class offsets from already-built lexical parent mappings, preserving collision avoidance.
- Pre-size mappings from the number of private elements.
Found by the semantic fuzzer added in #25594 (seed `15979`).
## What
`ToNumber(!x)` was computed from `ToNumber(x)` instead of
`ToBoolean(x)`:
```rust
Expression::UnaryExpression(unary) if unary.operator.is_not() => {
let number = unary.argument.to_number(ctx)?;
Some(if number == 0.0 { 1.0 } else { 0.0 })
}
```
`!x` is `ToBoolean(x)` negated, so the two predicates must agree — and
they don't. `NaN` is falsy but does not number to `0`, while `"0"`, `"
"`, `"0.0"` and `[]` all number to `0` yet are truthy.
## Why it was not caught
`minimize_conditional_expression` rewrites `a ? 1 : 0` into `+!!a`. That
rewrite is correct, but when a parent expression evaluated the freshly
built `+!!a` in the same pass, the broken branch flipped the result:
```js
(NaN ? 1 : 0) || 2 // 2, minified to 0
('0' ? 1 : 0) || 2 // 1, minified to 2
([] ? 1 : 0) || 2 // 1, minified to 2
```
A `!!x` written in the source is folded correctly by `fold_unary_expr`
before any parent observes it, so only pass-generated nodes were
affected. That is why existing tests and the conformance suites pass.
refs https://github.com/oxc-project/oxc/pull/8267
## How
Derive the value from `to_boolean`. This is strictly more conservative:
when `to_boolean` returns `None` the fold is skipped rather than
answered wrongly.
## Testing
- New regression test `test_conditional_to_unary_plus_uses_to_boolean`
covering `NaN`, `"0"`, `" "`, `"0.0"` and `[]`.
- `cargo test -p oxc_minifier -p oxc_ecmascript`, `cargo clippy
--all-targets`, `cargo fmt --check` all pass.
- `just minsize`: no snapshot change, so no size regression.
## AI disclosure
This change was produced with AI assistance (Claude Code). I have
reviewed and understand every line; the root cause and each minimal
reproduction were verified by hand against Node.js.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
### Summary
Fix private-member mangling for class heritage expressions.
The code generator previously activated a class's private-member mapping before emitting that class's decorators and `extends` expression. A private identifier in those expressions is evaluated in the enclosing lexical class scope, not the class currently being declared.
This change:
- reserves each class's mapping ID in AST traversal order;
- activates that mapping only immediately before emitting its class body;
- adds a mangler/codegen regression test covering a nested class with identically named private members.
### Reproduction
```js
class HasOuterBrand {}
class MissingOuterBrand {}
class Outer {
#x;
create(o) {
return class Inner extends (
#x in o ? HasOuterBrand : MissingOuterBrand
) {
#x;
};
}
}
```
`#x` in `Inner`'s heritage expression refers to `Outer.#x`. `Inner.#x` is not in scope until the `Inner` class body.
### Scope model
```text
Outer class body: active private mapping
Outer.#x -> #e
create(o) {
class Inner extends (#x in o ? ... : ...)
│
└─ resolves in Outer scope: #e
Inner class body: active private mapping
Inner.#x -> #t
}
```
### Before
Codegen entered `Inner` before emitting the entire class header:
```rust
p.enter_class();
p.print_decorators(&self.decorators, ctx);
...
heritage.expression.print_expr(...);
...
self.body.print(p, ctx);
```
That made private-identifier lookup prefer `Inner`'s mapping while printing `extends`, producing the wrong brand:
```js
return class t extends (#t in e ? HasOuterBrand : MissingOuterBrand) {
#t;
};
```
The `#t` in the heritage expression is invalid because `Inner.#t` is not yet in scope.
### After
The class ID is reserved first, preserving mapping IDs assigned in traversal order, but the class mapping is pushed only for the body:
```rust
let class_id = p.reserve_class();
p.print_decorators(&self.decorators, ctx);
...
heritage.expression.print_expr(...);
p.enter_class(class_id);
self.body.print(p, ctx);
p.exit_class();
```
```text
Class-ID allocation Active private-name scope
──────────────────── ─────────────────────────────
reserve Outer ID enclosing class / none
reserve nested header IDs enclosing class / none
enter Outer ID Outer body
enter Inner ID Inner body
```
Separating reservation from activation matters: moving allocation until after heritage could shift IDs for nested classes encountered in a heritage expression and break the mangler's precomputed mapping association.
### Regression coverage
The existing minifier mangler integration harness now snapshots this case with private-member mappings enabled. It confirms that the generated heritage expression uses Outer’s mapping (`#e`) while Inner’s field uses its distinct mapping (`#t`):
```js
class Outer {
#e;
create(e) {
return class t extends (#e in e ? HasOuterBrand : MissingOuterBrand) {
#t;
};
}
}
```
Preserve block statements inside labeled statements when they contain an
iteration statement (for/while/do-while).
Previously, these blocks were removed, which caused continue label
syntax errors to not be preserved in the minified output:
```js
label: {
for (let i = 0; i < 10; i++) {
continue label; // SyntaxError: 'label' does not denote an iteration statement
}
}
```
fixes#25215
------------------
this guard is broad as we are not checking if inner loop has labeled
continue, this optimization could be implemented but its better if we
wait for #24155
```js
label: for (let i = 0; i < 10; i++) continue label;
```
Slots were assigned to non-manglable symbols as well. This made the slot re-use non-optimal.
This PR fixes that.
I used AI to find out this bug and to generate the code and then reviewed the code.
Constant evaluation can fold a string addition through a tracked constant even when direct identifier inlining rejects that value as too large. This materializes the large string at every reference and can double chunks containing embedded assets.
This change shares constant materialization eligibility between direct inlining and string-add folding. A fold that would grow the expression through a non-inlineable constant is skipped, while single-read and cheap constants remain foldable. The policy follows the conservative strategy used by esbuild and SWC; permanent source links are included beside the helper.
## Example
Input:
```js
const p = "…1 MB payload…";
export const a = atob(p);
export const b = "y" + p;
```
Before, Rolldown 1.2.3 emitted two copies of the payload, growing the output from about 1 MB to 2 MB. After, `p` remains a binding and the payload is stored once.
Verified with `cargo test -p oxc_minifier`, `just minsize` (no minsize or allocation snapshot changes), `cargo clippy -p oxc_minifier -- -D warnings`, and `just fmt`. Regression tests cover both full compression and DCE.
Fixesrolldown/rolldown#10656.
AI-assisted. The contributor remains responsible for reviewing, understanding, and submitting the changes under Oxc's AI usage policy.
Inlining constant template expressions can join a decimal digit onto a
preceding raw `\0`. The resulting `\00` is a forbidden octal escape
inside template literals, so DCE can emit JavaScript that fails to
parse. This surfaced in rolldown/rolldown#10661.
When a fold would create that boundary, serialize the null as `\x00` and
continue merging the template fragments. This preserves the cooked value
and the optimization, matches Terser and esbuild, and leaves
escaped-backslash cases unchanged.
## Example
Input:
```js
return `${messageId}\0${correlationId}\0${0}`;
```
Before:
```js
return `${messageId}\0${correlationId}\00`;
```
After:
```js
return `${messageId}\0${correlationId}\x000`;
```
Verified with `cargo test -p oxc_minifier` (643 passed, 35 ignored), the
DCE example's `--twice` idempotency check, a runtime equivalence check,
`just minsize` with unchanged size/allocation snapshots, Clippy with
warnings denied, and formatting. `just ready` additionally passed
dependency, typo, generation, formatting, workspace checks, and all
minifier tests; the remaining workspace run stopped on three unrelated
Oxlint ANSI-color snapshots because the environment sets `NO_COLOR=1`,
and a color-enabled rerun was blocked by the filesystem running out of
space.
Refs rolldown/rolldown#10661.
Implemented with AI assistance (OpenAI Codex). The contributor remains
responsible for reviewing, understanding, and submitting the change
under the Oxc AI usage policy.
merge newly added `test_fold_pow` with `test_fold_math_functions_pow`
and enable those tests
splits `test_fold_roots` to `test_fold_math_functions_sqrt` and
`test_fold_math_functions_cbrt` and moves them to same place where other
math tests live
fix bug with termination detection for `try finally`,
when try has termination stmt and finally is missing it and we do not
have catch stmt current code passes it and allows stmt to fold
```js
// base
function f() {
if (a) try { return g(); } finally {}
else i();
}
// main
function f() {
if (a) try { return g(); } finally {}
i();
}
// this branch
function f() {
if (a) try { return g(); } finally {}
else i();
}
```
this issue has been introduced by
https://github.com/oxc-project/oxc/pull/24441 and copied over by
https://github.com/oxc-project/oxc/pull/24950fixes#25214
revert #24813 currently there is no way to validate if stmt can be
removed if its inside a block
correct issue with incorrect removal of `return` stmt not at termination
points
```js
(function() {
label: {
console.log('before return');
return;
}
console.log('unreachable!');
})();
```
ref: https://github.com/rolldown/rolldown/issues/10555
## Summary
- Preserve legal comments whose `@license` marker ends exactly at comment-content EOF.
- Add parser and minifier regression coverage.
## Details
The optimized marker scan excluded the final valid start for the eight-byte `@license` marker. Comments such as `/*x@license*/` were parsed but classified as ordinary comments and could therefore be dropped from legal-comment output or extraction.
The parser hot path now includes that final candidate while retaining unchecked reads, with an explicit remaining-length guard before reading the nine-byte `@preserve` marker.
## Ecosystem compatibility
This makes Oxc recognize terminal markers such as `/*x@license*/` and `//x@license`, matching tested behavior in esbuild 0.28.1 and SWC 1.15.47 when their legal/`"some"` comment retention is enabled.
do not rewrite if else where `alternate` could produce invalid code and
change behavior
```js
function g(a,b){if(a){}else if(b){return()=>typeof f}else function f(){}}
```
https://github.com/oxc-project/oxc/pull/24997/changes#r3659223987
> **<sub><sub></sub></sub>
Preserve Annex B branch scoping when wrapping terminated blocks**
>
> In sloppy scripts, broadening this condition from direct jumps to any
terminated statement lets a block-returning branch trigger the synthetic
wrapper. For `function g(a,b){if(a){}else if(b){return()=>typeof f}else
function f(){}}`, the subsequent `handle_if_statement` pass detaches the
`else` into that wrapper, emitting the equivalent of
`if(!a){if(b)return()=>typeof f;function f(){}}`. The Annex B function
is then instantiated when the block is entered, so `g(false,true)()`
changes from `"undefined"` to `"function"`; avoid flattening
scope-sensitive alternates or otherwise preserve their original branch
scope.
based on changes from #24997
---------
Co-authored-by: Cameron <cameron.clark@hey.com>
The value of a StringLiteral and the cooked value of a template quasi encode lone surrogates with `\u{FFFD}` escapes and mark the node with a `lone_surrogates` flag. ToJsString and constant evaluation returned the encoded text, and consumers materialized it into new string literals without the flag, printing the escape encoding as literal text: `"\ud800" + "y"` folded to a string containing a literal `d800`.
Return `None` for such strings and template quasis so folds keep the original nodes.
```js
// source
console.log("\ud800" + "y"); // node prints: �y
// before (miscompiled)
console.log(`�d800y`); // node prints: �d800y
// after
console.log(`\ud800`+`y`); // node prints: �y
```
The parser cannot produce negative or non-finite NumericLiterals: `-1` parses as unary negation, and codegen prints `Infinity` as `1/0`, which parses as a division. The loop's constant folds re-derive the literal from either spelling and record a change for it, one phantom change per such number on every run over printed output.
Collapse `-` into a negative literal in the Normalize pass — the same treatment `NaN` and `Infinity` identifiers already get — and keep `1/0` and `-1/0` unfolded like the existing `!0` and `void 0` guards: they are the canonical printed spellings of the non-finite numbers.
The guard shifts an inlining decision in lodash.js, making it 20 bytes smaller; all other minsize outputs are unchanged.
Calling `right.to_js_string(ctx)` to get the string of the expression and check whether its string belongs to any `typeof` value to keep compressing `typeof v == [1]` to `!1`.
I may be overthinking this case since this kind of use is quite rare in real-world scenarios. However, I didn't find it hard to implement, so I just got it to work.
RegExp literal `.source` folding used a hard-coded approximation of ES2015 support. It rejected syntax that configured targets support, such as the `u` flag for ES2015, while allowing newer syntax without checking the configured engines.
The same target compatibility mapping already exists in `oxc_ecmascript` for RegExp constructor analysis. Expose one parsed-syntax predicate from that layer and reuse it for `.source` folding. This removes the duplicate policy and the minifier's direct `oxc_regular_expression` dependency. An unspecified target remains conservative, while an explicit supporting target unlocks the fold.
Covered by `test_fold_regex_source` with adjacent unsupported and supported targets for `y`, `u`, `s`, `d`, `v`, named groups, Unicode property escapes, lookbehind, duplicate named groups, and pattern modifiers.
Verified with `cargo test -p oxc_ecmascript --features side_effects`, `cargo test -p oxc_minifier` (631 passed, 42 ignored), `just minsize` (no minsize or allocation snapshot changes), affected-crate clippy with warnings denied, and `just fmt`.
Follow-up to #24712 and [its non-blocking review](https://github.com/oxc-project/oxc/pull/24712#discussion_r3619967432).
Implemented with AI assistance (OpenAI Codex and Claude). The contributor remains responsible for reviewing, understanding, and submitting these changes under the repository AI usage policy.
Integer exponentiation remains uncompressed even when its Number result is an exact safe integer and a shorter literal can represent it. This also prevents the existing `Math.pow` rewrite from completing beyond the exponentiation form.
Fold these cases only when both operands have side-effect-free integer-valued numeric values, the result is within the Number safe-integer range, and the serialized literal is no longer than a conservative lower bound for the original expression. Number exponentiation is implementation-approximated, so fractional operands remain unchanged. BigInt, observable coercions, side effects, unsafe results, and size regressions are rejected.
## Example
Input:
```js
use(2 ** 3);
```
Before:
```js
use(2 ** 3);
```
After:
```js
use(8);
```
The minsize corpus reduces Three.js by 2 raw bytes and 1 byte with gzip, below the reported 0.01 kB snapshot precision. The tracked kitchen-sink fixture records six additional arena node allocations from the new replacement; heap metrics and arena size are unchanged.
Verified with the full `oxc_minifier` test suite, `just minsize`, Clippy with warnings denied, formatting, and `git diff --check`.
Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
- allow removing trailing unlabeled `continue;` in `do...while` block
bodies (same as other loop block bodies).
- allow removing trailing unlabeled `break;` in `do...while` block
bodies **only** when the loop test is side-effect-free and statically
`false`.
- enable and update `do...while` exit-point tests.
#### changes in stack
1. https://github.com/oxc-project/oxc/pull/24920
2. https://github.com/oxc-project/oxc/pull/24921 < This PR
Finite division and remainder expressions remain uncompressed even when their Number result is an exact safe integer and a shorter literal can represent it. For example, `360 / 360` and `3 % 2` remain as binary expressions.
Fold these cases only when both operands have side-effect-free numeric values, the result is within the Number safe-integer range, and the serialized literal is no longer than a conservative lower bound for the original expression. The existing `NaN` and `Infinity` paths remain unchanged. This preserves `-0` while rejecting BigInt, fractional or unsafe results, observable coercions, and size regressions.
## Example
Input:
```js
use(360 / 360);
```
Before:
```js
use(360 / 360);
```
After:
```js
use(1);
```
The minsize corpus has no change at its reported 0.01 kB precision. The tracked kitchen-sink fixture records four additional arena node allocations from the new replacements; heap metrics and arena size are unchanged.
Verified with the full `oxc_minifier` test suite, `just minsize`, Clippy with warnings denied, and formatting.
Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
Extract duplicate checks for removable trailing `continue;` and
`return;` statements into a single `can_remove_termination_statement()`
helper.
The helper now includes `while` loop bodies alongside
`for`/`for-in`/`for-of` for `continue` statement but under normal flow
`while` should not be needed as we always convert it to `for(;a;)`
#### changes in stack
1. https://github.com/oxc-project/oxc/pull/24920 < This PR
2. https://github.com/oxc-project/oxc/pull/24921
Boolean-literal ternaries whose tests already require parentheses—such as assignments and nested conditionals—are currently left uncompressed. The size guard checks the rewritten logical operand, but does not account for parentheses that were already required around the original ternary test.
Compare the parenthesis cost before and after the rewrite. Equality comparisons are negated by inverting their operator; other binary and logical tests remain rejected. The existing branch, `??`, and `||`-under-`&&` guards are unchanged.
## Example
**Input**
```js
use((flag = a === b) ? false : value);
```
**Minified output**
```diff
-use((flag = a === b) ? !1 : value);
+use(!(flag = a === b) && value);
```
The rewritten output is one byte shorter. The current minsize corpus contains no matching cases, so its size and allocation snapshots are unchanged.
Stacked on #24943.
Verified with the full `oxc_minifier` test suite, `just minsize`, Clippy with warnings denied, and formatting.
Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
Boolean-literal ternaries with a sequence in the non-literal branch are left uncompressed because the logical-operand size guard counts sequence parentheses as newly added. A sequence already requires parentheses in a conditional branch, so this misses shorter `&&` / `||` forms.
Treat sequence branches as having no additional parenthesis cost while keeping the existing guard for assignments, nested conditionals, arrows, `yield`, and incompatible logical operators. The change is confined to full expression compression and does not affect statement or tree-shaking passes.
## Example
```js
// input
use(flag ? false : (touch(), true));
// before
use(flag?!1:(touch(),!0));
// after
use(!flag&&(touch(),!0));
```
The minsize corpus improves GZIP by 0.01 kB each for Victory, ECharts, Ant Design, and TypeScript, with no regressions or iteration changes. The additional folds add 3–42 arena allocations per tracked fixture (1,152 arena bytes at most) and no system allocations.
Verified with `cargo test -p oxc_minifier`, `just minsize`, Clippy with warnings denied, formatting checks, and `just ready`.
Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
Added a new IsTerminated trait and wired it into handle_if_statement in
minimize-exit-points.
before this, we only treated direct jump statements as terminating. Now
we also handle nested terminated branches, so cases like nested if/else
termination optimize correctly.
for now this is only used in this one optimization path, but we should
reuse IsTerminated in more places (e.g. switch-case and other
exit-point/termination optimizations).