Skip to content

Commit

Permalink
Add support for Lua chunks
Browse files Browse the repository at this point in the history
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)
```
  • Loading branch information
davydog187 committed Aug 23, 2024
1 parent f58fe4e commit e607f40
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 14 deletions.
78 changes: 64 additions & 14 deletions lib/lua.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ defmodule Lua do

defstruct [:state]

alias Luerl.New, as: Luerl

alias Lua.Util

@default_sandbox [
Expand Down Expand Up @@ -74,25 +72,35 @@ defmodule Lua do
@doc """
Write Lua code that is parsed at compile-time.
iex> ~LUA"return 2 + 2"
"return 2 + 2"
If the code cannot be lexed and parsed, it raises a `Lua.CompilerException`
#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 """
Expand Down Expand Up @@ -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}}

Expand All @@ -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
Expand Down Expand Up @@ -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
10 changes: 10 additions & 0 deletions lib/lua/chunk.ex
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions test/lua_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -166,6 +174,15 @@ 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
lua = Lua.new()

assert {ref, lua} = :luerl_emul.load_chunk(chunk.instructions, lua.state)

assert {:ok, [4], _} = :luerl_new.call_chunk(ref, lua)
end

test "invalid functions raise" do
lua = Lua.new()

Expand Down

0 comments on commit e607f40

Please sign in to comment.