Repository: gleam-lang/gleam (rust)

Issue:
Incorrect Erlang code with block pattern during record update
<!--
Please include the following:
- Steps and code to reproduce the problem
- Versions for Gleam, OS, the runtime and any other software used
- Logs from running Gleam with the `GLEAM_LOG` environment variable set to `trace`
-->
Hi !

I found a funny bug when experimenting with Gleam 1.7.0.
The following code:

```gleam
pub type A {
  A(b: B)
}

pub type B {
  B(c: C)
}

pub type C {
  C(val: Int)
}

pub fn example(a: A) {
  A(..a, b: { 
    B(..a.b, c: { 
      C(..a.b.c, val: 0) 
    }) 
  })
}
```

Compiles incorrectly to:

```erlang
example(A) ->
    _record = A,
    {a, (_record@1 = erlang:element(2, A),
        {b, (_record@2 = erlang:element(2, erlang:element(2, A)),
            {c, 0})})}.
```

Which is invalid, the `(_record, {` pattern is not working, but when removing the block pattern, like this:

```gleam
pub fn example(a: A) {
  A(..a, b: 
    B(..a.b, c:  
      C(..a.b.c, val: 0) 
    ) 
  )
}
```

Compiles correctly to:

```erlang
example(A) ->
    _record = A,
    {a,
        begin
            _record@1 = erlang:element(2, A),
            {b,
                begin
                    _record@2 = erlang:element(2, erlang:element(2, A)),
                    {c, 0}
                end}
        end}.
```

It triggers a hard to understand build error looking like (because of a `,` error)

```
project/src/module.gleam: spec for undefined function example/1
%   32|   A(..a, b: B(..a.b, c: { C(..a.b.c, val: 0) }))
%     |  ^
```

---

Edit: I made sure it's working with JS: everything gets compiled as IIFE, so there's no problem.

Pull request:
Fix invalid erlang for record updates inside blocks
fix #4154

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3c9f0d9b..abc45ee38 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,3 +50,8 @@
 - Fixed a bug where build tool could fail to add new dependencies when
   dependencies with optional dependencies are present in the manifest.
   ([Louis Pilfold](https://github.com/lpil))
+
+- Fixed a bug where a block expression containing a singular record update would
+  produce invalid erlang.
+  ([yoshi](https://github.com/joshi-monster))
+  
diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs
index a2ac679be..c49d643e0 100644
--- a/compiler-core/src/ast.rs
+++ b/compiler-core/src/ast.rs
@@ -2377,13 +2377,6 @@ impl TypedStatement {
         }
     }
 
-    pub fn is_non_pipe_expression(&self) -> bool {
-        match self {
-            Statement::Expression(expression) => !expression.is_pipeline(),
-            _ => false,
-        }
-    }
-
     pub fn location(&self) -> SrcSpan {
         match self {
             Statement::Expression(expression) => expression.location(),
diff --git a/compiler-core/src/erlang.rs b/compiler-core/src/erlang.rs
index 2a93565de..936b8c5cd 100644
--- a/compiler-core/src/erlang.rs
+++ b/compiler-core/src/erlang.rs
@@ -944,8 +944,12 @@ where
 }
 
 fn block<'a>(statements: &'a Vec1<TypedStatement>, env: &mut Env<'a>) -> Document<'a> {
-    if statements.len() == 1 && statements.first().is_non_pipe_expression() {
-        return docvec!['(', statement(statements.first(), env), ')'];
+    if statements.len() == 1 {
+        if let Statement::Expression(expression) = statements.first() {
+            if !needs_begin_end_wrapping(expression) {
+                return docvec!['(', expr(expression, env), ')'];
+            }
+        }
     }
 
     let vars = env.current_scope_vars.clone();


Test patch:
diff --git a/compiler-core/src/erlang/tests/records.rs b/compiler-core/src/erlang/tests/records.rs
index d9c994959..89eb6f6f0 100644
--- a/compiler-core/src/erlang/tests/records.rs
+++ b/compiler-core/src/erlang/tests/records.rs
@@ -383,3 +383,20 @@ pub fn main() {
 }"
     );
 }
+
+#[test]
+fn nested_record_update_with_blocks() {
+    assert_erl!(
+        "pub type A { A(b: B) }
+pub type B { B(c: C) }
+pub type C { C(val: Int) }
+
+pub fn main(a: A) {
+    A(..a, b: {
+        B(..a.b, c: {
+            C(..a.b.c, val: 0)
+        })
+    })
+}"
+    )
+}
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__records__nested_record_update_with_blocks.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__records__nested_record_update_with_blocks.snap
new file mode 100644
index 000000000..783c6d18b
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__records__nested_record_update_with_blocks.snap
@@ -0,0 +1,43 @@
+---
+source: compiler-core/src/erlang/tests/records.rs
+expression: "pub type A { A(b: B) }\npub type B { B(c: C) }\npub type C { C(val: Int) }\n\npub fn main(a: A) {\n    A(..a, b: {\n        B(..a.b, c: {\n            C(..a.b.c, val: 0)\n        })\n    })\n}"
+---
+----- SOURCE CODE
+pub type A { A(b: B) }
+pub type B { B(c: C) }
+pub type C { C(val: Int) }
+
+pub fn main(a: A) {
+    A(..a, b: {
+        B(..a.b, c: {
+            C(..a.b.c, val: 0)
+        })
+    })
+}
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([main/1]).
+-export_type([a/0, b/0, c/0]).
+
+-type a() :: {a, b()}.
+
+-type b() :: {b, c()}.
+
+-type c() :: {c, integer()}.
+
+-file("/root/project/test/my/mod.gleam", 5).
+-spec main(a()) -> a().
+main(A) ->
+    _record = A,
+    {a,
+        begin
+            _record@1 = erlang:element(2, A),
+            {b,
+                begin
+                    _record@2 = erlang:element(2, erlang:element(2, A)),
+                    {c, 0}
+                end}
+        end}.