Files
oxc-project__oxc/crates/oxc_minifier/docs/CLAUDE.md
Dunqing 09176f8829 refactor(minifier): route all peephole mutations through typed helpers (#23196)
> Developed with AI assistance (Claude Code); reviewed, tested, and benchmarked by the contributor.

## Summary

Replaces the ~190 manual `ctx.state.changed = true` writes scattered across `crates/oxc_minifier/src/peephole/` with typed mutation helpers on `MinifierTraverseCtx` (`replace_expression`, `replace_statement`, `replace_assignment_target_property`, `replace_property_key`, `replace_for_statement_left`, `drop_expression`, `drop_statement`, `drop_class_element`, `notice_change`).

The mutation signal itself becomes a private `mutated: bool` on `MinifierState`, readable only via `take_mutated()` (read-and-reset in a single call) — the compiler now enforces that the fixed-point loop's signal can only be set by the helpers and only consumed by the loop driver.

"Forgot to mark the loop dirty" — the bug pattern behind #22722 and #22552 — becomes structurally hard to write. The migration also carries the latent silent-mutation fixes surfaced by routing every mutation through a helper (missed `*slot = …` writes in `remove_unused_expression`, `convert_to_dotted_properties`, `minimize_statements`, and field-write bypass sites), plus idempotency gates in `try_fold_if` so the typed helpers don't block fixed-point convergence.

This is stage 1 of 2 re-cutting the approach prototyped in #22736; the incremental scoping refresh stacks on top.

## Verification

- `cargo test -p oxc_minifier` — 509 pass
- `just minsize` — **bit-identical output** across the entire size-test corpus (this PR is a pure refactor)
- `cargo coverage -- minifier` — conformance unchanged
- clippy (all features/targets) clean
- `allocs_minifier.snap`: small arena-only deltas (+0.02–0.08%) from helper call-site construction patterns; sys allocs unchanged
2026-06-15 15:06:58 +00:00

4.7 KiB

Oxc Minifier - AI Assistant Guide

Mission

Build the best JavaScript minifier: smallest output, 100% correct, reasonably fast.

Quick Reference

Architecture

  • Fixed-point iteration in PeepholeOptimizations::run_in_loop
  • Arena allocation via oxc_allocator
  • 17+ optimizations in src/peephole/

Key Files

src/
├── lib.rs           # Entry point
├── compressor.rs    # Pipeline orchestration
├── ctx.rs           # Shared utilities
├── state.rs         # Optimization state
├── peephole/
│   ├── mod.rs       # Optimization dispatch
│   └── *.rs         # Individual optimizations

Adding New Optimizations

  1. Research: Study implementation in other minifiers (Terser, Closure, esbuild)
  2. Design: Determine transformation rules and edge cases
  3. Implement: Add to src/peephole/ following existing patterns
  4. Hook up: Add to traversal in peephole/mod.rs
  5. Test: Add comprehensive tests
  6. Measure: Check size impact with just minsize
  7. Validate: Run conformance with cargo coverage

Testing Commands

just test           # Unit tests
just minsize        # Size benchmarks (PRIMARY METRIC)
cargo coverage      # Conformance tests
just e2e           # End-to-end tests
just ready         # Run before committing

Common Patterns

Optimization Structure

pub fn optimize_something(expr: &mut Expression, ctx: &mut Ctx) {
    // Check if optimization applies
    if !can_optimize(expr, ctx) {
        return;
    }

    // Apply transformation
    let new_expr = create_optimized_form(ctx);
    ctx.replace_expression(expr, new_expr);
}

Always route slot replacements through the typed ctx.replace_* helpers — they are the only way to signal a mutation: the mutation flag is private to MinifierState (compiler-enforced), so a bare *slot = … write cannot mark the pass as changed and will stall the fixed-point loop. For in-place edits that don't replace a slot (operand swaps, field flips, element removals), call ctx.notice_change(); if a raw slot write is genuinely required (e.g. an enum slot with no typed helper), pair it with ctx.notice_change() and a comment explaining why.

Traversal Hook

// In peephole/mod.rs
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
    let ctx = &mut Ctx::new(ctx);
    match expr {
        Expression::BinaryExpression(_) => {
            Self::optimize_binary(expr, ctx);
        }
        // ... other cases
    }
}

Size Optimization Tips

  • Always prefer shorter syntax (!0 over true)
  • Combine related operations
  • Remove all redundancy
  • Fold constants aggressively
  • Eliminate dead code completely
  • Use comma operators for sequences
  • Prefer operators over keywords

Performance Tips

  • Minimize AST traversals
  • Use arena allocation (ctx.ast)
  • Batch related transformations
  • Early exit when possible
  • Check side effects early

Common Pitfalls to Avoid

  1. Side Effects: Always check may_have_side_effects()
  2. Scope Issues: Use scoping() for variable lookups
  3. TDZ: Be careful with let/const transformations
  4. Evaluation Order: Preserve execution order
  5. Edge Cases: Test with NaN, Infinity, -0, etc.

Debugging

# Debug conformance failures
cargo coverage -- --debug

# Test single file
cargo run -p oxc_minifier --example minifier test.js

# Test idempotency
cargo run -p oxc_minifier --example minifier test.js --twice

Key Utilities in ctx.rs

  • eval_binary() - Evaluate binary expressions
  • value_to_expr() - Convert constant to AST node
  • is_expression_undefined() - Check for undefined
  • expr_eq() - Compare expressions
  • scoping() - Access scope information
  • ast - AST builder

Development Flow

  1. Find optimization opportunity

    • Check minsize benchmarks
    • Study other minifiers
    • Look for patterns in real code
  2. Implement safely

    • Follow existing patterns
    • Handle all edge cases
    • Preserve semantics
  3. Test thoroughly

    cargo test -p oxc_minifier
    cargo coverage
    just minsize
    
  4. Document clearly

    • Add comments explaining the optimization
    • Document any assumptions
    • Add examples in tests

AST Manipulation

// Use TakeIn for moving nodes
use oxc_allocator::TakeIn;
let moved = node.take_in(ctx.ast);

// Use arena for allocations
ctx.ast.expression_boolean_literal(span, true)

// Check node types
if let Expression::BinaryExpression(e) = expr {
    // work with binary expression
}

Remember

  • Size is the primary goal
  • Correctness is non-negotiable
  • Document assumptions clearly
  • Test with real-world code
  • Learn from other minifiers