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.
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>
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;
```
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.
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
- Groups `Class::super_class` and `Class::super_type_arguments` into `Class::heritage`.
- Introduces `ClassHeritage` containing the superclass expression and its optional TypeScript type arguments.
- Makes superclass type arguments without a superclass expression unrepresentable.
## Breaking Change
The Rust AST changes from:
```rust
pub struct Class<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub r#type: ClassType,
pub decorators: Vec<'a, Decorator<'a>>,
pub id: Option<BindingIdentifier<'a>>,
#[ts]
pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
pub super_class: Option<Expression<'a>>,
#[ts]
pub super_type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
#[ts]
pub implements: Vec<'a, TSClassImplements<'a>>,
pub body: Box<'a, ClassBody<'a>>,
#[ts]
pub r#abstract: bool,
#[ts]
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
```
to:
```rust
pub struct Class<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub r#type: ClassType,
pub decorators: Vec<'a, Decorator<'a>>,
pub id: Option<BindingIdentifier<'a>>,
#[ts]
pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
pub heritage: Option<ClassHeritage<'a>>,
#[ts]
pub implements: Vec<'a, TSClassImplements<'a>>,
pub body: Box<'a, ClassBody<'a>>,
#[ts]
pub r#abstract: bool,
#[ts]
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
pub struct ClassHeritage<'a> {
pub expression: Expression<'a>,
#[ts]
pub type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
}
```
## Why?
ECMAScript defines the complete `extends` clause as `ClassHeritage`:
```text
ClassHeritage : extends LeftHandSideExpression
```
TypeScript additionally permits type arguments on the heritage expression:
```ts
class Derived extends Base<T> {}
```
Previously, the AST stored the expression and type arguments independently:
```text
Class {
super_class: Some(Base),
super_type_arguments: Some(<T>),
...
}
```
This allowed an invalid state to be constructed:
```text
Class {
super_class: None,
super_type_arguments: Some(<T>),
...
}
```
Superclass type arguments cannot exist without an `extends` expression.
The parser already treated these fields as a pair. It parsed a class heritage entry and then split its expression and type arguments into two independent fields. Builders and transforms therefore had to preserve their relationship manually.
Grouping them into `ClassHeritage` makes the syntactic dependency explicit and prevents consumers from observing or constructing contradictory class heritage.
## Class heritage forms
A class without an `extends` clause has no heritage:
```js
class Base {}
```
```text
Class {
heritage: None,
...
}
```
A class extending an expression has heritage without type arguments:
```js
class Derived extends Base {}
```
```text
Class {
heritage: Some(ClassHeritage {
expression: Base,
type_arguments: None,
}),
...
}
```
TypeScript type arguments belong to the same heritage structure:
```ts
class Derived extends Base<T> {}
```
```text
Class {
heritage: Some(ClassHeritage {
expression: Base,
type_arguments: Some(<T>),
}),
...
}
```
The heritage expression may be any valid class heritage expression:
```js
class Derived extends mixin(Base) {}
class NullDerived extends null {}
```
## How to migrate
Code that previously accessed the superclass expression directly:
```rust
if let Some(super_class) = &class.super_class {
// ...
}
```
can use the compatibility accessor:
```rust
if let Some(super_class) = class.super_class() {
// ...
}
```
Code that needs both parts should access the grouped heritage:
```rust
if let Some(heritage) = &class.heritage {
let expression = &heritage.expression;
let type_arguments = heritage.type_arguments.as_deref();
}
```
Mutating consumers should update the nested fields:
```rust
if let Some(heritage) = &mut class.heritage {
heritage.type_arguments = None;
}
```
Common migrations:
| Previous representation | New representation |
| --- | --- |
| `class.super_class` | `class.heritage.as_ref().map(\|h\| &h.expression)` or `class.super_class()` |
| `class.super_type_arguments` | `class.heritage.as_ref()?.type_arguments` or `class.super_type_arguments()` |
| `class.super_class.is_some()` | `class.heritage.is_some()` |
| Mutate `class.super_class` | Mutate `class.heritage.expression` |
| Clear `class.super_type_arguments` | Clear `class.heritage.type_arguments` |
| Pass superclass and type arguments separately to a `Class` builder | Construct and pass `ClassHeritage` |
| Clone both fields independently | Clone `class.heritage` |
```ts
superClass: Expression | null;
superTypeArguments?: TSTypeParameterInstantiation | null;
```
No `heritage` property or `ClassHeritage` node is added to the ESTree output.
fixes https://github.com/oxc-project/backlog/issues/216
## Summary
Narrow `TSInterfaceHeritage` from a general JavaScript `Expression` to `TSTypeName`.
```rust
pub struct TSInterfaceHeritage<'a> {
pub type_name: TSTypeName<'a>,
pub type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
}
```
## Breaking Change
The Rust AST field changes from:
```rust
expression: Expression<'a>
```
to:
```rust
type_name: TSTypeName<'a>
```
## Why?
The current AST can represent shapes such as:
```ts
interface A extends foo() {}
interface A extends A + B {}
interface A extends new Foo() {}
interface A extends true {}
```
Which is an overly wide type since all of the above variants are invalid.
This PR tightens up the AST, making these invalid variants irrepresentable.
## How to migrate?
This should be a trivial migration:
1. instead of accessing `expression` on `TSInterfaceHeritage`, now access `type_name`
2. change any pattern matching on the `type_name` field to use `TypeName` (this is a smaller subset so should allow deleting code!)
closes https://github.com/oxc-project/backlog/issues/215
## Summary
- Removes `VariableDeclarator::kind`.
- Makes `VariableDeclaration::kind` the single source of truth for every declarator in its declaration list.
- Makes contradictory parent and child declaration kinds unrepresentable.
## Breaking Change
The Rust AST changes from:
```rust
pub struct VariableDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub kind: VariableDeclarationKind,
pub declarations: Vec<'a, VariableDeclarator<'a>>,
#[ts]
pub declare: bool,
}
pub struct VariableDeclarator<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
#[estree(skip)]
pub kind: VariableDeclarationKind,
#[estree(via = VariableDeclaratorId)]
pub id: BindingPattern<'a>,
#[ts]
#[estree(skip)]
pub type_annotation: Option<Box<'a, TSTypeAnnotation<'a>>>,
pub init: Option<Expression<'a>>,
#[ts]
pub definite: bool,
}
```
to:
```rust
pub struct VariableDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub kind: VariableDeclarationKind,
pub declarations: Vec<'a, VariableDeclarator<'a>>,
#[ts]
pub declare: bool,
}
pub struct VariableDeclarator<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
#[estree(via = VariableDeclaratorId)]
pub id: BindingPattern<'a>,
#[ts]
#[estree(skip)]
pub type_annotation: Option<Box<'a, TSTypeAnnotation<'a>>>,
pub init: Option<Expression<'a>>,
#[ts]
pub definite: bool,
}
```
## Why?
A variable declaration has one declaration kind shared by every declarator in its declaration list:
```js
const first = 1, second = 2;
```
Previously, the AST stored that kind on both the parent declaration and every child declarator:
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { kind: Const, ... },
VariableDeclarator { kind: Const, ... },
],
}
```
This allowed contradictory ASTs to be constructed:
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { kind: Var, ... },
],
}
```
Different consumers read different copies:
- Code generation used `VariableDeclaration::kind`.
- Semantic binding used `VariableDeclarator::kind`.
- Lint rules, transforms, and minifier passes used a mixture of both.
As a result, one AST could be emitted as `const` while its binding was treated as function-scoped `var`.
Transforms also had to keep the copies synchronized manually. For example, const-to-let normalization updated the parent and then looped through every declarator to update each child. Explicit resource management performed similar paired mutations for `using` declarations.
Keeping the kind only on `VariableDeclaration` removes this synchronization requirement and prevents consumers from observing conflicting declaration semantics.
## Declaration forms
The declaration kind now belongs exclusively to the node that owns the declaration list:
```js
let value;
```
```text
VariableDeclaration {
kind: Let,
declarations: [
VariableDeclarator { id: value, ... },
],
}
```
Multiple declarators share the same parent kind:
```js
const first = 1, second = 2;
```
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { id: first, ... },
VariableDeclarator { id: second, ... },
],
}
```
## How to migrate
Code that previously read the kind from a declarator:
```rust
if declarator.kind.is_const() {
// ...
}
```
should now read it from the containing declaration:
```rust
if declaration.kind.is_const() {
// ...
}
```
Consumers operating through an AST node store should retrieve the parent `VariableDeclaration`:
```rust
let AstKind::VariableDeclaration(declaration) =
nodes.parent_kind(declarator.node_id())
else {
unreachable!();
};
let kind = declaration.kind;
```
Common migrations:
| Previous representation | New representation |
| --- | --- |
| `declarator.kind` | Containing `declaration.kind` |
| Match `VariableDeclarator::kind` during semantic binding | Read the parent `VariableDeclaration` from ancestry |
| Pass `kind` to `VariableDeclarator::new` | Remove the `kind` argument |
| Update parent and every child kind | Update only `VariableDeclaration::kind` |
| Recover kind after detaching a declarator | Capture or pass the parent kind before detaching it |
| Inspect a symbol’s declarator kind in lint rules | Resolve its parent declaration through the node store |
When constructing declarators, use the generated kindless `VariableDeclarator` builders and place them inside a `VariableDeclaration` carrying the required kind.
fixes https://github.com/oxc-project/backlog/issues/237
Pure refactor.
Simplify methods of `KeepVar` trait, removing generic params. Also convert the `&impl GetAstBuilder<'a>` to an `&impl AstBuild<'a>` before calling other methods, to reduce polymorphism (affects compilation time, not run time).
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 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.
## Summary
- generate `ChildScopeCollector` for both `oxc_traverse` and `oxc_minifier` from the same AST tools generator
- synchronize the minifier collector with the current AST schema
- prevent the two generated collector implementations from drifting
## Summary
- Introduces `ArrowFunctionBody` to represent concise expression and block bodies explicitly.
- Removes the `ArrowFunctionExpression::expression` boolean.
- Makes invalid arrow body combinations unrepresentable.
## Breaking Change
The Rust AST changes from:
```rust
pub struct ArrowFunctionExpression<'a> {
pub expression: bool,
pub body: Box<'a, FunctionBody<'a>>,
}
```
to:
```rust
pub enum ArrowFunctionBody<'a> {
FunctionBody(Box<'a, FunctionBody<'a>>) = 64,
INHERIT(Expression<'a>),
}
pub struct ArrowFunctionExpression<'a> {
pub body: ArrowFunctionBody<'a>,
}
```
Concise arrow bodies are now stored directly as expressions instead of synthetic `FunctionBody` and `ExpressionStatement` nodes. Block bodies continue to use `FunctionBody`.
## Why?
The previous representation required consumers to maintain this implicit invariant:
```text
expression == true
→ no directives
→ exactly one statement
→ that statement is an ExpressionStatement
```
Invalid combinations could cause panics, skipped work, or fallback behavior. The enum encodes the grammar directly and makes those invalid states unrepresentable.
Concise expressions also now have `ArrowFunctionExpression` as their direct traversal ancestor, without synthetic `FunctionBody` or `ExpressionStatement` nodes.
## How to migrate
Use the arrow body accessors or match on `ArrowFunctionBody` rather than inspecting `expression` and synthetic statements.
Common field migrations:
| Previous representation | New representation |
| --- | --- |
| `arrow.expression` | `arrow.is_expression()` |
| `arrow.get_expression()` | `arrow.get_expression()` |
| `&arrow.body.statements` | `arrow.get_function_body().map(\|body\| &body.statements)` |
| `&mut arrow.body.statements` | `arrow.get_function_body_mut().map(\|body\| &mut body.statements)` |
| Synthetic expression statement in `arrow.body` | Direct `ArrowFunctionBody` expression variant |
| `arrow.body = function_body` | `arrow.body = ArrowFunctionBody::FunctionBody(function_body)` |
| Expression-to-block conversion | Create a `FunctionBody` containing a return statement |
| Block-to-expression conversion | Assign the expression directly to `arrow.body` |
| Builder `(expression, body)` arguments | Pass a single `ArrowFunctionBody` |
closes https://github.com/oxc-project/backlog/issues/211
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.
AST builder methods accepted an `IntoIn<ArenaBox<T>>` for all params where an `ArenaBox<T>` is required. You could pass a plain `T`, and the builder method would automatically allocate space for it, and copy it into the arena.
This was originally intended as a convenience, but in fact it's a footgun - it encourages allocating late, holding types on the stack during other work, which results in register pressure, and churn of copying to/from the stack.
Remove the automatic conversion. All places where we were using auto-allocation are already fixed in previous PRs #25034, #25035, #25036, #25037.
Additional benefits of this change:
1. AST builder methods become less polymorphic which should help with compile times.
2. We can get rid of the `NONE` type, which was a hacky workaround, necessary for passing as params where an `IntoIn<Option<ArenaBox<T>>>` is required. Now these all become plain `None`.
Most of the diff in this PR is just replacing `NONE` with `None` all over the place.
Note: AST builder methods still define params which are `ArenaVec`s as `IntoIn<ArenaVec<T>>`, as this does add genuine value - it allows passing `[]` instead of the much more verbose `ArenaVec::new_in(self)`.
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).
optimizes labeled stmts with continue and break statements
```ts
// 0. base
function f() { g: if (a()) { return; } else { return; } return; }
// 1. current
function f() { g: if (a()) return; else return;}
// 1. current
function f() { g: if (a()) return; return;}
// 3. new
function f() { g: a(); }
```
this PR includes test changes from #24687
this change should have minimal impact on actual code as alot of other
changes are needed before we see it being used
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Constant evaluation can replace a short numeric expression with a longer exact floating-point literal, increasing minified output.
Using the serializer shared by #24876 and optimized by #24889, the minifier compares the folded literal with a conservative lower bound for the original numeric expression before accepting numeric addition, subtraction, and multiplication folds. Parentheses are omitted from the estimate, so an accepted fold cannot make output longer. Measuring the original tree instead of evaluated operand values also preserves profitable outer folds such as `.8` without regressing nested expressions such as `0+(.1+.05)`.
## Example
Input:
```js
console.log(0.1 + 0.05);
console.log(0.1 + 0.2 + 0.1 + 0.4);
console.log(0.1 * 1 + 0.05);
console.log(0.1 * 1 + 0.7);
console.log(0.3 - 0.1);
```
Before:
```js
console.log(.15000000000000002),console.log(.7999999999999999),console.log(.15000000000000002),console.log(.8),console.log(.19999999999999998);
```
After:
```js
console.log(.1+.05),console.log(.7+.1),console.log(.1+.05),console.log(.8),console.log(.3-.1);
```
Verified with the full `oxc_minifier` test suite, minsize and allocation snapshots, Clippy, and formatting.
Closes#24863.
AI assistance: Codex was used to implement and verify this change.
Integer-valued comparisons with zero remain verbose in value contexts, even when the minifier can prove that the left-hand side is an `int32` or `uint32`. Applying the same rewrite to every Number-valued expression would be incorrect for `NaN`, while bitwise expressions with unknown operands may still evaluate as BigInt.
Reuse the existing `IsInt32OrUint32` proof to lower equality with zero to logical negation and inequality with zero to double negation. The optimization remains inside binary-expression compression and does not change statement transforms or DCE.
```js
// before
f((a & 1) != 0);
f((a ^ 2) === 0);
// after
f(!!(a & 1));
f(!(a ^ 2));
```
Verified with `just ready`, `just minsize`, and `just allocs`. On the latest `main`, the minsize snapshot improves Three.js raw output from 642.46 kB to 642.45 kB and TypeScript GZIP output from 852.94 kB to 852.81 kB; allocations and compression-pass iterations are unchanged.
AI disclosure: Codex was used to research, implement, and verify this change.
#24784 and #24785 left function-body traversal state as a three-element tuple. The two optimizations share the same stack lifecycle, but numeric field access and tuple destructuring obscure which invariant each call site reads or mutates.
Replace the tuple with a named `BodyFrame` containing the body scope, hoisted-variable inlining safety, and derived-constructor `this` initialization offset. The root reset and function-body push/pop behavior remain unchanged.
Verified with `cargo test -p oxc_minifier`, `just minsize` (including allocation tracking), `cargo clippy -p oxc_minifier -- -D warnings`, and `just fmt`. Minsize and allocation snapshots are unchanged.
Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting these changes under the repository AI usage policy.
> **PR stack (2 of 2)**
> 1. #24784
> 2. **#24785 (this PR)**
>
> Review #24784 first. This PR is based on its branch, so GitHub shows only the nested-class fix.
Computed keys of a class nested before `super()` use the enclosing constructor's lexical `this`. The derived-constructor check found that outer constructor scope but paired it with the nearest class body. When the nested class did not extend another class, conditional assignment merging could therefore treat pre-`super()` `this` as initialized and move its access ahead of side effects.
The check now locates the class that owns the constructor method. Computed keys and decorators continue through nested classes to the outer lexical `this`, while method bodies and field initializers keep their own initialized `this` bindings.
## Example
Input:
```js
class Outer extends P {
constructor() {
class Inner {
[f() ? this.k = 1 : this.k = 2]() {}
}
super();
}
}
```
Before:
```js
class Outer extends P {
constructor() {
class Inner {
[this.k = f() ? 1 : 2]() {}
}
super();
}
}
```
After:
```js
class Outer extends P {
constructor() {
class Inner {
[f() ? this.k = 1 : this.k = 2]() {}
}
super();
}
}
```
Verified with the full `oxc_minifier` suite (634 passed, 42 ignored), Clippy with warnings denied, formatting, `just minsize`, and allocation regeneration. Minsize and allocation snapshots are unchanged.
Follow-up to #24784.
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.
> **PR stack (1 of 2)**
> 1. **#24784 (this PR)**
> 2. #24785
>
> Review and merge in this order. #24785 is based on this branch.
Nested derived-constructor parameter defaults can temporarily consult an enclosing function body's `super()` state because parameters are traversed before the inner function body frame is pushed. If the enclosing derived constructor has already called `super()`, conditional assignment merging can treat the inner constructor's uninitialized `this` as safe and move its access before the condition, changing side effects and the thrown error.
Resolve the derived constructor scope whose lexical `this` is used, then find its matching body frame before consulting `super()` source offsets. A missing owner frame identifies constructor parameter evaluation, while body frames belonging to nested arrows continue to share initialization of the same lexical `this`. The span-based ordering and its existing optimization coverage are otherwise unchanged.
## Example
Input:
```js
class Outer extends P {
constructor() {
super();
class Inner extends Q {
constructor(a = f() ? this.x = 1 : this.x = 2) {
super();
}
}
new Inner();
}
}
```
Before:
```js
class Outer extends P{constructor(){super();class Inner extends Q{constructor(a=this.x=f()?1:2){super()}}new Inner}}
```
After:
```js
class Outer extends P{constructor(){super();class Inner extends Q{constructor(a=f()?this.x=1:this.x=2){super()}}new Inner}}
```
Verified with the full `oxc_minifier` suite (633 passed, 42 ignored), Clippy with warnings denied, formatting, `just minsize`, and allocation regeneration. Minsize and allocation snapshots are unchanged.
Follow-up to #24755.
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.
Pure-function summaries currently treat functions with identifier parameters as side-effect-free without checking parameter initializers. This lets dead-code elimination remove calls that must evaluate a default value.
For example:
```js
var foo = (a = side_effect()) => {};
foo();
```
Before this change, the call is removed entirely. After this change, it is preserved so `side_effect()` runs.
This skips the summary when an ordinary parameter has a default initializer and adds regression coverage for both arrow functions and function declarations.
Verified with `cargo test -p oxc_minifier`, `just minsize`, `cargo clippy -p oxc_minifier -- -D warnings`, and `just fmt`. No minsize or allocation snapshot changed.
Implemented with AI assistance (OpenAI Codex and Claude). Claude identified the bug during review; Codex implemented and verified the fix. The contributor remains responsible for reviewing and understanding the change.
### Summary
The minifier folded `a ? 1 : -0` to `+a` (and `a ? -0 : 1` to `+!a`),
turning a `-0` branch into `+0` and changing the produced value.
### Problem
The fold matched the `-0` operand against the `0.0` literal pattern
(`-0.0 == 0.0` in Rust), so `a ? 1 : -0` became `+a`. When the test
selects the `-0` branch, `+a` / `+!a` yields `+0`. `-0` and `+0` are
equal under `===`, but they are distinct values: `Object.is(-0, +0) ===
false`, `1 / -0 === -Infinity` vs `1 / +0 === +Infinity`, `Math.atan2(0,
-0) === Math.PI` vs `0`. Code relying on SameValue (e.g. React's
`Object.is`-based `useState`/`useMemo` bailout) or on the sign of zero
behaves differently.
### Fix
Bind the zero operand with `@` and guard it with `!is_sign_negative()`,
so only a `+0` branch is folded. Pure `+0` cases (`a ? 1 : 0` → `+!!a`,
`a ? 0 : 1` → `+!a`) are unchanged.
### Testing
- Added cases to `test_minimize_conditional_numeric`: `a ? 1 : -0` and
`a ? -0 : 1` are left unfolded; `!y ? 1 : -0` may still be negated and
branch-swapped to `y ? -0 : 1`, but `-0` is preserved.
- `cargo test -p oxc_minifier` (536 passed) and `cargo lint -- -D
warnings` pass.
Conditional assignment merging can move a `this` member target ahead of a `super()` condition. In a derived constructor, that rewrites valid source into an access of uninitialized `this`, causing a `ReferenceError`.
Keep reorder-sensitive `this` targets guarded until the first unconditional top-level `super()` call in a derived constructor. This preserves the optimization after `super()` has initialized `this`, including the real-world Terser fixture that caused the earlier size increase.
## Example
Input:
```js
class B extends A {
constructor() {
super() ? (this.b = 0) : (this.b = 1);
}
}
```
Before:
```js
class B extends A{constructor(){this.b=+!super()}}
```
After:
```js
class B extends A{constructor(){super()?this.b=0:this.b=1}}
```
Verified with the full `oxc_minifier` suite (625 passed, 42 ignored), Clippy with warnings denied, formatting, `just minsize`, allocation regeneration, and byte-for-byte comparison of all 12 minsize outputs against `main`. Minsize and allocation snapshots are unchanged.
Addresses the wrong-code issue raised in https://github.com/oxc-project/oxc/pull/24706#discussion_r3614230970.
Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.