Repository: gleam-lang/gleam (rust)

Issue:
Support float segments without the need to add the `float` option
Right now we can already specify int segments and string segments without having to explicitly annotate them with `:int` or `:utf-8`: `<<1, "abc">>`. But we can't do the same with floats: `<<1.2>>` is an error. Since it's unambiguous it would be nice to not have to write `<<1.2:float>>` and it would be consistent with the other types of segment allowed in a bit array expression/constant/pattern

Pull request description:
Allow to omit the `float` option in bit array segments with literal floats
This PR closes #4396: it is now possible to not annotate bit array segments that are constant literal floats with the `:float` type annotation

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6cf7762e..a37e9f96e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -75,15 +75,31 @@
 
   ([Surya Rose](https://github.com/GearsDatapacks))
 
+- It is now possible to omit the `:float` option for literal floats used in a
+  `BitArray` segment.
+
+  ```gleam
+  <<1.11>>
+  ```
+
+  Is the same as:
+
+  ```gleam
+  <<1.11:float>>
+  ```
+
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
+
 ### Build tool
 
 - Include a type annotation for the `main` function generated by `gleam new`.
   ([Drew Olson](https://github.com/drewolson))
 
 - Two entry point scripts are now always generated by `gleam export erlang-shipment`:
+
   - `entrypoint.sh` for POSIX Shell
   - `entrypoint.ps1` for PowerShell
-  ([Greg Burri](https://github.com/ummon))
+    ([Greg Burri](https://github.com/ummon))
 
 - `gleam export` now takes a `package-information` option to export the
   project's `gleam.toml` as a JSON file.
diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs
index 7093f1b73..89f4ba126 100644
--- a/compiler-core/src/ast.rs
+++ b/compiler-core/src/ast.rs
@@ -2231,6 +2231,13 @@ impl<Value> BitArraySegment<Value, Arc<Type>> {
     }
 }
 
+impl<Value, Type> BitArraySegment<Value, Type> {
+    #[must_use]
+    pub(crate) fn has_type_option(&self) -> bool {
+        self.options.iter().any(|option| option.is_type_option())
+    }
+}
+
 impl TypedExprBitArraySegment {
     pub fn find_node(&self, byte_index: u32) -> Option<Located<'_>> {
         self.value.find_node(byte_index)
@@ -2374,6 +2381,29 @@ impl<A> BitArrayOption<A> {
             BitArrayOption::Unit { .. } => "unit".into(),
         }
     }
+
+    fn is_type_option(&self) -> bool {
+        match self {
+            BitArrayOption::Bytes { .. }
+            | BitArrayOption::Int { .. }
+            | BitArrayOption::Float { .. }
+            | BitArrayOption::Bits { .. }
+            | BitArrayOption::Utf8 { .. }
+            | BitArrayOption::Utf16 { .. }
+            | BitArrayOption::Utf32 { .. }
+            | BitArrayOption::Utf8Codepoint { .. }
+            | BitArrayOption::Utf16Codepoint { .. }
+            | BitArrayOption::Utf32Codepoint { .. } => true,
+
+            BitArrayOption::Signed { .. }
+            | BitArrayOption::Unsigned { .. }
+            | BitArrayOption::Big { .. }
+            | BitArrayOption::Little { .. }
+            | BitArrayOption::Native { .. }
+            | BitArrayOption::Size { .. }
+            | BitArrayOption::Unit { .. } => false,
+        }
+    }
 }
 
 impl BitArrayOption<TypedPattern> {
diff --git a/compiler-core/src/type_/error.rs b/compiler-core/src/type_/error.rs
index 35aed1385..e0ea90c58 100644
--- a/compiler-core/src/type_/error.rs
+++ b/compiler-core/src/type_/error.rs
@@ -930,6 +930,7 @@ pub enum FeatureKind {
     ConstantStringConcatenation,
     ArithmeticInGuards,
     UnannotatedUtf8StringSegment,
+    UnannotatedFloatSegment,
     NestedTupleAccess,
     InternalAnnotation,
     AtInJavascriptModules,
@@ -965,6 +966,7 @@ impl FeatureKind {
             }
 
             FeatureKind::JavaScriptUnalignedBitArray => Version::new(1, 9, 0),
+            FeatureKind::UnannotatedFloatSegment => Version::new(1, 10, 0),
         }
     }
 }
diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs
index d59fb38de..09c8abd73 100644
--- a/compiler-core/src/type_/expression.rs
+++ b/compiler-core/src/type_/expression.rs
@@ -1204,20 +1204,43 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
     ) -> Result<TypedExpr, Error> {
         let segments = segments
             .into_iter()
-            .map(|s| {
-                let options = match s.value.as_ref() {
-                    UntypedExpr::String { location, .. } if s.options.is_empty() => {
-                        self.track_feature_usage(
-                            FeatureKind::UnannotatedUtf8StringSegment,
-                            *location,
-                        );
-                        vec![BitArrayOption::Utf8 {
-                            location: SrcSpan::default(),
-                        }]
+            .map(|mut segment| {
+                // If the segment doesn't have an explicit type option we add a default
+                // one ourselves if the pattern is unambiguous: literal strings are
+                // implicitly considered utf-8 encoded strings, while floats are
+                // implicitly given the float type option.
+                if !segment.has_type_option() {
+                    match segment.value.as_ref() {
+                        UntypedExpr::String { location, .. } => {
+                            self.track_feature_usage(
+                                FeatureKind::UnannotatedUtf8StringSegment,
+                                *location,
+                            );
+                            segment.options.push(BitArrayOption::Utf8 {
+                                location: SrcSpan::default(),
+                            });
+                        }
+
+                        UntypedExpr::Float { location, .. } => {
+                            self.track_feature_usage(
+                                FeatureKind::UnannotatedFloatSegment,
+                                *location,
+                            );
+                            segment.options.push(BitArrayOption::Float {
+                                location: SrcSpan::default(),
+                            })
+                        }
+
+                        _ => (),
                     }
-                    _ => s.options,
-                };
-                self.infer_bit_segment(*s.value, options, s.location, |env, expr| env.infer(expr))
+                }
+
+                self.infer_bit_segment(
+                    *segment.value,
+                    segment.options,
+                    segment.location,
+                    |env, expr| env.infer(expr),
+                )
             })
             .try_collect()?;
 
@@ -1235,22 +1258,43 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
     ) -> Result<TypedConstant, Error> {
         let segments = segments
             .into_iter()
-            .map(|s| {
-                let options = match s.value.as_ref() {
-                    Constant::String { location, .. } if s.options.is_empty() => {
-                        self.track_feature_usage(
-                            FeatureKind::UnannotatedUtf8StringSegment,
-                            *location,
-                        );
-                        vec![BitArrayOption::Utf8 {
-                            location: SrcSpan::default(),
-                        }]
+            .map(|mut segment| {
+                // If the segment doesn't have an explicit type option we add a default
+                // one ourselves if the pattern is unambiguous: literal strings are
+                // implicitly considered utf-8 encoded strings, while floats are
+                // implicitly given the float type option.
+                if !segment.has_type_option() {
+                    match segment.value.as_ref() {
+                        Constant::String { location, .. } => {
+                            self.track_feature_usage(
+                                FeatureKind::UnannotatedUtf8StringSegment,
+                                *location,
+                            );
+                            segment.options.push(BitArrayOption::Utf8 {
+                                location: SrcSpan::default(),
+                            });
+                        }
+
+                        Constant::Float { location, .. } => {
+                            self.track_feature_usage(
+                                FeatureKind::UnannotatedFloatSegment,
+                                *location,
+                            );
+                            segment.options.push(BitArrayOption::Float {
+                                location: SrcSpan::default(),
+                            })
+                        }
+
+                        _ => (),
                     }
-                    _ => s.options,
-                };
-                self.infer_bit_segment(*s.value, options, s.location, |env, expr| {
-                    Ok(env.infer_const(&None, expr))
-                })
+                }
+
+                self.infer_bit_segment(
+                    *segment.value,
+                    segment.options,
+                    segment.location,
+                    |env, expr| Ok(env.infer_const(&None, expr)),
+                )
             })
             .try_collect()?;
 
diff --git a/compiler-core/src/type_/pattern.rs b/compiler-core/src/type_/pattern.rs
index 4200994f3..a560093fe 100644
--- a/compiler-core/src/type_/pattern.rs
+++ b/compiler-core/src/type_/pattern.rs
@@ -261,27 +261,35 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
 
     fn infer_pattern_segment(
         &mut self,
-        segment: UntypedPatternBitArraySegment,
+        mut segment: UntypedPatternBitArraySegment,
         is_last_segment: bool,
     ) -> Result<TypedPatternBitArraySegment, Error> {
-        let UntypedPatternBitArraySegment {
-            location,
-            options,
-            value,
-            ..
-        } = segment;
-
-        let options = match value.as_ref() {
-            Pattern::String { location, .. } if options.is_empty() => {
-                self.track_feature_usage(FeatureKind::UnannotatedUtf8StringSegment, *location);
-                vec![BitArrayOption::Utf8 {
-                    location: SrcSpan::default(),
-                }]
+        // If the segment doesn't have an explicit type option we add a default
+        // one ourselves if the pattern is unambiguous: literal strings are
+        // implicitly considered utf-8 encoded strings, while floats are
+        // implicitly given the float type option.
+        if !segment.has_type_option() {
+            match segment.value.as_ref() {
+                Pattern::String { location, .. } => {
+                    self.track_feature_usage(FeatureKind::UnannotatedUtf8StringSegment, *location);
+                    segment.options.push(BitArrayOption::Utf8 {
+                        location: SrcSpan::default(),
+                    });
+                }
+
+                Pattern::Float { location, .. } => {
+                    self.track_feature_usage(FeatureKind::UnannotatedFloatSegment, *location);
+                    segment.options.push(BitArrayOption::Float {
+                        location: SrcSpan::default(),
+                    })
+                }
+
+                _ => (),
             }
-            _ => options,
-        };
+        }
 
-        let options: Vec<_> = options
+        let options: Vec<_> = segment
+            .options
             .into_iter()
             .map(|o| {
                 crate::analyse::infer_bit_array_option(o, |value, type_| {
@@ -334,20 +342,20 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
         }
 
         let type_ = {
-            match value.deref() {
+            match segment.value.deref() {
                 Pattern::Variable { .. } if segment_type == string() => {
                     Err(Error::BitArraySegmentError {
                         error: bit_array::ErrorType::VariableUtfSegmentInPattern,
-                        location,
+                        location: segment.location,
                     })
                 }
                 _ => Ok(segment_type),
             }
         }?;
-        let typed_value = self.unify(*value, type_.clone(), None)?;
+        let typed_value = self.unify(*segment.value, type_.clone(), None)?;
 
         Ok(BitArraySegment {
-            location,
+            location: segment.location,
             value: Box::new(typed_value),
             options,
             type_,
diff --git a/compiler-core/src/warning.rs b/compiler-core/src/warning.rs
index 6de55395b..20465851a 100644
--- a/compiler-core/src/warning.rs
+++ b/compiler-core/src/warning.rs
@@ -1092,6 +1092,9 @@ See: https://tour.gleam.run/functions/pipelines/",
                         FeatureKind::UnannotatedUtf8StringSegment => {
                             "The ability to omit the `utf8` annotation for string segments was"
                         }
+                        FeatureKind::UnannotatedFloatSegment => {
+                            "The ability to omit the `float` annotation for float segments was"
+                        }
                         FeatureKind::NestedTupleAccess => {
                             "The ability to access nested tuple fields was"
                         }


Test patch:
diff --git a/compiler-core/src/type_/tests/errors.rs b/compiler-core/src/type_/tests/errors.rs
index 5a5e9f570..f87703f26 100644
--- a/compiler-core/src/type_/tests/errors.rs
+++ b/compiler-core/src/type_/tests/errors.rs
@@ -29,11 +29,6 @@ fn bit_arrays4() {
     assert_error!("let <<x:utf32>> = <<1>>");
 }
 
-#[test]
-fn bit_array() {
-    assert_error!("case <<1>> { <<2.0, a>> -> 1 _ -> 2 }");
-}
-
 #[test]
 fn bit_array_float() {
     assert_error!("case <<1>> { <<a:float>> if a > 1 -> 1 _ -> 2 }");
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__missing_float_option_in_bit_array_constant_segment_requires_v1_10.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__missing_float_option_in_bit_array_constant_segment_requires_v1_10.snap
new file mode 100644
index 000000000..bbedc5c30
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__missing_float_option_in_bit_array_constant_segment_requires_v1_10.snap
@@ -0,0 +1,21 @@
+---
+source: compiler-core/src/type_/tests/warnings.rs
+expression: pub const bits = <<1.2>>
+---
+----- SOURCE CODE
+pub const bits = <<1.2>>
+
+----- WARNING
+warning: Incompatible gleam version range
+  ┌─ /src/warning/wrn.gleam:1:20
+  │
+1 │ pub const bits = <<1.2>>
+  │                    ^^^ This requires a Gleam version >= 1.10.0
+
+The ability to omit the `float` annotation for float segments was
+introduced in version v1.10.0. But the Gleam version range specified in
+your `gleam.toml` would allow this code to run on an earlier version like
+v1.0.0, resulting in compilation errors!
+Hint: Remove the version constraint from your `gleam.toml` or update it to be:
+
+    gleam = ">= 1.10.0"
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__missing_float_option_in_bit_array_pattern_segment_requires_v1_10.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__missing_float_option_in_bit_array_pattern_segment_requires_v1_10.snap
new file mode 100644
index 000000000..09f6896fc
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__missing_float_option_in_bit_array_pattern_segment_requires_v1_10.snap
@@ -0,0 +1,28 @@
+---
+source: compiler-core/src/type_/tests/warnings.rs
+expression: "\npub fn main(a) {\n  case a {\n    <<1.2>> -> Nil\n    _ -> Nil\n  }\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main(a) {
+  case a {
+    <<1.2>> -> Nil
+    _ -> Nil
+  }
+}
+
+
+----- WARNING
+warning: Incompatible gleam version range
+  ┌─ /src/warning/wrn.gleam:4:7
+  │
+4 │     <<1.2>> -> Nil
+  │       ^^^ This requires a Gleam version >= 1.10.0
+
+The ability to omit the `float` annotation for float segments was
+introduced in version v1.10.0. But the Gleam version range specified in
+your `gleam.toml` would allow this code to run on an earlier version like
+v1.0.0, resulting in compilation errors!
+Hint: Remove the version constraint from your `gleam.toml` or update it to be:
+
+    gleam = ">= 1.10.0"
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__missing_float_option_in_bit_array_segment_requires_v1_10.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__missing_float_option_in_bit_array_segment_requires_v1_10.snap
new file mode 100644
index 000000000..fdf16b42e
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__missing_float_option_in_bit_array_segment_requires_v1_10.snap
@@ -0,0 +1,25 @@
+---
+source: compiler-core/src/type_/tests/warnings.rs
+expression: "\npub fn main() {\n  <<1.2>>\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  <<1.2>>
+}
+
+
+----- WARNING
+warning: Incompatible gleam version range
+  ┌─ /src/warning/wrn.gleam:3:5
+  │
+3 │   <<1.2>>
+  │     ^^^ This requires a Gleam version >= 1.10.0
+
+The ability to omit the `float` annotation for float segments was
+introduced in version v1.10.0. But the Gleam version range specified in
+your `gleam.toml` would allow this code to run on an earlier version like
+v1.0.0, resulting in compilation errors!
+Hint: Remove the version constraint from your `gleam.toml` or update it to be:
+
+    gleam = ">= 1.10.0"
diff --git a/compiler-core/src/type_/tests/version_inference.rs b/compiler-core/src/type_/tests/version_inference.rs
index 35d1eafa1..b647f6640 100644
--- a/compiler-core/src/type_/tests/version_inference.rs
+++ b/compiler-core/src/type_/tests/version_inference.rs
@@ -299,6 +299,39 @@ pub fn main() {
     assert_eq!(version, Version::new(1, 5, 0));
 }
 
+#[test]
+fn missing_float_option_in_bit_array_segment_requires_v1_10() {
+    let version = infer_version(
+        "
+pub fn main() {
+  <<1.2>>
+}
+",
+    );
+    assert_eq!(version, Version::new(1, 10, 0));
+}
+
+#[test]
+fn missing_float_option_in_bit_array_constant_segment_requires_v1_10() {
+    let version = infer_version("const bits = <<1.2>>");
+    assert_eq!(version, Version::new(1, 10, 0));
+}
+
+#[test]
+fn missing_float_option_in_bit_array_pattern_segment_requires_v1_10() {
+    let version = infer_version(
+        "
+pub fn main() {
+  case todo {
+    <<1.11>> -> todo
+    _ -> todo
+  }
+}
+",
+    );
+    assert_eq!(version, Version::new(1, 10, 0));
+}
+
 #[test]
 fn inference_picks_the_bigger_of_two_versions() {
     let version = infer_version(
diff --git a/compiler-core/src/type_/tests/warnings.rs b/compiler-core/src/type_/tests/warnings.rs
index eb156d4da..434450410 100644
--- a/compiler-core/src/type_/tests/warnings.rs
+++ b/compiler-core/src/type_/tests/warnings.rs
@@ -2479,6 +2479,41 @@ pub fn main(a) {
     );
 }
 
+#[test]
+fn missing_float_option_in_bit_array_segment_requires_v1_10() {
+    assert_warnings_with_gleam_version!(
+        Range::higher_than(Version::new(1, 0, 0)),
+        "
+pub fn main() {
+  <<1.2>>
+}
+",
+    );
+}
+
+#[test]
+fn missing_float_option_in_bit_array_constant_segment_requires_v1_10() {
+    assert_warnings_with_gleam_version!(
+        Range::higher_than(Version::new(1, 0, 0)),
+        "pub const bits = <<1.2>>"
+    );
+}
+
+#[test]
+fn missing_float_option_in_bit_array_pattern_segment_requires_v1_10() {
+    assert_warnings_with_gleam_version!(
+        Range::higher_than(Version::new(1, 0, 0)),
+        "
+pub fn main(a) {
+  case a {
+    <<1.2>> -> Nil
+    _ -> Nil
+  }
+}
+",
+    );
+}
+
 #[test]
 fn record_update_variant_inference_requires_v1_6() {
     assert_warnings_with_gleam_version!(