Repository: gleam-lang/gleam (rust)

Issue:
Invalid Erlang generated if a public @external function has an argument starting with an underscore
See title. It looks like the compiler automatically generates a wrapper in that specific case for Erlang, but uses the variable names from the Gleam definition. If the Gleam variable is ignored and starts with an underscore, a single underscore is emitted in Erlang as the variable name. Javascript is fine because it just re-exports the function. I prefixed the argument with an underscore to get rid of the "unused variable" warning for the default implementation.

```gleam
@external(erlang, "erlang", "display")
pub fn log(_ignored: a) -> Nil {
  // contrived; do nothing for javascript
  Nil
}
```

produces

```erl
-module(bug).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).

-export([log/1]).

-file("/home/yoshi/Projects/probe/bug/src/bug.gleam", 1).
-spec log(any()) -> nil.
log(_) ->
    erlang:display(_).

```

and 

```
/home/yoshi/Projects/probe/bug/src/bug.gleam:5:20: variable '_' is unbound
%   5| }
%     |  ^^^^^^^^^^^^^^^^^^^

error: Shell command failure

There was a problem when running the shell command `escript`.
```

Gleam 1.5.1 :purple_heart: 

Pull request description:
Fix code generation for external functions with ignored arguments
This PR fixes #3680

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae55c92d0..dd3ea23b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -92,6 +92,10 @@
   note on how to have a field accessor only if it makes sense.
   ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
 
+- Fixes a bug where incorrect code would be generated for external function on
+  the Erlang target if any of their arguments were discarded.
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
+
 ### Formatter
 
 ### Language Server
diff --git a/compiler-core/src/erlang.rs b/compiler-core/src/erlang.rs
index 5301c1252..66b6b6d87 100644
--- a/compiler-core/src/erlang.rs
+++ b/compiler-core/src/erlang.rs
@@ -405,7 +405,11 @@ fn module_function<'a>(
     let return_spec = type_printer.print(&function.return_type);
 
     let spec = fun_spec(function_name, args_spec, return_spec);
-    let arguments = fun_args(&function.arguments, &mut env);
+    let arguments = if function.external_erlang.is_some() {
+        external_fun_args(&function.arguments, &mut env)
+    } else {
+        fun_args(&function.arguments, &mut env)
+    };
 
     let body = function
         .external_erlang
@@ -442,6 +446,22 @@ fn file_attribute<'a>(
     docvec!["-file(\"", path, "\", ", line, ")."]
 }
 
+fn external_fun_args<'a>(args: &'a [TypedArg], env: &mut Env<'a>) -> Document<'a> {
+    wrap_args(args.iter().map(|a| {
+        let name = match &a.names {
+            ArgNames::Discard { name, .. }
+            | ArgNames::LabelledDiscard { name, .. }
+            | ArgNames::Named { name, .. }
+            | ArgNames::NamedLabelled { name, .. } => name,
+        };
+        if name.chars().all(|c| c == '_') {
+            env.next_local_var_name(&format!("{name}ignored"))
+        } else {
+            env.next_local_var_name(name)
+        }
+    }))
+}
+
 fn fun_args<'a>(args: &'a [TypedArg], env: &mut Env<'a>) -> Document<'a> {
     wrap_args(args.iter().map(|a| match &a.names {
         ArgNames::Discard { .. } | ArgNames::LabelledDiscard { .. } => "_".to_doc(),


Test patch:
diff --git a/compiler-core/src/erlang/tests/external_fn.rs b/compiler-core/src/erlang/tests/external_fn.rs
index f4577bc51..2c8591283 100644
--- a/compiler-core/src/erlang/tests/external_fn.rs
+++ b/compiler-core/src/erlang/tests/external_fn.rs
@@ -348,3 +348,33 @@ pub fn main() {
 "#
     );
 }
+
+#[test]
+fn discarded_arg_in_external_are_passed_correctly() {
+    assert_erl!(
+        r#"
+@external(erlang, "wibble", "wobble")
+pub fn woo(_a: a) -> Nil
+"#
+    );
+}
+
+#[test]
+fn multiple_discarded_args_in_external_are_passed_correctly() {
+    assert_erl!(
+        r#"
+@external(erlang, "wibble", "wobble")
+pub fn woo(_: a, _: b) -> Nil
+"#
+    );
+}
+
+#[test]
+fn multiple_discarded_args_in_external_are_passed_correctly_2() {
+    assert_erl!(
+        r#"
+@external(erlang, "wibble", "wobble")
+pub fn woo(__: a, _two: b) -> Nil
+"#
+    );
+}
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__external_fn__discarded_arg_in_external_are_passed_correctly.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__external_fn__discarded_arg_in_external_are_passed_correctly.snap
new file mode 100644
index 000000000..c3ac0f4ba
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__external_fn__discarded_arg_in_external_are_passed_correctly.snap
@@ -0,0 +1,13 @@
+---
+source: compiler-core/src/erlang/tests/external_fn.rs
+expression: "\n@external(erlang, \"wibble\", \"wobble\")\npub fn woo(_a: a) -> Nil\n"
+---
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([woo/1]).
+
+-file("/root/project/test/my/mod.gleam", 3).
+-spec woo(any()) -> nil.
+woo(_a) ->
+    wibble:wobble(_a).
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__external_fn__multiple_discarded_args_in_external_are_passed_correctly.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__external_fn__multiple_discarded_args_in_external_are_passed_correctly.snap
new file mode 100644
index 000000000..1c80e6308
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__external_fn__multiple_discarded_args_in_external_are_passed_correctly.snap
@@ -0,0 +1,13 @@
+---
+source: compiler-core/src/erlang/tests/external_fn.rs
+expression: "\n@external(erlang, \"wibble\", \"wobble\")\npub fn woo(_: a, _: b) -> Nil\n"
+---
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([woo/2]).
+
+-file("/root/project/test/my/mod.gleam", 3).
+-spec woo(any(), any()) -> nil.
+woo(_ignored, _ignored@1) ->
+    wibble:wobble(_ignored, _ignored@1).
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__external_fn__multiple_discarded_args_in_external_are_passed_correctly_2.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__external_fn__multiple_discarded_args_in_external_are_passed_correctly_2.snap
new file mode 100644
index 000000000..892f57eb4
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__external_fn__multiple_discarded_args_in_external_are_passed_correctly_2.snap
@@ -0,0 +1,13 @@
+---
+source: compiler-core/src/erlang/tests/external_fn.rs
+expression: "\n@external(erlang, \"wibble\", \"wobble\")\npub fn woo(__: a, _two: b) -> Nil\n"
+---
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([woo/2]).
+
+-file("/root/project/test/my/mod.gleam", 3).
+-spec woo(any(), any()) -> nil.
+woo(__ignored, _two) ->
+    wibble:wobble(__ignored, _two).
diff --git a/compiler-core/src/javascript/tests/externals.rs b/compiler-core/src/javascript/tests/externals.rs
index dc5ce246d..7742c44ef 100644
--- a/compiler-core/src/javascript/tests/externals.rs
+++ b/compiler-core/src/javascript/tests/externals.rs
@@ -313,3 +313,13 @@ pub fn should_not_be_generated() {
 "#
     );
 }
+
+#[test]
+fn discarded_names_in_external_are_passed_correctly() {
+    assert_js!(
+        r#"
+@external(javascript, "wibble", "wobble")
+pub fn woo(_ignored: a) -> Nil
+"#
+    );
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__externals__discarded_names_in_external_are_passed_correctly.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__externals__discarded_names_in_external_are_passed_correctly.snap
new file mode 100644
index 000000000..1df1df470
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__externals__discarded_names_in_external_are_passed_correctly.snap
@@ -0,0 +1,7 @@
+---
+source: compiler-core/src/javascript/tests/externals.rs
+expression: "\n@external(javascript, \"wibble\", \"wobble\")\npub fn woo(_ignored: a) -> Nil\n"
+---
+import { wobble as woo } from "wibble";
+
+export { woo };