Repository: seaofvoices/darklua (rust)

Issue:
Numbers truncated resulting in loss of precision
darklua 0.15.0

Given this script:
```lua
print(tonumber('4.6982573308436185e159') == 4.6982573308436185e159)
```
Saved as `test.lua`
Run: `darklua minify test.lua test.min.lua`
Results in test.min.lua:
```lua
print(tonumber('4.6982573308436185e159')==4.698257330843618e159)
```

When both scripts are ran using luau, the first script will print `true` while the second one will print `false`

Pull request:
Fix floating point number representation
Closes #248 

This PR change the inner representation of the decimal numbers to avoid miss-formatting the numbers when printing them back.

I decided to remove the `DecimalNumber::get_raw_float` method from the public API.

- [x] add entry to the changelog


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff63160..917a833 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 # Changelog
 
+* fix floating point number representation ([#251](https://github.com/seaofvoices/darklua/pull/251))
 * read Luau configuration files (`.luaurc`) to get path aliases ([#246](https://github.com/seaofvoices/darklua/pull/246))
 * support Luau types when bundling ([#249](https://github.com/seaofvoices/darklua/pull/249))
 
diff --git a/src/generator/dense.rs b/src/generator/dense.rs
index 571429d..14c9eac 100644
--- a/src/generator/dense.rs
+++ b/src/generator/dense.rs
@@ -811,8 +811,8 @@ impl LuaGenerator for DenseLuaGenerator {
         use nodes::NumberExpression::*;
 
         match number {
-            Decimal(number) => {
-                let float = number.get_raw_float();
+            Decimal(decimal) => {
+                let float = decimal.get_raw_float();
                 if float.is_nan() {
                     self.push_char('(');
                     self.push_char('0');
@@ -829,17 +829,7 @@ impl LuaGenerator for DenseLuaGenerator {
                     self.push_char('0');
                     self.push_char(')');
                 } else {
-                    let mut result = format!("{}", float);
-
-                    if let Some(exponent) = number.get_exponent() {
-                        let exponent_char = number
-                            .is_uppercase()
-                            .map(|is_uppercase| if is_uppercase { 'E' } else { 'e' })
-                            .unwrap_or('e');
-
-                        result.push(exponent_char);
-                        result.push_str(&format!("{}", exponent));
-                    };
+                    let result = utils::write_number(number);
 
                     self.push_str(&result);
                 }
diff --git a/src/generator/mod.rs b/src/generator/mod.rs
index 5157b69..0b75742 100644
--- a/src/generator/mod.rs
+++ b/src/generator/mod.rs
@@ -1053,6 +1053,7 @@ mod $mod_name {
             number_100_25 => 100.25,
             number_2000_05 => 2000.05,
             binary_0b10101 => BinaryNumber::new(0b10101, false),
+            number_4_6982573308436185e159 => "4.6982573308436185e159".parse::<NumberExpression>().ok(),
         ));
 
         snapshot_node!($mod_name, $generator, table, write_expression => (
diff --git a/src/generator/utils.rs b/src/generator/utils.rs
index a373604..8e13950 100644
--- a/src/generator/utils.rs
+++ b/src/generator/utils.rs
@@ -1,6 +1,8 @@
 //! A module that contains the main [LuaGenerator](trait.LuaGenerator.html) trait
 //! and its implementations.
 
+use std::convert::TryInto;
+
 use crate::nodes::{
     Expression, FieldExpression, FunctionCall, IndexExpression, NumberExpression, Prefix,
     Statement, StringSegment, TableExpression, Variable,
@@ -199,25 +201,41 @@ pub fn write_number(number: &NumberExpression) -> String {
     match number {
         NumberExpression::Decimal(number) => {
             let float = number.get_raw_float();
+            #[allow(clippy::if_same_then_else)]
             if float.is_nan() {
                 "(0/0)".to_owned()
             } else if float.is_infinite() {
                 format!("({}1/0)", if float.is_sign_negative() { "-" } else { "" })
+            } else if let Some(exponent) = number
+                .get_exponent()
+                .map(TryInto::try_into)
+                .and_then(Result::ok)
+            {
+                let mantissa: f64 = float / 10.0_f64.powi(exponent);
+
+                let formatted = format!(
+                    "{}{}{}",
+                    mantissa,
+                    if number.is_uppercase().unwrap_or_default() {
+                        "E"
+                    } else {
+                        "e"
+                    },
+                    exponent
+                );
+
+                // verify if we did not lose any precision
+                if formatted.parse::<f64>() == Ok(float) {
+                    formatted
+                } else if number.is_uppercase().unwrap_or_default() {
+                    format!("{:E}", float)
+                } else {
+                    format!("{:e}", float)
+                }
+            } else if float.fract() == 0.0 {
+                format!("{}", float)
             } else {
-                format!(
-                    "{}{}",
-                    float,
-                    number
-                        .get_exponent()
-                        .map(|exponent| {
-                            let exponent_char = number
-                                .is_uppercase()
-                                .map(|is_uppercase| if is_uppercase { 'E' } else { 'e' })
-                                .unwrap_or('e');
-                            format!("{}{}", exponent_char, exponent)
-                        })
-                        .unwrap_or_else(|| "".to_owned())
-                )
+                format!("{:.}", float)
             }
         }
         NumberExpression::Hex(number) => {
diff --git a/src/nodes/expressions/mod.rs b/src/nodes/expressions/mod.rs
index fc8bebf..2027076 100644
--- a/src/nodes/expressions/mod.rs
+++ b/src/nodes/expressions/mod.rs
@@ -109,9 +109,8 @@ impl From<f64> for Expression {
                     UnaryExpression::new(UnaryOperator::Minus, Expression::from(value.abs())).into()
                 } else if value < 0.1 {
                     let exponent = value.log10().floor();
-                    let new_value = value / 10_f64.powf(exponent);
 
-                    DecimalNumber::new(new_value)
+                    DecimalNumber::new(value)
                         .with_exponent(exponent as i64, true)
                         .into()
                 } else if value > 999.0 && (value / 100.0).fract() == 0.0 {
@@ -123,7 +122,7 @@ impl From<f64> for Expression {
                         power /= 10.0;
                     }
 
-                    DecimalNumber::new(value / power)
+                    DecimalNumber::new(value)
                         .with_exponent(exponent as i64, true)
                         .into()
                 } else {
@@ -346,6 +345,7 @@ mod test {
         snapshot_from_expression!(
             f64_0 => 0_f64,
             f64_1e42 => 1e42_f64,
+            f64_1_2345e50 => 1.2345e50_f64,
             f64_infinity => f64::INFINITY,
             i64_minus_one => -1_i64,
             f64_minus_zero => -0.0,
diff --git a/src/nodes/expressions/number.rs b/src/nodes/expressions/number.rs
index bea3490..30b65e3 100644
--- a/src/nodes/expressions/number.rs
+++ b/src/nodes/expressions/number.rs
@@ -47,7 +47,7 @@ impl DecimalNumber {
     }
 
     #[inline]
-    pub fn get_raw_float(&self) -> f64 {
+    pub(crate) fn get_raw_float(&self) -> f64 {
         self.float
     }
 
@@ -62,11 +62,7 @@ impl DecimalNumber {
     }
 
     pub fn compute_value(&self) -> f64 {
-        if let Some((exponent, _)) = self.exponent {
-            self.float * 10_f64.powf(exponent as f64)
-        } else {
-            self.float
-        }
+        self.float
     }
 
     super::impl_token_fns!(iter = [token]);
@@ -406,13 +402,18 @@ impl FromStr for NumberExpression {
                         .map(filter_underscore)
                         .and_then(|string| string.parse().ok())
                         .ok_or(Self::Err::InvalidDecimalExponent)?;
-                    let number = value
+                    let _number: f64 = value
                         .get(0..index)
                         .map(filter_underscore)
                         .and_then(|string| string.parse().ok())
                         .ok_or(Self::Err::InvalidDecimalNumber)?;
 
-                    DecimalNumber::new(number).with_exponent(exponent, exponent_is_uppercase)
+                    DecimalNumber::new(
+                        filter_underscore(value)
+                            .parse::<f64>()
+                            .map_err(|_| Self::Err::InvalidDecimalNumber)?,
+                    )
+                    .with_exponent(exponent, exponent_is_uppercase)
                 } else {
                     let number = filter_underscore(value)
                         .parse::<f64>()
@@ -528,16 +529,17 @@ mod test {
             parse_multiple_decimal_with_underscore_after_point("0._24") => DecimalNumber::new(0.24_f64),
             parse_float_with_trailing_dot("123.") => DecimalNumber::new(123_f64),
             parse_starting_with_dot(".123") => DecimalNumber::new(0.123_f64),
-            parse_digit_with_exponent("1e10") => DecimalNumber::new(1_f64).with_exponent(10, false),
-            parse_digit_with_exponent_and_underscore("1e_10") => DecimalNumber::new(1_f64).with_exponent(10, false),
-            parse_number_with_exponent("123e456") => DecimalNumber::new(123_f64).with_exponent(456, false),
-            parse_number_with_exponent_and_plus_symbol("123e+456") => DecimalNumber::new(123_f64).with_exponent(456, false),
-            parse_number_with_negative_exponent("123e-456") => DecimalNumber::new(123_f64).with_exponent(-456, false),
-            parse_number_with_upper_exponent("123E4") => DecimalNumber::new(123_f64).with_exponent(4, true),
-            parse_number_with_upper_negative_exponent("123E-456") => DecimalNumber::new(123_f64).with_exponent(-456, true),
-            parse_float_with_exponent("10.12e8") => DecimalNumber::new(10.12_f64).with_exponent(8, false),
-            parse_float_with_exponent_and_underscores("10_0.12_e_8") => DecimalNumber::new(100.12_f64).with_exponent(8, false),
-            parse_trailing_dot_with_exponent("10.e8") => DecimalNumber::new(10_f64).with_exponent(8, false),
+            parse_digit_with_exponent("1e10") => DecimalNumber::new(1e10_f64).with_exponent(10, false),
+            parse_digit_with_exponent_and_underscore("1e_10") => DecimalNumber::new(1e10_f64).with_exponent(10, false),
+            parse_number_with_exponent("123e101") => DecimalNumber::new(123e101_f64).with_exponent(101, false),
+            parse_number_with_exponent_and_plus_symbol("123e+121") => DecimalNumber::new(123e121_f64).with_exponent(121, false),
+            parse_number_with_negative_exponent("123e-456") => DecimalNumber::new(123e-456_f64).with_exponent(-456, false),
+            parse_number_with_upper_exponent("123E4") => DecimalNumber::new(123e4_f64).with_exponent(4, true),
+            parse_number_with_upper_negative_exponent("123E-456") => DecimalNumber::new(123e-456_f64).with_exponent(-456, true),
+            parse_float_with_exponent("10.12e8") => DecimalNumber::new(10.12e8_f64).with_exponent(8, false),
+            parse_float_with_exponent_and_underscores("10_0.12_e_8") => DecimalNumber::new(100.12e8_f64).with_exponent(8, false),
+            parse_float_with_exponent_2("4.6982573308436185e159") => DecimalNumber::new(4.6982573308436185e159_f64).with_exponent(159, false),
+            parse_trailing_dot_with_exponent("10.e8") => DecimalNumber::new(10e8_f64).with_exponent(8, false),
             parse_hex_number("0x12") => HexNumber::new(18, false),
             parse_hex_number_with_underscore_before_x("0_x12") => HexNumber::new(18, false),
             parse_hex_number_with_underscores_around_x("0_x_12") => HexNumber::new(18, false),
diff --git a/src/nodes/expressions/string.rs b/src/nodes/expressions/string.rs
index e2d42f3..f7cdf04 100644
--- a/src/nodes/expressions/string.rs
+++ b/src/nodes/expressions/string.rs
@@ -42,8 +42,7 @@ impl StringExpression {
 
         match (chars.next(), chars.next_back()) {
             (Some((_, first_char)), Some((_, last_char))) if first_char == last_char => {
-                string_utils::read_escaped_string(chars, Some(string.as_bytes().len()))
-                    .map(Self::from_value)
+                string_utils::read_escaped_string(chars, Some(string.len())).map(Self::from_value)
             }
             (None, None) | (None, Some(_)) | (Some(_), None) => {
                 Err(StringError::invalid("missing quotes"))


Test patch:
diff --git a/src/generator/snapshots/darklua_core__generator__test__dense__snapshots__number__dense_number_number_4_6982573308436185e159.snap b/src/generator/snapshots/darklua_core__generator__test__dense__snapshots__number__dense_number_number_4_6982573308436185e159.snap
new file mode 100644
index 0000000..dfa0c2f
--- /dev/null
+++ b/src/generator/snapshots/darklua_core__generator__test__dense__snapshots__number__dense_number_number_4_6982573308436185e159.snap
@@ -0,0 +1,5 @@
+---
+source: src/generator/mod.rs
+expression: generator.into_string()
+---
+4.6982573308436185e159
diff --git a/src/generator/snapshots/darklua_core__generator__test__readable__snapshots__number__readable_number_number_4_6982573308436185e159.snap b/src/generator/snapshots/darklua_core__generator__test__readable__snapshots__number__readable_number_number_4_6982573308436185e159.snap
new file mode 100644
index 0000000..dfa0c2f
--- /dev/null
+++ b/src/generator/snapshots/darklua_core__generator__test__readable__snapshots__number__readable_number_number_4_6982573308436185e159.snap
@@ -0,0 +1,5 @@
+---
+source: src/generator/mod.rs
+expression: generator.into_string()
+---
+4.6982573308436185e159
diff --git a/src/generator/snapshots/darklua_core__generator__test__token_based__snapshots__number__token_based_number_number_4_6982573308436185e159.snap b/src/generator/snapshots/darklua_core__generator__test__token_based__snapshots__number__token_based_number_number_4_6982573308436185e159.snap
new file mode 100644
index 0000000..dfa0c2f
--- /dev/null
+++ b/src/generator/snapshots/darklua_core__generator__test__token_based__snapshots__number__token_based_number_number_4_6982573308436185e159.snap
@@ -0,0 +1,5 @@
+---
+source: src/generator/mod.rs
+expression: generator.into_string()
+---
+4.6982573308436185e159
diff --git a/src/nodes/expressions/snapshots/darklua_core__nodes__expressions__test__expression_from_floats__f64_1_2345e50.snap b/src/nodes/expressions/snapshots/darklua_core__nodes__expressions__test__expression_from_floats__f64_1_2345e50.snap
new file mode 100644
index 0000000..4324855
--- /dev/null
+++ b/src/nodes/expressions/snapshots/darklua_core__nodes__expressions__test__expression_from_floats__f64_1_2345e50.snap
@@ -0,0 +1,18 @@
+---
+source: src/nodes/expressions/mod.rs
+expression: result
+---
+Number(
+    Decimal(
+        DecimalNumber {
+            float: 1.2345e50,
+            exponent: Some(
+                (
+                    46,
+                    true,
+                ),
+            ),
+            token: None,
+        },
+    ),
+)
diff --git a/src/nodes/expressions/snapshots/darklua_core__nodes__expressions__test__expression_from_floats__f64_1e42.snap b/src/nodes/expressions/snapshots/darklua_core__nodes__expressions__test__expression_from_floats__f64_1e42.snap
index 41f1229..64ba0db 100644
--- a/src/nodes/expressions/snapshots/darklua_core__nodes__expressions__test__expression_from_floats__f64_1e42.snap
+++ b/src/nodes/expressions/snapshots/darklua_core__nodes__expressions__test__expression_from_floats__f64_1e42.snap
@@ -5,7 +5,7 @@ expression: result
 Number(
     Decimal(
         DecimalNumber {
-            float: 1.0,
+            float: 1e42,
             exponent: Some(
                 (
                     42,