Repository: tv-labs/lua (elixir)

Issue:
Consider LUA sigil turning script into chunk
I think that given that chunks are portable and don't need a state, it might be interesting to use luerl to turn ~LUA usages into Lua chunks. After all, it's inevitable that they will be loaded at some point, so why not have them converted to chunks already?

Pull request:
Add support for Lua chunks
Adds chunk support, which allows users to precompile Lua statements and then execute them later, as opposed re-parsing and code generating at eval time. We achieve this with the following two features

1. The ability to return chunks from the Lua sigil with the 'c' modifier

```elixir
~LUA"""
print("hello world")
"""c
```

2. The ability to load and evaluate chunks

```elixir
{[4], _} = Lua.eval!(~LUA[return 2 + 2]c)
```

Closes #35 

Patch:
diff --git a/lib/lua.ex b/lib/lua.ex
index 0cf7d32..3bac632 100644
--- a/lib/lua.ex
+++ b/lib/lua.ex
@@ -9,8 +9,6 @@ defmodule Lua do
 
   defstruct [:state]
 
-  alias Luerl.New, as: Luerl
-
   alias Lua.Util
 
   @default_sandbox [
@@ -74,7 +72,6 @@ defmodule Lua do
   @doc """
   Write Lua code that is parsed at compile-time.
 
-
       iex> ~LUA"return 2 + 2"
       "return 2 + 2"
 
@@ -82,17 +79,28 @@ defmodule Lua do
 
       #iex> ~LUA":not_lua"
       ** (Lua.CompilerException) Failed to compile Lua!
+
+  As an optimization, the `c` modifier can be used to return a pre-compiled Lua chunk
+
+      iex> ~LUA"return 2 + 2"c
   """
-  defmacro sigil_LUA(code, _opts) do
+  defmacro sigil_LUA(code, opts) do
     code =
       case code do
         {:<<>>, _, [literal]} -> literal
         _ -> raise "~Lua only accepts string literals, received:\n\n#{Macro.to_string(code)}"
       end
 
-    raise_on_invalid_lua!(code)
+    chunk =
+      case :luerl_comp.string(code, [:return]) do
+        {:ok, chunk} -> %Lua.Chunk{instructions: chunk}
+        {:error, error, _warnings} -> raise Lua.CompilerException, error
+      end
 
-    code
+    case opts do
+      [?c] -> Macro.escape(chunk)
+      _ -> code
+    end
   end
 
   @doc """
@@ -230,11 +238,44 @@ defmodule Lua do
 
       iex> {[42], _} = Lua.eval!(Lua.new(), "return 42")
 
+
+  `eval!/2` can also evaluate chunks by passing instead of a script. As a
+  peformance optimization, it is recommended to call `load_chunk/2` if you
+  will be executing a chunk many times, but it is not necessary.
+
+      iex> {[4], _} = Lua.eval!(~LUA[return 2 + 2]c)
+
   """
   def eval!(state \\ new(), script)
 
   def eval!(%__MODULE__{state: state} = lua, script) when is_binary(script) do
-    case Luerl.do_dec(state, script) do
+    case :luerl_new.do_dec(script, state) do
+      {:ok, result, new_state} ->
+        {result, %__MODULE__{lua | state: new_state}}
+
+      {:lua_error, _e, _state} = error ->
+        raise Lua.RuntimeException, error
+
+      {:error, [error | _], _} ->
+        raise Lua.CompilerException, error
+    end
+  rescue
+    e in [UndefinedFunctionError] ->
+      reraise Lua.RuntimeException,
+              Util.format_function([e.module, e.function], e.arity),
+              __STACKTRACE__
+
+    e in [Lua.RuntimeException, Lua.CompilerException] ->
+      reraise e, __STACKTRACE__
+
+    e ->
+      reraise Lua.RuntimeException, e, __STACKTRACE__
+  end
+
+  def eval!(%__MODULE__{} = lua, %Lua.Chunk{} = chunk) do
+    {chunk, lua} = load_chunk(lua, chunk)
+
+    case :luerl_new.call_chunk(chunk.ref, lua.state) do
       {:ok, result, new_state} ->
         {result, %__MODULE__{lua | state: new_state}}
 
@@ -257,6 +298,22 @@ defmodule Lua do
       reraise Lua.RuntimeException, e, __STACKTRACE__
   end
 
+  @doc """
+  Similar to `:luerl_new.load/3`, it takes a `t:Lua.Chunk.t`, and loads
+  it into state so that it can be evaluated via `eval!/2`
+
+      iex> {%Lua.Chunk{}, %Lua{}} = Lua.load_chunk(Lua.new(), ~LUA[return 2 + 2]c)
+  """
+  def load_chunk(%__MODULE__{state: state} = lua, %Lua.Chunk{ref: nil} = chunk) do
+    {ref, state} = :luerl_emul.load_chunk(chunk.instructions, state)
+
+    {%Lua.Chunk{chunk | ref: ref}, %__MODULE__{lua | state: state}}
+  end
+
+  def load_chunk(%__MODULE__{} = lua, %Lua.Chunk{} = chunk) do
+    {chunk, lua}
+  end
+
   @doc """
   Calls a function in Lua's state
 
@@ -455,11 +512,4 @@ defmodule Lua do
   end
 
   defp wrap(state), do: %__MODULE__{state: state}
-
-  defp raise_on_invalid_lua!(code) when is_binary(code) do
-    case :luerl_comp.string(code, [:return]) do
-      {:ok, _chunk} -> :ok
-      {:error, error, _warnings} -> raise Lua.CompilerException, error
-    end
-  end
 end
diff --git a/lib/lua/chunk.ex b/lib/lua/chunk.ex
new file mode 100644
index 0000000..8223e62
--- /dev/null
+++ b/lib/lua/chunk.ex
@@ -0,0 +1,10 @@
+defmodule Lua.Chunk do
+  @moduledoc """
+  A pre-compiled [chunk](https://www.lua.org/pil/1.1.html) of Lua code
+  that can be executed at a future point in time
+  """
+
+  @type t :: %__MODULE__{}
+
+  defstruct [:instructions, :ref]
+end


Test patch:
diff --git a/test/lua_test.exs b/test/lua_test.exs
index cbfba39..6990e46 100644
--- a/test/lua_test.exs
+++ b/test/lua_test.exs
@@ -42,6 +42,14 @@ defmodule LuaTest do
       end
     end
 
+    test "it returns the Lua string by default" do
+      assert ~LUA[print("hello")] == ~S[print("hello")]
+    end
+
+    test "it returns chunks with the ~LUA c option" do
+      assert %Lua.Chunk{} = ~LUA[print("hello")]c
+    end
+
     test "it can handle multi-line programs" do
       message = """
       Failed to compile Lua!
@@ -166,6 +174,12 @@ defmodule LuaTest do
       assert {[2], %Lua{}} = Lua.eval!(lua, "return foo(1)")
     end
 
+    test "it can evaluate chunks" do
+      assert %Lua.Chunk{} = chunk = ~LUA[return 2 + 2]c
+
+      assert {[4], _} = Lua.eval!(chunk)
+    end
+
     test "invalid functions raise" do
       lua = Lua.new()
 
@@ -206,6 +220,23 @@ defmodule LuaTest do
     end
   end
 
+  describe "load_chunk/2" do
+    test "loads a chunk into state" do
+      assert %Lua.Chunk{ref: nil} = chunk = ~LUA[print("hello")]c
+      assert {%Lua.Chunk{} = chunk, %Lua{}} = Lua.load_chunk(Lua.new(), chunk)
+      assert chunk.ref
+    end
+
+    test "chunks can be loaded multiple times" do
+      lua = Lua.new()
+      chunk = ~LUA[print("hello")]c
+
+      assert {chunk, lua} = Lua.load_chunk(lua, chunk)
+      assert {chunk, lua} = Lua.load_chunk(lua, chunk)
+      assert {_chunk, _lua} = Lua.load_chunk(lua, chunk)
+    end
+  end
+
   describe "call_function/3" do
     test "can call standard library functions" do
       assert {["hello robert"], %Lua{}} =