mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
perf(minifier): replace expressions without take_in dummies (#25836)
`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.
This commit is contained in:
1 parent
241c559b83
commit
673b04bc46
3 files changed
+44
-20
No files matched your search
@@ -1,4 +1,3 @@
|
||||
use oxc_allocator::TakeIn;
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_ecmascript::constant_evaluation::DetermineValueType;
|
||||
use oxc_span::GetSpan;
|
||||
@@ -32,8 +31,14 @@ impl<'a> PeepholeOptimizations {
|
||||
Expression::UnaryExpression(e)
|
||||
if e.operator.is_not() && e.argument.value_type(ctx).is_boolean() =>
|
||||
{
|
||||
let new_expr = e.argument.take_in(ctx);
|
||||
ctx.replace_expression(expr, new_expr);
|
||||
// Both discarded `!` wrappers contain no references.
|
||||
ctx.replace_expression_with(expr, |old, _| {
|
||||
let Expression::UnaryExpression(outer) = old else { unreachable!() };
|
||||
let Expression::UnaryExpression(inner) = outer.unbox().argument else {
|
||||
unreachable!()
|
||||
};
|
||||
inner.unbox().argument
|
||||
});
|
||||
}
|
||||
// `!(a == b)` => `a != b`
|
||||
// `!(a != b)` => `a == b`
|
||||
@@ -41,8 +46,7 @@ impl<'a> PeepholeOptimizations {
|
||||
// `!(a !== b)` => `a === b`
|
||||
Expression::BinaryExpression(binary_expr) if binary_expr.operator.is_equality() => {
|
||||
binary_expr.operator = binary_expr.operator.equality_inverse_operator().unwrap();
|
||||
let new_expr = e.argument.take_in(ctx);
|
||||
ctx.replace_expression(expr, new_expr);
|
||||
ctx.replace_expression_with(expr, Self::unwrap_not);
|
||||
}
|
||||
// `!(a == b || c == d)` => `a != b && c != d`
|
||||
// `!(a == b && c == d)` => `a != b || c != d`
|
||||
@@ -56,23 +60,27 @@ impl<'a> PeepholeOptimizations {
|
||||
if Self::de_morgan_paren_delta(logical_expr).is_some_and(|delta| delta <= 0) =>
|
||||
{
|
||||
Self::de_morgan_invert_logical(logical_expr);
|
||||
let new_expr = e.argument.take_in(ctx);
|
||||
ctx.replace_expression(expr, new_expr);
|
||||
ctx.replace_expression_with(expr, Self::unwrap_not);
|
||||
}
|
||||
// "!(a, b)" => "a, !b"
|
||||
Expression::SequenceExpression(sequence_expr) => {
|
||||
if let Some(last_expr) = sequence_expr.expressions.last_mut() {
|
||||
let new_last =
|
||||
Self::minimize_not(last_expr.span(), last_expr.take_in(ctx), ctx);
|
||||
ctx.replace_expression(last_expr, new_last);
|
||||
let new_expr = e.argument.take_in(ctx);
|
||||
ctx.replace_expression(expr, new_expr);
|
||||
ctx.replace_expression_with(last_expr, |old, ctx| {
|
||||
Self::minimize_not(old.span(), old, ctx)
|
||||
});
|
||||
ctx.replace_expression_with(expr, Self::unwrap_not);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unwrap `!x` into `x`. The discarded `!` wrapper contains no references.
|
||||
fn unwrap_not(old: Expression<'a>, _ctx: &mut TraverseCtx<'a>) -> Expression<'a> {
|
||||
let Expression::UnaryExpression(e) = old else { unreachable!() };
|
||||
e.unbox().argument
|
||||
}
|
||||
|
||||
/// Character delta from parentheses added or removed by De Morgan's law
|
||||
/// (flipping `&&` <-> `||` changes which nested operands need parens), or
|
||||
/// `None` if some operand cannot invert its operator in place.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use oxc_allocator::GetAllocator;
|
||||
use oxc_allocator::{GetAllocator, ReplaceWith};
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_compat::{ESFeature, EngineTargets};
|
||||
use oxc_ecmascript::{
|
||||
@@ -452,6 +452,22 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
|
||||
self.state.record_ast_change();
|
||||
}
|
||||
|
||||
/// Replace an expression slot with a value built from the owned old expression.
|
||||
///
|
||||
/// Prefer this over `take_in` followed by [`Self::replace_expression`]. It avoids
|
||||
/// leaving a dummy node in the arena. The closure must report any discarded
|
||||
/// subtrees through the `drop_*` helpers; values moved into its result are not dropped.
|
||||
#[inline]
|
||||
pub fn replace_expression_with(
|
||||
&mut self,
|
||||
slot: &mut Expression<'a>,
|
||||
replacer: impl FnOnce(Expression<'a>, &mut Self) -> Expression<'a>,
|
||||
) {
|
||||
let ctx = &mut *self;
|
||||
slot.replace_with(|old| replacer(old, ctx));
|
||||
self.state.record_ast_change();
|
||||
}
|
||||
|
||||
/// Replace a statement slot. Marks the pass as having mutated the AST.
|
||||
#[inline]
|
||||
pub fn replace_statement(&mut self, slot: &mut Statement<'a>, new: Statement<'a>) {
|
||||
|
||||
@@ -5,7 +5,7 @@ checker.ts:
|
||||
sys deallocs: 150
|
||||
sys alloc bytes: 14996600 # 15.00 MB
|
||||
sys peak growth: 10169560 # 10.17 MB
|
||||
arena allocs: 148426
|
||||
arena allocs: 148403
|
||||
arena reallocs: 28468
|
||||
arena size: 19204712 # 19.20 MB
|
||||
|
||||
@@ -16,7 +16,7 @@ App.tsx:
|
||||
sys deallocs: 61
|
||||
sys alloc bytes: 2128555 # 2.13 MB
|
||||
sys peak growth: 1612427 # 1.61 MB
|
||||
arena allocs: 16046
|
||||
arena allocs: 16038
|
||||
arena reallocs: 2721
|
||||
arena size: 2887488 # 2.89 MB
|
||||
|
||||
@@ -38,7 +38,7 @@ pdf.mjs:
|
||||
sys deallocs: 2427
|
||||
sys alloc bytes: 5334247 # 5.33 MB
|
||||
sys peak growth: 3693355 # 3.69 MB
|
||||
arena allocs: 45881
|
||||
arena allocs: 45817
|
||||
arena reallocs: 7718
|
||||
arena size: 6326224 # 6.33 MB
|
||||
|
||||
@@ -49,9 +49,9 @@ antd.js:
|
||||
sys deallocs: 1038
|
||||
sys alloc bytes: 29976031 # 29.98 MB
|
||||
sys peak growth: 19934725 # 19.93 MB
|
||||
arena allocs: 371736
|
||||
arena allocs: 371595
|
||||
arena reallocs: 76289
|
||||
arena size: 49111064 # 49.11 MB
|
||||
arena size: 49108808 # 49.11 MB
|
||||
|
||||
binder.ts:
|
||||
file size: 193077 # 193.08 kB
|
||||
@@ -60,7 +60,7 @@ binder.ts:
|
||||
sys deallocs: 33
|
||||
sys alloc bytes: 963008 # 963.01 kB
|
||||
sys peak growth: 657404 # 657.40 kB
|
||||
arena allocs: 7032
|
||||
arena allocs: 7030
|
||||
arena reallocs: 842
|
||||
arena size: 1167360 # 1.17 MB
|
||||
|
||||
@@ -71,6 +71,6 @@ kitchen-sink.tsx:
|
||||
sys deallocs: 1854
|
||||
sys alloc bytes: 5307512 # 5.31 MB
|
||||
sys peak growth: 3707425 # 3.71 MB
|
||||
arena allocs: 66770
|
||||
arena allocs: 66752
|
||||
arena reallocs: 12252
|
||||
arena size: 9424360 # 9.42 MB
|
||||
Reference in new issue
Block a user