Repository: gleam-lang/gleam (rust)

Issue:
Unlabelled arguemnt after labelled argument error not being emitted for custom type variant constructor
```gleam
pub type Bad {
  Bad(labelled: Int, Float)
}
```

This code is invalid and should be rejected. Using this record will result in invalid code being generated also.

Pull request:
Disallow unlabelled arguments after labelled ones
This PR fixes #4331.
I found a JS codegen test which relied on this bug, so I have removed it.
Edit: There was also a language test which uses this invalid syntax, which has also been removed.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b72c38367..03080031f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,11 @@
 
 - Fixed a bug where tuples with atoms in the first position could be
   incorrectly formatted by `echo`.
+  ([Louis Pilfold](https://github.com/lpil))
+
+- Fixed a bug where unlabelled arguments would be allowed after labelled arguments
+  in variant constructor definitions.
+  ([Surya Rose](https://github.com/GearsDatapacks))
 
 ## v1.9.1 - 2025-03-10
 
diff --git a/compiler-core/src/analyse.rs b/compiler-core/src/analyse.rs
index a064018ef..c65ba480c 100644
--- a/compiler-core/src/analyse.rs
+++ b/compiler-core/src/analyse.rs
@@ -28,7 +28,7 @@ use crate::{
         environment::*,
         error::{Error, FeatureKind, MissingAnnotation, Named, Problems, convert_unify_error},
         expression::{ExprTyper, FunctionDefinition, Implementations},
-        fields::{FieldMap, FieldMapBuilder},
+        fields::FieldMapBuilder,
         hydrator::Hydrator,
         prelude::*,
     },
@@ -996,12 +996,16 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
                 continue;
             }
 
-            let mut field_map = FieldMap::new(constructor.arguments.len() as u32);
+            let mut field_map_builder = FieldMapBuilder::new(constructor.arguments.len() as u32);
             let mut args_types = Vec::with_capacity(constructor.arguments.len());
             let mut fields = Vec::with_capacity(constructor.arguments.len());
 
-            for (i, RecordConstructorArg { label, ast, .. }) in
-                constructor.arguments.iter().enumerate()
+            for RecordConstructorArg {
+                label,
+                ast,
+                location,
+                ..
+            } in constructor.arguments.iter()
             {
                 // Build a type from the annotation AST
                 let t = match hydrator.type_from_ast(ast, environment, &mut self.problems) {
@@ -1020,17 +1024,17 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
                 // Register the type for this parameter
                 args_types.push(t);
 
-                // Register the label for this parameter, if there is one
-                if let Some((location, label)) = label {
-                    if field_map.insert(label.clone(), i as u32).is_err() {
-                        self.problems.error(Error::DuplicateField {
-                            label: label.clone(),
-                            location: *location,
-                        });
-                    };
+                let (label_location, label) = match label {
+                    Some((location, label)) => (*location, Some(label)),
+                    None => (*location, None),
+                };
+
+                // Register the label for this parameter
+                if let Err(error) = field_map_builder.add(label, label_location) {
+                    self.problems.error(error);
                 }
             }
-            let field_map = field_map.into_option();
+            let field_map = field_map_builder.finish();
             // Insert constructor function into module scope
             let mut type_ = type_.deref().clone();
             type_.set_custom_type_variant(index as u16);


Test patch:
diff --git a/compiler-core/src/javascript/tests/custom_types.rs b/compiler-core/src/javascript/tests/custom_types.rs
index 19693f5ea..5c50f5b5a 100644
--- a/compiler-core/src/javascript/tests/custom_types.rs
+++ b/compiler-core/src/javascript/tests/custom_types.rs
@@ -311,25 +311,6 @@ fn go(cat) {
     )
 }
 
-#[test]
-fn destructure_custom_type_with_mixed_fields_second_unlabelled() {
-    assert_js!(
-        r#"
-type Cat {
-  Cat(name: String, Int)
-}
-
-fn go(cat) {
-  let Cat(x, y) = cat
-  let Cat(name: x, ..) = cat
-  let Cat(y, name: x) = cat
-  x
-}
-
-"#,
-    )
-}
-
 #[test]
 fn nested_pattern_with_labels() {
     assert_js!(
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__custom_types__destructure_custom_type_with_mixed_fields_second_unlabelled.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__custom_types__destructure_custom_type_with_mixed_fields_second_unlabelled.snap
deleted file mode 100644
index 927c8b3e3..000000000
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__custom_types__destructure_custom_type_with_mixed_fields_second_unlabelled.snap
+++ /dev/null
@@ -1,38 +0,0 @@
----
-source: compiler-core/src/javascript/tests/custom_types.rs
-expression: "\ntype Cat {\n  Cat(name: String, Int)\n}\n\nfn go(cat) {\n  let Cat(x, y) = cat\n  let Cat(name: x, ..) = cat\n  let Cat(y, name: x) = cat\n  x\n}\n\n"
----
------ SOURCE CODE
-
-type Cat {
-  Cat(name: String, Int)
-}
-
-fn go(cat) {
-  let Cat(x, y) = cat
-  let Cat(name: x, ..) = cat
-  let Cat(y, name: x) = cat
-  x
-}
-
-
-
------ COMPILED JAVASCRIPT
-import { CustomType as $CustomType } from "../gleam.mjs";
-
-class Cat extends $CustomType {
-  constructor(name, x1) {
-    super();
-    this.name = name;
-    this[1] = x1;
-  }
-}
-
-function go(cat) {
-  let x = cat.name;
-  let y = cat[1];
-  let x$1 = cat.name;
-  let x$2 = cat.name;
-  let y$1 = cat[1];
-  return x$2;
-}
diff --git a/compiler-core/src/type_/snapshots/gleam_core__type___tests__unlabelled_argument_not_allowed_after_labelled_argument.snap b/compiler-core/src/type_/snapshots/gleam_core__type___tests__unlabelled_argument_not_allowed_after_labelled_argument.snap
new file mode 100644
index 000000000..541b675f2
--- /dev/null
+++ b/compiler-core/src/type_/snapshots/gleam_core__type___tests__unlabelled_argument_not_allowed_after_labelled_argument.snap
@@ -0,0 +1,19 @@
+---
+source: compiler-core/src/type_/tests.rs
+expression: "\npub type Bad {\n  Bad(labelled: Int, Float)\n}\n"
+---
+----- SOURCE CODE
+
+pub type Bad {
+  Bad(labelled: Int, Float)
+}
+
+
+----- ERROR
+error: Unlabelled argument after labelled argument
+  ┌─ /src/one/two.gleam:3:22
+  │
+3 │   Bad(labelled: Int, Float)
+  │                      ^^^^^
+
+All unlabelled arguments must come before any labelled arguments.
diff --git a/compiler-core/src/type_/tests.rs b/compiler-core/src/type_/tests.rs
index 33e9b0a5e..65e964700 100644
--- a/compiler-core/src/type_/tests.rs
+++ b/compiler-core/src/type_/tests.rs
@@ -3080,3 +3080,14 @@ type Wibble {
 "
     );
 }
+
+#[test]
+fn unlabelled_argument_not_allowed_after_labelled_argument() {
+    assert_module_error!(
+        "
+pub type Bad {
+  Bad(labelled: Int, Float)
+}
+"
+    );
+}
diff --git a/test/language/test/language_test.gleam b/test/language/test/language_test.gleam
index 0f078c45c..7665f786a 100644
--- a/test/language/test/language_test.gleam
+++ b/test/language/test/language_test.gleam
@@ -2143,10 +2143,6 @@ type NestedCat {
   NestedCat(Cat, String, cuteness: Int)
 }
 
-type InverseCat {
-  InverseCat(cuteness: Int, String)
-}
-
 fn mixed_arg_match_tests() {
   [
     "matching second labelled arg as first"
@@ -2170,16 +2166,6 @@ fn mixed_arg_match_tests() {
           NestedCat(Cat("fluffy", 10), "gleamy", 100)
         assert_equal(#(x, y, y2), #("fluffy", 10, 100))
       }),
-    "matching first labelled arg as first"
-      |> example(fn() {
-        let InverseCat(cuteness: y, ..) = InverseCat(10, "fluffy")
-        assert_equal(y, 10)
-      }),
-    "matching first labelled arg as second"
-      |> example(fn() {
-        let InverseCat(x, cuteness: y) = InverseCat(10, "fluffy")
-        assert_equal(#(x, y), #("fluffy", 10))
-      }),
   ]
 }