Repository: tv-labs/lua
Pull request #81 (2025-05-15 13:37:27)

feat: allow guards in deflua functions
Adds the ability for guards to be used with `deflua` functions. Since `deflua` uses some creative syntax for passing the state separately (we do this so we can know which are function arguments and which are not), guards previously would cause the AST to be invalid.

We now rewrite the AST correctly, fixing this issue


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11a9e75..091373a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## Unreleased
+
+### Fixed
+- `deflua` function can now specify guards when using or not using state
+
 ## [v0.2.1] - 2025-05-14
 
 ### Added
diff --git a/lib/lua/api.ex b/lib/lua/api.ex
index 4310676..160942a 100644
--- a/lib/lua/api.ex
+++ b/lib/lua/api.ex
@@ -122,7 +122,7 @@ defmodule Lua.API do
   @doc """
   Defines a function that can be exposed in Lua through `Lua.load_api/3`
 
-      deflua add_two(number) do
+      deflua add_two(number) when is_number(number) do
         number + 2
       end
 
@@ -143,6 +143,18 @@ defmodule Lua.API do
         {[], Lua.set!(lua, [key])}
       end
 
+  ## Using guards
+
+  Since `deflua` uses non-conventional syntax to receive the current state, make sure
+  you specifiy the `when` clause and guards first, e.g.
+
+      deflua set_int(key, value) when is_integer(value), state do
+        # Return nothing but modify the state
+        {[], Lua.set!(lua, [key])}
+      end
+
+  Specifyiing the `when` cluase and guards last will result in a confusing error message.
+
   ## Variadic functions
 
   Technically, all Lua functions are variadic, which means they can receive
@@ -164,13 +176,20 @@ defmodule Lua.API do
 
   """
   defmacro deflua(fa, state, rest) do
+    name =
+      case fa do
+        {:when, _, [{name, _, _} | _]} -> name
+        {name, _, _} -> name
+      end
+
     {fa, _acc} =
-      Macro.prewalk(fa, false, fn
-        {name, context, args}, false -> {{name, context, args ++ List.wrap(state)}, true}
-        ast, true -> {ast, true}
-      end)
+      Macro.prewalk(fa, :ok, fn
+        {^name, context, args}, acc ->
+          {{name, context, args ++ List.wrap(state)}, acc}
 
-    {name, _, _} = fa
+        ast, acc ->
+          {ast, acc}
+      end)
 
     quote do
       @lua_function validate_func!(


Test patch:
diff --git a/test/lua/api_test.exs b/test/lua/api_test.exs
index 3e16f59..3729732 100644
--- a/test/lua/api_test.exs
+++ b/test/lua/api_test.exs
@@ -299,5 +299,41 @@ defmodule Lua.APITest do
       assert module.fail(1, 2) == 3
       assert module.fail(1, "2") == "rescued"
     end
+
+    test "deflua functions can have guards" do
+      assert [{module, _}] =
+               Code.compile_string("""
+               defmodule WithGuards do
+                 use Lua.API
+
+                 deflua has_a_guard(a) when is_integer(a) do
+                   a
+                 end
+
+                 deflua has_a_guard(a) when is_binary(a) and not is_boolean(a) do
+                   a
+                 end
+
+                 deflua has_a_guard(_) do
+                   "not a int"
+                 end
+
+                 deflua with_state(a) when is_integer(a), state do
+                   {a, state}
+                 end
+
+                 deflua with_state(_), state do
+                   {"not a int", state}
+                 end
+               end
+               """)
+
+      assert module.has_a_guard(1) == 1
+      assert module.has_a_guard("foo") == "foo"
+      assert module.has_a_guard(true) == "not a int"
+
+      assert {1, _} = module.with_state(1, Lua.new())
+      assert {"not a int", _} = module.with_state(true, Lua.new())
+    end
   end
 end