Skip to content

Commit

Permalink
feat(minifier): fold .toString() (#8308)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Jan 7, 2025
1 parent 30cee0e commit 922c514
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 34 deletions.
8 changes: 7 additions & 1 deletion crates/oxc_ecmascript/src/to_boolean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ impl<'a> ToBoolean<'a> for Expression<'a> {
| Expression::ObjectExpression(_) => Some(true),
Expression::NullLiteral(_) => Some(false),
Expression::BooleanLiteral(boolean_literal) => Some(boolean_literal.value),
Expression::NumericLiteral(number_literal) => Some(number_literal.value != 0.0),
Expression::NumericLiteral(lit) => Some({
if lit.value.is_nan() {
false
} else {
lit.value != 0.0
}
}),
Expression::BigIntLiteral(big_int_literal) => Some(!big_int_literal.is_zero()),
Expression::StringLiteral(string_literal) => Some(!string_literal.value.is_empty()),
Expression::TemplateLiteral(template_literal) => {
Expand Down
8 changes: 7 additions & 1 deletion crates/oxc_ecmascript/src/to_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,13 @@ impl<'a> ToJsString<'a> for IdentifierReference<'a> {
impl<'a> ToJsString<'a> for NumericLiteral<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
use oxc_syntax::number::ToJsString;
Some(Cow::Owned(self.value.to_js_string()))
let value = self.value;
let s = value.to_js_string();
Some(if value == 0.0 {
Cow::Borrowed("0")
} else {
Cow::Owned(if value.is_sign_negative() && value != 0.0 { format!("-{s}") } else { s })
})
}
}

Expand Down
32 changes: 1 addition & 31 deletions crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use oxc_ast::ast::*;
use oxc_ecmascript::{
constant_evaluation::{ConstantEvaluation, ConstantValue, ValueType},
side_effects::MayHaveSideEffects,
ToJsString,
};
use oxc_span::{GetSpan, SPAN};
use oxc_syntax::{
Expand Down Expand Up @@ -37,8 +36,7 @@ impl<'a> Traverse<'a> for PeepholeFoldConstants {
Expression::StaticMemberExpression(e) => Self::try_fold_static_member_expr(e, ctx),
Expression::LogicalExpression(e) => Self::try_fold_logical_expr(e, ctx),
Expression::ChainExpression(e) => Self::try_fold_optional_chain(e, ctx),
Expression::CallExpression(e) => Self::try_fold_number_constructor(e, ctx)
.or_else(|| Self::try_fold_to_string(e, ctx)),
Expression::CallExpression(e) => Self::try_fold_number_constructor(e, ctx),
_ => None,
} {
*expr = folded_expr;
Expand Down Expand Up @@ -606,26 +604,6 @@ impl<'a, 'b> PeepholeFoldConstants {
))
}

fn try_fold_to_string(e: &CallExpression<'a>, ctx: Ctx<'a, 'b>) -> Option<Expression<'a>> {
let Expression::StaticMemberExpression(member_expr) = &e.callee else { return None };
if member_expr.property.name != "toString" {
return None;
}
if !e.arguments.is_empty() {
return None;
}
let object = &member_expr.object;
if !matches!(
ValueType::from(object),
ValueType::String | ValueType::Boolean | ValueType::Number
) {
return None;
}
object
.to_js_string()
.map(|value| ctx.ast.expression_string_literal(object.span(), value, None))
}

// `typeof a === typeof b` -> `typeof a == typeof b`, `typeof a != typeof b` -> `typeof a != typeof b`,
// `typeof a == typeof a` -> `true`, `typeof a != typeof a` -> `false`
fn try_fold_binary_typeof_comparison(
Expand Down Expand Up @@ -1829,14 +1807,6 @@ mod test {
test_same("var Number; Number(1)");
}

#[test]
fn test_fold_to_string() {
test("'x'.toString()", "'x'");
test("1 .toString()", "'1'");
test("true.toString()", "'true'");
test("false.toString()", "'false'");
}

#[test]
fn test_fold_typeof_addition_string() {
test_same("typeof foo");
Expand Down
117 changes: 117 additions & 0 deletions crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ impl<'a> PeepholeReplaceKnownMethods {
"charCodeAt" => Self::try_fold_string_char_code_at(ce, member, ctx),
"replace" | "replaceAll" => Self::try_fold_string_replace(ce, member, ctx),
"fromCharCode" => Self::try_fold_string_from_char_code(ce, member, ctx),
"toString" => Self::try_fold_to_string(ce, member, ctx),
_ => None,
};
if let Some(replacement) = replacement {
Expand Down Expand Up @@ -246,6 +247,81 @@ impl<'a> PeepholeReplaceKnownMethods {
}
Some(ctx.ast.expression_string_literal(ce.span, s, None))
}

#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_lossless,
clippy::float_cmp
)]
fn try_fold_to_string(
ce: &CallExpression<'a>,
member: &StaticMemberExpression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
let args = &ce.arguments;
match &member.object {
// Number.prototype.toString()
// Number.prototype.toString(radix)
Expression::NumericLiteral(lit) if args.len() <= 1 => {
let mut radix: u32 = 0;
if args.is_empty() {
radix = 10;
}
if let Some(Argument::NumericLiteral(n)) = args.first() {
if n.value >= 2.0 && n.value <= 36.0 && n.value.fract() == 0.0 {
radix = n.value as u32;
}
}
if radix == 0 {
return None;
}
if lit.value.is_nan() {
return Some(ctx.ast.expression_string_literal(ce.span, "NaN", None));
}
if lit.value.is_infinite() {
return Some(ctx.ast.expression_string_literal(ce.span, "Infinity", None));
}
if radix == 10 {
use oxc_syntax::number::ToJsString;
return Some(ctx.ast.expression_string_literal(
ce.span,
lit.value.to_js_string(),
None,
));
}
// Only convert integers for other radix values.
let value = lit.value;
if value >= 0.0 && value.fract() != 0.0 {
return None;
}
let i = value as u32;
if i as f64 != value {
return None;
}
Some(ctx.ast.expression_string_literal(ce.span, Self::format_radix(i, radix), None))
}
e if e.is_literal() && args.is_empty() => {
use oxc_ecmascript::ToJsString;
e.to_js_string().map(|s| ctx.ast.expression_string_literal(ce.span, s, None))
}
_ => None,
}
}

fn format_radix(mut x: u32, radix: u32) -> String {
debug_assert!((2..=36).contains(&radix));
let mut result = vec![];
loop {
let m = x % radix;
x /= radix;
result.push(std::char::from_digit(m, radix).unwrap());
if x == 0 {
break;
}
}
result.into_iter().rev().collect()
}
}

/// Port from: <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java>
Expand Down Expand Up @@ -1109,4 +1185,45 @@ mod test {
test("String.fromCharCode('x')", "'\\0'");
test("String.fromCharCode('0.5')", "'\\0'");
}

#[test]
fn test_to_string() {
test("false.toString()", "'false';");
test("true.toString()", "'true';");
test("'xy'.toString()", "'xy';");
test("0 .toString()", "'0';");
test("123 .toString()", "'123';");
test("NaN.toString()", "'NaN';");
test("Infinity.toString()", "'Infinity';");
// test("/a\\\\b/ig.toString()", "'/a\\\\\\\\b/ig';");

test("100 .toString(0)", "100 .toString(0)");
test("100 .toString(1)", "100 .toString(1)");
test("100 .toString(2)", "'1100100'");
test("100 .toString(5)", "'400'");
test("100 .toString(8)", "'144'");
test("100 .toString(13)", "'79'");
test("100 .toString(16)", "'64'");
test("10000 .toString(19)", "'18d6'");
test("10000 .toString(23)", "'iki'");
test("1000000 .toString(29)", "'1c01m'");
test("1000000 .toString(31)", "'12hi2'");
test("1000000 .toString(36)", "'lfls'");
test("0 .toString(36)", "'0'");
test("0.5.toString()", "'0.5'");

test("false.toString(b)", "false.toString(b)");
test("true.toString(b)", "true.toString(b)");
test("'xy'.toString(b)", "'xy'.toString(b)");
test("123 .toString(b)", "123 .toString(b)");
test("1e99.toString(b)", "1e99.toString(b)");
test("/./.toString(b)", "/./.toString(b)");

// Will get constant folded into positive values
test_same("(-0).toString()");
test_same("(-123).toString()");
test_same("(-Infinity).toString()");
test_same("(-1000000).toString(36)");
test_same("(-0).toString(36)");
}
}
2 changes: 1 addition & 1 deletion crates/oxc_minifier/src/node_util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl<'a> Ctx<'a, '_> {
}

pub fn is_identifier_nan(self, ident: &IdentifierReference) -> bool {
if ident.name == "Infinity" && ident.is_global_reference(self.symbols()) {
if ident.name == "NaN" && ident.is_global_reference(self.symbols()) {
return true;
}
false
Expand Down

0 comments on commit 922c514

Please sign in to comment.