Skip to content

Commit

Permalink
feat(codegen): print negative numbers (#6624)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Oct 16, 2024
1 parent 7f6b219 commit e5ed6a5
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 135 deletions.
7 changes: 6 additions & 1 deletion crates/oxc_codegen/src/binary_expr_visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,15 @@ impl<'a> BinaryExpressionVisitor<'a> {
}
}
BinaryishOperator::Binary(BinaryOperator::Exponential) => {
if matches!(e.left(), Expression::UnaryExpression(_)) {
// Negative numbers are printed using a unary operator
if matches!(
e.left(),
Expression::UnaryExpression(_) | Expression::NumericLiteral(_)
) {
self.left_precedence = Precedence::Call;
}
}

_ => {}
}

Expand Down
8 changes: 8 additions & 0 deletions crates/oxc_codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ bitflags! {
/// [In]
const FORBID_IN = 1 << 0;
const FORBID_CALL = 1 << 1;
const TYPESCRIPT = 1 << 2;
}
}

Expand All @@ -20,6 +21,13 @@ impl Context {
self.contains(Self::FORBID_CALL)
}

#[inline]
#[must_use]
pub fn with_typescript(mut self) -> Self {
self |= Self::TYPESCRIPT;
self
}

#[inline]
#[must_use]
pub fn and_forbid_in(self, include: bool) -> Self {
Expand Down
131 changes: 52 additions & 79 deletions crates/oxc_codegen/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ impl<'a> GenExpr for Expression<'a> {
match self {
Self::BooleanLiteral(lit) => lit.print(p, ctx),
Self::NullLiteral(lit) => lit.print(p, ctx),
Self::NumericLiteral(lit) => lit.print(p, ctx),
Self::NumericLiteral(lit) => lit.print_expr(p, precedence, ctx),
Self::BigIntLiteral(lit) => lit.print(p, ctx),
Self::RegExpLiteral(lit) => lit.print(p, ctx),
Self::StringLiteral(lit) => lit.print(p, ctx),
Expand Down Expand Up @@ -1102,86 +1102,44 @@ impl Gen for NullLiteral {
}
}

// Need a space before "." if it could be parsed as a decimal point.
fn need_space_before_dot(s: &str, p: &mut Codegen) {
if !s.bytes().any(|b| matches!(b, b'.' | b'e' | b'x')) {
p.need_space_before_dot = p.code_len();
}
}

impl<'a> Gen for NumericLiteral<'a> {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
fn gen(&self, p: &mut Codegen, _ctx: Context) {
impl<'a> GenExpr for NumericLiteral<'a> {
fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) {
p.add_source_mapping(self.span.start);
if !p.options.minify && !self.raw.is_empty() {
let value = self.value;
if ctx.contains(Context::TYPESCRIPT) {
p.print_str(self.raw);
need_space_before_dot(self.raw, p);
} else if self.value != f64::INFINITY {
} else if value.is_nan() {
p.print_space_before_identifier();
p.print_str("NaN");
} else if value.is_infinite() {
let wrap = (p.options.minify && precedence >= Precedence::Multiply)
|| (value.is_sign_negative() && precedence >= Precedence::Prefix);
p.wrap(wrap, |p| {
if value.is_sign_negative() {
p.print_space_before_operator(Operator::Unary(UnaryOperator::UnaryNegation));
p.print_ascii_byte(b'-');
} else {
p.print_space_before_identifier();
}
if p.options.minify {
p.print_str("1/0");
} else {
p.print_str("1 / 0");
}
});
} else if value.is_sign_positive() {
p.print_space_before_identifier();
let abs_value = self.value.abs();
if self.value.is_sign_negative() {
p.print_space_before_operator(Operator::Unary(UnaryOperator::UnaryNegation));
p.print_str("-");
}
let result = get_minified_number(abs_value);
let bytes = result.as_str();
p.print_str(bytes);
need_space_before_dot(bytes, p);
} else if self.value == f64::INFINITY && self.raw.is_empty() {
p.print_str("Infinity");
need_space_before_dot("Infinity", p);
p.print_non_negative_float(value);
} else if precedence >= Precedence::Prefix {
p.print_str("(-");
p.print_non_negative_float(value.abs());
p.print_ascii_byte(b')');
} else {
p.print_str(self.raw);
need_space_before_dot(self.raw, p);
};
}
}

// https://github.com/terser/terser/blob/c5315c3fd6321d6b2e076af35a70ef532f498505/lib/output.js#L2418
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_possible_wrap)]
fn get_minified_number(num: f64) -> String {
use oxc_syntax::number::ToJsString;
if num < 1000.0 && num.fract() == 0.0 {
return num.to_js_string();
}

let mut s = num.to_js_string();

if s.starts_with("0.") {
s = s[1..].to_string();
}

s = s.cow_replacen("e+", "e", 1).to_string();

let mut candidates = vec![s.clone()];

if num.fract() == 0.0 {
candidates.push(format!("0x{:x}", num as u128));
}

if s.starts_with(".0") {
// create `1e-2`
if let Some((i, _)) = s[1..].bytes().enumerate().find(|(_, c)| *c != b'0') {
let len = i + 1; // `+1` to include the dot.
let digits = &s[len..];
candidates.push(format!("{digits}e-{}", digits.len() + len - 1));
p.print_space_before_operator(Operator::Unary(UnaryOperator::UnaryNegation));
p.print_ascii_byte(b'-');
p.print_non_negative_float(value.abs());
}
} else if s.ends_with('0') {
// create 1e2
if let Some((len, _)) = s.bytes().rev().enumerate().find(|(_, c)| *c != b'0') {
candidates.push(format!("{}e{len}", &s[0..s.len() - len]));
}
} else if let Some((integer, point, exponent)) =
s.split_once('.').and_then(|(a, b)| b.split_once('e').map(|e| (a, e.0, e.1)))
{
// `1.2e101` -> ("1", "2", "101")
candidates.push(format!(
"{integer}{point}e{}",
exponent.parse::<isize>().unwrap() - point.len() as isize
));
}

candidates.into_iter().min_by_key(String::len).unwrap()
}

impl<'a> Gen for BigIntLiteral<'a> {
Expand Down Expand Up @@ -1568,13 +1526,25 @@ impl<'a> Gen for ObjectProperty<'a> {
}
}

if self.computed {
let mut computed = self.computed;

// "{ -1: 0 }" must be printed as "{ [-1]: 0 }"
// "{ 1/0: 0 }" must be printed as "{ [1/0]: 0 }"
if !computed {
if let Some(Expression::NumericLiteral(n)) = self.key.as_expression() {
if n.value.is_sign_negative() || n.value.is_infinite() {
computed = true;
}
}
}

if computed {
p.print_ascii_byte(b'[');
}
if !shorthand {
self.key.print(p, ctx);
}
if self.computed {
if computed {
p.print_ascii_byte(b']');
}
if !shorthand {
Expand Down Expand Up @@ -2843,6 +2813,7 @@ impl<'a> Gen for TSTypeAnnotation<'a> {

impl<'a> Gen for TSType<'a> {
fn gen(&self, p: &mut Codegen, ctx: Context) {
let ctx = ctx.with_typescript();
match self {
Self::TSFunctionType(ty) => ty.print(p, ctx),
Self::TSConstructorType(ty) => ty.print(p, ctx),
Expand Down Expand Up @@ -3162,7 +3133,7 @@ impl<'a> Gen for TSLiteral<'a> {
match self {
Self::BooleanLiteral(decl) => decl.print(p, ctx),
Self::NullLiteral(decl) => decl.print(p, ctx),
Self::NumericLiteral(decl) => decl.print(p, ctx),
Self::NumericLiteral(decl) => decl.print_expr(p, Precedence::Lowest, ctx),
Self::BigIntLiteral(decl) => decl.print(p, ctx),
Self::RegExpLiteral(decl) => decl.print(p, ctx),
Self::StringLiteral(decl) => decl.print(p, ctx),
Expand Down Expand Up @@ -3618,7 +3589,9 @@ impl<'a> Gen for TSEnumMember<'a> {
TSEnumMemberName::StaticIdentifier(decl) => decl.print(p, ctx),
TSEnumMemberName::StaticStringLiteral(decl) => decl.print(p, ctx),
TSEnumMemberName::StaticTemplateLiteral(decl) => decl.print(p, ctx),
TSEnumMemberName::StaticNumericLiteral(decl) => decl.print(p, ctx),
TSEnumMemberName::StaticNumericLiteral(decl) => {
decl.print_expr(p, Precedence::Lowest, ctx);
}
decl @ match_expression!(TSEnumMemberName) => {
p.print_str("[");
decl.to_expression().print_expr(p, Precedence::Lowest, ctx);
Expand Down
63 changes: 63 additions & 0 deletions crates/oxc_codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,69 @@ impl<'a> Codegen<'a> {
}
}

fn print_non_negative_float(&mut self, num: f64) {
use oxc_syntax::number::ToJsString;
if num < 1000.0 && num.fract() == 0.0 {
self.print_str(&num.to_js_string());
self.need_space_before_dot = self.code_len();
} else {
let s = Self::get_minified_number(num);
self.print_str(&s);
if !s.bytes().any(|b| matches!(b, b'.' | b'e' | b'x')) {
self.need_space_before_dot = self.code_len();
}
}
}

// `get_minified_number` from terser
// https://github.com/terser/terser/blob/c5315c3fd6321d6b2e076af35a70ef532f498505/lib/output.js#L2418
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_possible_wrap)]
fn get_minified_number(num: f64) -> String {
use cow_utils::CowUtils;
use oxc_syntax::number::ToJsString;
if num < 1000.0 && num.fract() == 0.0 {
return num.to_js_string();
}

let mut s = num.to_js_string();

if s.starts_with("0.") {
s = s[1..].to_string();
}

s = s.cow_replacen("e+", "e", 1).to_string();

let mut candidates = vec![s.clone()];

if num.fract() == 0.0 {
candidates.push(format!("0x{:x}", num as u128));
}

if s.starts_with(".0") {
// create `1e-2`
if let Some((i, _)) = s[1..].bytes().enumerate().find(|(_, c)| *c != b'0') {
let len = i + 1; // `+1` to include the dot.
let digits = &s[len..];
candidates.push(format!("{digits}e-{}", digits.len() + len - 1));
}
} else if s.ends_with('0') {
// create 1e2
if let Some((len, _)) = s.bytes().rev().enumerate().find(|(_, c)| *c != b'0') {
candidates.push(format!("{}e{len}", &s[0..s.len() - len]));
}
} else if let Some((integer, point, exponent)) =
s.split_once('.').and_then(|(a, b)| b.split_once('e').map(|e| (a, e.0, e.1)))
{
// `1.2e101` -> ("1", "2", "101")
candidates.push(format!(
"{integer}{point}e{}",
exponent.parse::<isize>().unwrap() - point.len() as isize
));
}

candidates.into_iter().min_by_key(String::len).unwrap()
}

#[inline]
fn wrap<F: FnMut(&mut Self)>(&mut self, wrap: bool, mut f: F) {
if wrap {
Expand Down
Loading

0 comments on commit e5ed6a5

Please sign in to comment.