I started reviewing `minimize_conditional_expression` and looks like
there is alot of wasted effort there, esp, when we are re-creating
`ConditionalExpression` recursively by calling `minimize_conditional`,
for now i decided to only optimize `!==` and `!`, but folding of
`SequenceExpression` and creation of all new
`Expression::new_conditional_expression` could be removed as we should
just mutate current expression
`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.
Statement transforms still use `take_in` after popping or consuming their parent nodes. Removing those dummies is less mechanical because references from discarded branches and pass mutations must remain accounted for.
Move the owned statement fields directly, centralize the shared return/throw conditional rewrite, and use explicit dropped-subtree or mutation reporting at each ownership boundary. The adjacent-`if` rewrite also uses the closure-based replacement API introduced at the bottom of this stack.
Relative to its parent, the tracked allocation fixtures record 2,039 fewer arena allocations and 31,552 fewer arena bytes. Across the complete stack, the improvement is 25,252 arena allocations and 404,352 arena bytes. Minsize output is unchanged.
Validated with `cargo test -p oxc_minifier`, `just minsize`, `cargo clippy -p oxc_minifier -- -D warnings`, `just fmt`, and `git diff --check`.
AI assistance was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
Several peephole transforms call `take_in` after removing or consuming the parent AST node. At that point there is no borrowed slot to preserve, so allocating a dummy replacement is unnecessary.
Move those already-owned child nodes directly in the straightforward expression and declaration cases. This second branch stays intentionally mechanical and leaves the statement/reference-bookkeeping cases to the final PR in the stack.
Relative to its parent, the tracked allocation fixtures record 22,957 fewer arena allocations and 370,544 fewer arena bytes. Minsize output is unchanged.
Validated with `cargo test -p oxc_minifier`, `just minsize`, `cargo clippy -p oxc_minifier -- -D warnings`, `just fmt`, and `git diff --check`.
AI assistance was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
`replace_expression` currently makes callers extract the old value before constructing its replacement, leaving an arena-backed dummy node behind.
Add a closure-based replacement path that owns the old expression and records the AST mutation. Its dropped-subtree contract remains explicit: callers report discarded subtrees, while values moved into the replacement stay live. Convert the logical-not unwrapping sites whose discarded wrappers contain no references.
This is the foundation of a three-PR Graphite stack. The tracked allocation fixtures record 256 fewer arena allocations and 2,256 fewer arena bytes. Minsize output is unchanged.
Validated with `cargo test -p oxc_minifier`, `just minsize`, `cargo clippy -p oxc_minifier -- -D warnings`, `just fmt`, and `git diff --check`.
AI assistance was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
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.
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.