Skip to content

Commit

Permalink
Add support for Lua chunks (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
davydog187 authored Aug 23, 2024
1 parent f58fe4e commit fd1368e
Show file tree
Hide file tree
Showing 3 changed files with 105 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
31 changes: 31 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,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()

Expand Down Expand Up @@ -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{}} =
Expand Down

0 comments on commit fd1368e

Please sign in to comment.