Repository: gleam-lang/gleam (rust)

Issue:
Allow Strings in a BitArray literal without having to specify utf8 encoding
I think it would be a neat little ux improvement to allow strings in a bit array without specifying the encoding, defaulting to utf8.

```gleam
// instead of
<<"a":utf8, 1, 2, 3>>
// just write
<<"a", 1, 2, 3>>
```

Pull request description:
Allow to omit `utf8` option for bit array string literals
This PR closes #3495 

I haven't made any changes to the formatter but it could remove any `utf8` redundant option from string literals to enforce a single style.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33a9c1463..cce16d7ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,21 @@
 
 ### Compiler
 
+- It is now possible to omit the `:utf8` option for literal strings used in a
+  `BitArray` segment.
+
+  ```gleam
+  <<"Hello", " ", "world">>
+  ```
+
+  Is the same as:
+
+  ```gleam
+  <<"Hello":utf8, " ":utf8, "world":utf8>>
+  ```
+
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
+
 ### Formatter
 
 ### Language Server
diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs
index 394a708f7..02743e779 100644
--- a/compiler-core/src/type_/expression.rs
+++ b/compiler-core/src/type_/expression.rs
@@ -1054,7 +1054,15 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
         let segments = segments
             .into_iter()
             .map(|s| {
-                self.infer_bit_segment(*s.value, s.options, s.location, |env, expr| env.infer(expr))
+                let options = match s.value.as_ref() {
+                    UntypedExpr::String { .. } if s.options.is_empty() => {
+                        vec![BitArrayOption::Utf8 {
+                            location: SrcSpan::default(),
+                        }]
+                    }
+                    _ => s.options,
+                };
+                self.infer_bit_segment(*s.value, options, s.location, |env, expr| env.infer(expr))
             })
             .try_collect()?;
 
@@ -1073,7 +1081,13 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
         let segments = segments
             .into_iter()
             .map(|s| {
-                self.infer_bit_segment(*s.value, s.options, s.location, |env, expr| {
+                let options = match s.value.as_ref() {
+                    Constant::String { .. } => vec![BitArrayOption::Utf8 {
+                        location: SrcSpan::default(),
+                    }],
+                    _ => s.options,
+                };
+                self.infer_bit_segment(*s.value, options, s.location, |env, expr| {
                     Ok(env.infer_const(&None, expr))
                 })
             })
diff --git a/compiler-core/src/type_/pattern.rs b/compiler-core/src/type_/pattern.rs
index 9dddf4934..4484e82eb 100644
--- a/compiler-core/src/type_/pattern.rs
+++ b/compiler-core/src/type_/pattern.rs
@@ -7,7 +7,9 @@ use itertools::Itertools;
 use super::*;
 use crate::{
     analyse::{name::check_name_case, Inferred},
-    ast::{AssignName, ImplicitCallArgOrigin, Layer, UntypedPatternBitArraySegment},
+    ast::{
+        AssignName, BitArrayOption, ImplicitCallArgOrigin, Layer, UntypedPatternBitArraySegment,
+    },
 };
 use std::sync::Arc;
 
@@ -179,6 +181,13 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
             ..
         } = segment;
 
+        let options = match value.as_ref() {
+            Pattern::String { .. } if options.is_empty() => vec![BitArrayOption::Utf8 {
+                location: SrcSpan::default(),
+            }],
+            _ => options,
+        };
+
         let options: Vec<_> = options
             .into_iter()
             .map(|o| crate::analyse::infer_bit_array_option(o, |value, typ| self.unify(value, typ)))


Test patch:
diff --git a/compiler-core/src/erlang/tests/bit_arrays.rs b/compiler-core/src/erlang/tests/bit_arrays.rs
index 31bdc1aaf..eba9754e2 100644
--- a/compiler-core/src/erlang/tests/bit_arrays.rs
+++ b/compiler-core/src/erlang/tests/bit_arrays.rs
@@ -22,7 +22,7 @@ fn bit_array_float() {
         r#"pub fn main() {
   let b = 16
   let floats = <<1.0:16-float, 5.0:float-32, 6.0:float-64-little, 1.0:float-size(b)>>
-  let assert <<1.0:16-float, 5.0:float-32, 6.0:float-64-little, 1.0:float-size(b)>> = floats 
+  let assert <<1.0:16-float, 5.0:float-32, 6.0:float-64-little, 1.0:float-size(b)>> = floats
 }"#
     );
 }
@@ -161,3 +161,36 @@ fn unicode_bit_array_2() {
 }"#
     );
 }
+
+#[test]
+fn bit_array_literal_string_constant_is_treated_as_utf8() {
+    assert_erl!(
+        r#"
+const a = <<"hello", " ", "world">>
+pub fn main() { a }
+"#
+    );
+}
+
+#[test]
+fn bit_array_literal_string_is_treated_as_utf8() {
+    assert_erl!(
+        r#"
+pub fn main() {
+  <<"hello", " ", "world">>
+}"#
+    );
+}
+
+#[test]
+fn bit_array_literal_string_pattern_is_treated_as_utf8() {
+    assert_erl!(
+        r#"
+pub fn main() {
+  case <<>> {
+    <<"a", "b", _:bits>> -> 1
+    _ -> 2
+  }
+}"#
+    );
+}
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__bit_array_literal_string_constant_is_treated_as_utf8.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__bit_array_literal_string_constant_is_treated_as_utf8.snap
new file mode 100644
index 000000000..0bc0c4077
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__bit_array_literal_string_constant_is_treated_as_utf8.snap
@@ -0,0 +1,12 @@
+---
+source: compiler-core/src/erlang/tests/bit_arrays.rs
+expression: "\nconst a = <<\"hello\", \" \", \"world\">>\npub fn main() { a }\n"
+---
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([main/0]).
+
+-spec main() -> bitstring().
+main() ->
+    <<"hello"/utf8, " "/utf8, "world"/utf8>>.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__bit_array_literal_string_is_treated_as_utf8.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__bit_array_literal_string_is_treated_as_utf8.snap
new file mode 100644
index 000000000..0c302c9a0
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__bit_array_literal_string_is_treated_as_utf8.snap
@@ -0,0 +1,12 @@
+---
+source: compiler-core/src/erlang/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  <<\"hello\", \" \", \"world\">>\n}"
+---
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([main/0]).
+
+-spec main() -> bitstring().
+main() ->
+    <<"hello"/utf8, " "/utf8, "world"/utf8>>.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__bit_array_literal_string_pattern_is_treated_as_utf8.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__bit_array_literal_string_pattern_is_treated_as_utf8.snap
new file mode 100644
index 000000000..4d432c7aa
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__bit_array_literal_string_pattern_is_treated_as_utf8.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/erlang/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  case <<>> {\n    <<\"a\", \"b\", _:bits>> -> 1\n    _ -> 2\n  }\n}"
+---
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([main/0]).
+
+-spec main() -> integer().
+main() ->
+    case <<>> of
+        <<"a"/utf8, "b"/utf8, _/bitstring>> ->
+            1;
+
+        _ ->
+            2
+    end.
diff --git a/compiler-core/src/javascript/tests/bit_arrays.rs b/compiler-core/src/javascript/tests/bit_arrays.rs
index 66abfaf6c..0d1836b4a 100644
--- a/compiler-core/src/javascript/tests/bit_arrays.rs
+++ b/compiler-core/src/javascript/tests/bit_arrays.rs
@@ -583,3 +583,31 @@ fn go() {
 "#,
     );
 }
+
+#[test]
+fn bit_array_literal_string_constant_is_treated_as_utf8() {
+    assert_js!(r#"const a = <<"hello", " ", "world">>"#);
+}
+
+#[test]
+fn bit_array_literal_string_is_treated_as_utf8() {
+    assert_js!(
+        r#"
+pub fn main() {
+  <<"hello", " ", "world">>
+}"#
+    );
+}
+
+#[test]
+fn bit_array_literal_string_pattern_is_treated_as_utf8() {
+    assert_js!(
+        r#"
+pub fn main() {
+  case <<>> {
+    <<"a", "b", _:bytes>> -> 1
+    _ -> 2
+  }
+}"#
+    );
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_constant_is_treated_as_utf8.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_constant_is_treated_as_utf8.snap
new file mode 100644
index 000000000..68092bbfa
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_constant_is_treated_as_utf8.snap
@@ -0,0 +1,11 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "const a = <<\"hello\", \" \", \"world\">>"
+---
+import { toBitArray, stringBits } from "../gleam.mjs";
+
+const a = /* @__PURE__ */ toBitArray([
+  stringBits("hello"),
+  stringBits(" "),
+  stringBits("world"),
+]);
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_is_treated_as_utf8.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_is_treated_as_utf8.snap
new file mode 100644
index 000000000..1663e9305
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_is_treated_as_utf8.snap
@@ -0,0 +1,9 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  <<\"hello\", \" \", \"world\">>\n}"
+---
+import { toBitArray, stringBits } from "../gleam.mjs";
+
+export function main() {
+  return toBitArray([stringBits("hello"), stringBits(" "), stringBits("world")]);
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_pattern_is_treated_as_utf8.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_pattern_is_treated_as_utf8.snap
new file mode 100644
index 000000000..0329bf5d5
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_pattern_is_treated_as_utf8.snap
@@ -0,0 +1,14 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  case <<>> {\n    <<\"a\", \"b\", _:bytes>> -> 1\n    _ -> 2\n  }\n}"
+---
+import { toBitArray } from "../gleam.mjs";
+
+export function main() {
+  let $ = toBitArray([]);
+  if ($.byteAt(0) === 0x61 && $.byteAt(1) === 0x62 && $.length >= 2) {
+    return 1;
+  } else {
+    return 2;
+  }
+}