Repository: gleam-lang/gleam (rust)

Issue:
Compilation fails when no digits are specified for exponent
Currently, the compiler allows things like `1.5e`, with an empty exponent, as a valid float literal. However, this is not valid syntax in either erlang or javascript, and so the erlang compiler and javascript parser both throw errors if this is used.
I think we should disallow this syntax. From my understanding it would not be considered a breaking change as you cannot write valid code containing such syntax.

Pull request description:
Report error for missing exponents in float literal
Fixes #3856

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e1b13622..da508f26e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,10 @@
 
 ### Bug fixed
 
+- The compiler now throws an error when a float literal ends with an `e` and
+  is missing an exponent.
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
 ## v1.6.1 - 2024-11-19
 
 ### Bug fixed
diff --git a/compiler-core/src/parse/error.rs b/compiler-core/src/parse/error.rs
index c7be0ec03..dbd8dfc54 100644
--- a/compiler-core/src/parse/error.rs
+++ b/compiler-core/src/parse/error.rs
@@ -24,7 +24,8 @@ pub enum LexicalErrorType {
     DigitOutOfRadix,                                 // 0x012 , 2 is out of radix
     NumTrailingUnderscore,                           // 1_000_ is not allowed
     RadixIntNoValue,                                 // 0x, 0b, 0o without a value
-    UnexpectedStringEnd,                             // Unterminated string literal
+    MissingExponent,     // 1.0e, for example, where there is no exponent
+    UnexpectedStringEnd, // Unterminated string literal
     UnrecognizedToken { tok: char },
     InvalidTripleEqual,
 }
@@ -420,6 +421,10 @@ impl LexicalError {
                 vec!["Hint: remove it.".into()],
             ),
             LexicalErrorType::RadixIntNoValue => ("This integer has no value", vec![]),
+            LexicalErrorType::MissingExponent => (
+                "This float is missing an exponent",
+                vec!["Hint: Add an exponent or remove the trailing `e`".into()],
+            ),
             LexicalErrorType::UnexpectedStringEnd => {
                 ("The string starting here was left open", vec![])
             }
diff --git a/compiler-core/src/parse/lexer.rs b/compiler-core/src/parse/lexer.rs
index 8fcc3f43f..bc14cf61e 100644
--- a/compiler-core/src/parse/lexer.rs
+++ b/compiler-core/src/parse/lexer.rs
@@ -154,7 +154,7 @@ where
                 check_for_minus = true;
                 let name = self.lex_name()?;
                 self.emit(name);
-                self.maybe_lex_dot_access();
+                self.maybe_lex_dot_access()?;
             } else if self.is_number_start(c, self.chr1) {
                 check_for_minus = true;
                 let num = self.lex_number()?;
@@ -522,10 +522,10 @@ where
                 let _ = self.next_char();
                 self.lex_number_radix(start_pos, 2, "0b")?
             } else {
-                self.lex_decimal_number()
+                self.lex_decimal_number()?
             }
         } else {
-            self.lex_decimal_number()
+            self.lex_decimal_number()?
         };
 
         if Some('_') == self.chr0 {
@@ -580,15 +580,15 @@ where
 
     // Lex a normal number, that is, no octal, hex or binary number.
     // This function cannot be reached without the head of the stream being either 0-9 or '-', 0-9
-    fn lex_decimal_number(&mut self) -> Spanned {
+    fn lex_decimal_number(&mut self) -> LexResult {
         self.lex_decimal_or_int_number(true)
     }
 
-    fn lex_int_number(&mut self) -> Spanned {
+    fn lex_int_number(&mut self) -> LexResult {
         self.lex_decimal_or_int_number(false)
     }
 
-    fn lex_decimal_or_int_number(&mut self, can_lex_decimal: bool) -> Spanned {
+    fn lex_decimal_or_int_number(&mut self, can_lex_decimal: bool) -> LexResult {
         let start_pos = self.get_pos();
         let mut value = String::new();
         // consume negative sign
@@ -612,42 +612,50 @@ where
                             .expect("lex_normal_number scientific negative"),
                     );
                 }
-                value.push_str(&self.radix_run(10));
+                let exponent_run = self.radix_run(10);
+                if exponent_run.is_empty() {
+                    return Err(LexicalError {
+                        error: LexicalErrorType::MissingExponent,
+                        location: SrcSpan::new(start_pos, self.get_pos()),
+                    });
+                }
+                value.push_str(&exponent_run);
             }
             let end_pos = self.get_pos();
-            (
+            Ok((
                 start_pos,
                 Token::Float {
                     value: value.into(),
                 },
                 end_pos,
-            )
+            ))
         } else {
             let int_value = super::parse_int_value(&value).expect("int value to parse as bigint");
             let end_pos = self.get_pos();
-            (
+            Ok((
                 start_pos,
                 Token::Int {
                     value: value.into(),
                     int_value,
                 },
                 end_pos,
-            )
+            ))
         }
     }
 
     // Maybe lex dot access that comes after name token.
-    fn maybe_lex_dot_access(&mut self) {
+    fn maybe_lex_dot_access(&mut self) -> Result<(), LexicalError> {
         // It can be nested like: `tuple.1.2.3.4`
         loop {
             if Some('.') == self.chr0 && matches!(self.chr1, Some('0'..='9')) {
                 self.eat_single_char(Token::Dot);
-                let number = self.lex_int_number();
+                let number = self.lex_int_number()?;
                 self.emit(number);
             } else {
                 break;
             }
         }
+        Ok(())
     }
 
     // Consume a sequence of numbers with the given radix,


Test patch:
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__float_empty_exponent.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__float_empty_exponent.snap
new file mode 100644
index 000000000..60de73539
--- /dev/null
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__float_empty_exponent.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/parse/tests.rs
+expression: 1.32e
+---
+----- SOURCE CODE
+1.32e
+
+----- ERROR
+error: Syntax error
+  ┌─ /src/parse/error.gleam:1:1
+  │
+1 │ 1.32e
+  │ ^^^^^ This float is missing an exponent
+
+Hint: Add an exponent or remove the trailing `e`
diff --git a/compiler-core/src/parse/tests.rs b/compiler-core/src/parse/tests.rs
index 937b5d086..1c6f1cc62 100644
--- a/compiler-core/src/parse/tests.rs
+++ b/compiler-core/src/parse/tests.rs
@@ -1589,3 +1589,8 @@ pub fn main() {
 "#
     );
 }
+
+#[test]
+fn float_empty_exponent() {
+    assert_error!("1.32e");
+}