Skip to content

Commit

Permalink
feat(minifier): constant fold nullish coalescing operator (#5761)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Sep 13, 2024
1 parent 77d9170 commit e968e9f
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 1 deletion.
42 changes: 41 additions & 1 deletion crates/oxc_minifier/src/ast_passes/fold_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ use oxc_syntax::{
use oxc_traverse::{Ancestor, Traverse, TraverseCtx};

use crate::{
node_util::{is_exact_int64, IsLiteralValue, MayHaveSideEffects, NodeUtil, NumberValue},
node_util::{
is_exact_int64, IsLiteralValue, MayHaveSideEffects, NodeUtil, NumberValue, ValueType,
},
tri::Tri,
ty::Ty,
CompressorPass,
Expand Down Expand Up @@ -51,6 +53,9 @@ impl<'a> FoldConstants {
{
self.try_fold_and_or(e, ctx)
}
Expression::LogicalExpression(e) if e.operator == LogicalOperator::Coalesce => {
self.try_fold_coalesce(e, ctx)
}
Expression::UnaryExpression(e) => self.try_fold_unary_expression(e, ctx),
_ => None,
} {
Expand Down Expand Up @@ -655,4 +660,39 @@ impl<'a> FoldConstants {
}
None
}

/// Try to fold a nullish coalesce `foo ?? bar`.
pub fn try_fold_coalesce(
&self,
logical_expr: &mut LogicalExpression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
debug_assert_eq!(logical_expr.operator, LogicalOperator::Coalesce);
let left = &logical_expr.left;
let left_val = ctx.get_known_value_type(left);
match left_val {
ValueType::Null | ValueType::Void => {
Some(if left.may_have_side_effects() {
// e.g. `(a(), null) ?? 1` => `(a(), null, 1)`
let expressions = ctx.ast.vec_from_iter([
ctx.ast.move_expression(&mut logical_expr.left),
ctx.ast.move_expression(&mut logical_expr.right),
]);
ctx.ast.expression_sequence(SPAN, expressions)
} else {
// nullish condition => this expression evaluates to the right side.
ctx.ast.move_expression(&mut logical_expr.right)
})
}
ValueType::Number
| ValueType::Bigint
| ValueType::String
| ValueType::Boolean
| ValueType::Object => {
// non-nullish condition => this expression evaluates to the left side.
Some(ctx.ast.move_expression(&mut logical_expr.left))
}
ValueType::Undetermined => None,
}
}
}
35 changes: 35 additions & 0 deletions crates/oxc_minifier/src/node_util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ pub fn is_exact_int64(num: f64) -> bool {
num.fract() == 0.0
}

#[derive(Debug, Eq, PartialEq)]
pub enum ValueType {
Undetermined,
Null,
Void,
Number,
Bigint,
String,
Boolean,
Object,
}

pub trait NodeUtil {
fn symbols(&self) -> &SymbolTable;

Expand Down Expand Up @@ -391,4 +403,27 @@ pub trait NodeUtil {

return BigInt::parse_bytes(s.as_bytes(), 10);
}

/// Evaluate and attempt to determine which primitive value type it could resolve to.
/// Without proper type information some assumptions had to be made for operations that could
/// result in a BigInt or a Number. If there is not enough information available to determine one
/// or the other then we assume Number in order to maintain historical behavior of the compiler and
/// avoid breaking projects that relied on this behavior.
fn get_known_value_type(&self, e: &Expression<'_>) -> ValueType {
match e {
Expression::NumericLiteral(_) => ValueType::Number,
Expression::NullLiteral(_) => ValueType::Null,
Expression::ArrayExpression(_) | Expression::ObjectExpression(_) => ValueType::Object,
Expression::BooleanLiteral(_) => ValueType::Boolean,
Expression::Identifier(ident) if self.is_identifier_undefined(ident) => ValueType::Void,
Expression::SequenceExpression(e) => e
.expressions
.last()
.map_or(ValueType::Undetermined, |e| self.get_known_value_type(e)),
Expression::BigIntLiteral(_) => ValueType::Bigint,
Expression::StringLiteral(_) | Expression::TemplateLiteral(_) => ValueType::String,
// TODO: complete this
_ => ValueType::Undetermined,
}
}
}
34 changes: 34 additions & 0 deletions crates/oxc_minifier/tests/ast_passes/fold_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,40 @@ fn test_fold_logical_op2() {
test("x = [(function(){alert(x)})()] && x", "x=([function(){alert(x)}()],x)");
}

#[test]
fn test_fold_nullish_coalesce() {
// fold if left is null/undefined
test("null ?? 1", "1");
test("undefined ?? false", "false");
test("(a(), null) ?? 1", "(a(), null, 1)");

test("x = [foo()] ?? x", "x = [foo()]");

// short circuit on all non nullish LHS
test("x = false ?? x", "x = false");
test("x = true ?? x", "x = true");
test("x = 0 ?? x", "x = 0");
test("x = 3 ?? x", "x = 3");

// unfoldable, because the right-side may be the result
test_same("a = x ?? true");
test_same("a = x ?? false");
test_same("a = x ?? 3");
test_same("a = b ? c : x ?? false");
test_same("a = b ? x ?? false : c");

// folded, but not here.
test_same("a = x ?? false ? b : c");
test_same("a = x ?? true ? b : c");

test_same("x = foo() ?? true ?? bar()");
test("x = foo() ?? (true && bar())", "x = foo() ?? bar()");
test_same("x = (foo() || false) ?? bar()");

test("a() ?? (1 ?? b())", "a() ?? 1");
test("(a() ?? 1) ?? b()", "a() ?? 1 ?? b()");
}

#[test]
fn test_fold_void() {
test_same("void 0");
Expand Down

0 comments on commit e968e9f

Please sign in to comment.