mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
09176f8829
> 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
4.7 KiB
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
- Research: Study implementation in other minifiers (Terser, Closure, esbuild)
- Design: Determine transformation rules and edge cases
- Implement: Add to
src/peephole/following existing patterns - Hook up: Add to traversal in
peephole/mod.rs - Test: Add comprehensive tests
- Measure: Check size impact with
just minsize - 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 (
!0overtrue) - 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
- Side Effects: Always check
may_have_side_effects() - Scope Issues: Use
scoping()for variable lookups - TDZ: Be careful with
let/consttransformations - Evaluation Order: Preserve execution order
- 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 expressionsvalue_to_expr()- Convert constant to AST nodeis_expression_undefined()- Check for undefinedexpr_eq()- Compare expressionsscoping()- Access scope informationast- AST builder
Development Flow
-
Find optimization opportunity
- Check minsize benchmarks
- Study other minifiers
- Look for patterns in real code
-
Implement safely
- Follow existing patterns
- Handle all edge cases
- Preserve semantics
-
Test thoroughly
cargo test -p oxc_minifier cargo coverage just minsize -
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