external_resource = "README.md" defmodule Lua do @moduledoc external_resource |> File.read!() |> String.split("") |> Enum.fetch!(1) alias Lua.Util alias Lua.VM.AssertionError alias Lua.VM.Display alias Lua.VM.Executor alias Lua.VM.InternalError alias Lua.VM.RuntimeError alias Lua.VM.State alias Lua.VM.Table alias Lua.VM.TypeError alias Lua.VM.Value @external_resource external_resource @type t :: %__MODULE__{} # Compiler.compile/1 currently always succeeds but the spec allows {:error, _} # for future-proofing. Suppress dialyzer warnings on those defensive branches. @dialyzer {:no_match, eval!: 2, eval!: 3, parse_chunk: 1} defstruct [:state, debug: false] @default_sandbox [ [:io, :stdin], [:io, :stdout], [:io, :stderr], [:io, :read], [:io, :write], [:io, :open], [:io, :close], [:io, :lines], [:io, :popen], [:io, :tmpfile], [:io, :output], [:io, :input], [:io, :flush], [:io, :type], [:file], [:os, :execute], [:os, :exit], [:os, :getenv], [:os, :remove], [:os, :rename], [:os, :tmpname], [:package], [:load], [:loadfile], [:require], [:dofile], [:loadstring] ] defimpl Inspect do def inspect(_lua, _opts) do "#Lua<>" end end @doc """ Initializes a Lua VM sandbox iex> Lua.new() By default, the following Lua functions are sandboxed. #{Enum.map_join(@default_sandbox, "\n", fn func -> "* `#{inspect(func)}`" end)} To disable, use the `sandboxed` option, passing an empty list iex> Lua.new(sandboxed: []) Alternatively, you can pass your own list of functions to sandbox. This is equivalent to calling `Lua.sandbox/2`. iex> Lua.new(sandboxed: [[:os, :exit]]) ## Options * `:sandboxed` - list of paths to be sandboxed, e.g. `sandboxed: [[:require], [:os, :exit]]` * `:exclude` - list of paths to exclude from the sandbox, e.g. `exclude: [[:require], [:package]]` * `:debug` - (default `false`) when `true`, internal Lua VM frames are preserved in stack traces instead of being pruned. Useful when debugging library bugs. * `:max_call_depth` - (default `:infinity`) caps the depth of nested function calls. When a script recurses deeper than this, a catchable `"stack overflow"` runtime error is raised instead of letting the recursion exhaust the host process. Accepts a positive integer or `:infinity` for no limit. Note: this VM does not implement proper tail-call optimization, so a call in tail position (`return f(x)`) consumes a frame like any other call. A finite `:max_call_depth` therefore bounds tail recursion too — including loops that PUC-Lua would run indefinitely. Leave the default `:infinity` if you rely on unbounded tail recursion. iex> lua = Lua.new(max_call_depth: 10) iex> {[false, message], _lua} = Lua.eval!(lua, "local function f() return f() end return pcall(f)") iex> message =~ "stack overflow" true * `:max_string_bytes` - (default 256 MiB) ceiling for any single string the VM will build, whether via `..`, `string.rep`, or a `load` reader. An oversized result raises a catchable `"resulting string too large"` error — the size is computed *before* allocating, so the bomb is refused rather than detected after the fact. Accepts a positive integer or `:infinity` for no limit (matching `:max_call_depth` and `:max_instructions`). When running the VM inside a process capped with `:max_heap_size`, set this comfortably below the heap cap: the default ceiling permits strings large enough to trip a smaller heap cap mid-allocation, where the kill depends on garbage-collection timing rather than a deterministic refusal. iex> lua = Lua.new(max_string_bytes: 1024) iex> {[false, message], _lua} = Lua.eval!(lua, "return pcall(string.rep, 'x', 2048)") iex> message =~ "resulting string too large" true * `:max_instructions` - (default `:infinity`) caps the number of VM instructions a single evaluation may execute. When a script exceeds this budget, a catchable `"instruction budget exceeded"` runtime error is raised, giving library consumers a deterministic CPU bound without wrapping each call in a host `Task` + wall-clock timeout. Accepts a positive integer or `:infinity` for no limit. The budget is enforced at loop back-edges and call boundaries, so the default `:infinity` path carries no per-instruction cost. The budget is fresh per top-level evaluation and recoverable via `pcall`. iex> lua = Lua.new(max_instructions: 1000) iex> {[false, message], _lua} = Lua.eval!(lua, "return pcall(function() while true do end end)") iex> message =~ "instruction budget exceeded" true """ @spec new(keyword()) :: t() def new(opts \\ []) do opts = Keyword.validate!(opts, sandboxed: @default_sandbox, exclude: [], debug: false, max_call_depth: :infinity, max_string_bytes: Lua.VM.Limits.max_string_bytes(), max_instructions: :infinity ) exclude = Keyword.fetch!(opts, :exclude) debug = Keyword.fetch!(opts, :debug) max_call_depth = validate_max_call_depth!(Keyword.fetch!(opts, :max_call_depth)) max_string_bytes = validate_max_string_bytes!(Keyword.fetch!(opts, :max_string_bytes)) max_instructions = validate_max_instructions!(Keyword.fetch!(opts, :max_instructions)) state = %{ Lua.VM.Stdlib.install(State.new()) | max_call_depth: max_call_depth, max_string_bytes: max_string_bytes, max_instructions: max_instructions } opts |> Keyword.fetch!(:sandboxed) |> Enum.reject(fn path -> path in exclude end) |> Enum.reduce(%__MODULE__{state: state, debug: debug}, &sandbox(&2, &1)) end defp validate_max_call_depth!(:infinity), do: :infinity defp validate_max_call_depth!(depth) when is_integer(depth) and depth > 0, do: depth defp validate_max_call_depth!(other) do raise ArgumentError, ":max_call_depth must be a positive integer or :infinity, got: #{inspect(other)}" end defp validate_max_string_bytes!(:infinity), do: :infinity defp validate_max_string_bytes!(bytes) when is_integer(bytes) and bytes > 0, do: bytes defp validate_max_string_bytes!(other) do raise ArgumentError, ":max_string_bytes must be a positive integer or :infinity, got: #{inspect(other)}" end defp validate_max_instructions!(:infinity), do: :infinity defp validate_max_instructions!(instruction_count) when is_integer(instruction_count) and instruction_count > 0, do: instruction_count defp validate_max_instructions!(other) do raise ArgumentError, ":max_instructions must be a positive integer or :infinity, got: #{inspect(other)}" end @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 code = case code do {:<<>>, _, [literal]} -> literal _ -> raise "~Lua only accepts string literals, received:\n\n#{Macro.to_string(code)}" end chunk = case Lua.Parser.parse(code) do {:ok, ast} -> proto = Lua.Compiler.compile!(ast) %Lua.Chunk{prototype: proto} {:error, msg} -> raise Lua.CompilerException, msg end case opts do [?c] -> Macro.escape(chunk) _ -> code end end @doc """ Sandboxes the given path, swapping out the implementation with a function that raises when called iex> lua = Lua.new(sandboxed: []) iex> Lua.sandbox(lua, [:os, :exit]) """ @spec sandbox(t(), [atom() | String.t()]) :: t() def sandbox(lua, path) do set!(lua, path, fn args -> raise Lua.RuntimeException, "#{Util.format_function(path, Enum.count(args))} is sandboxed" end) end @doc """ Sets the path patterns that the VM will look in when requiring Lua scripts. For example, if you store Lua files in your application's priv directory: #iex> lua = Lua.new(exclude: [[:package], [:require]]) #iex> Lua.set_lua_paths(lua, ["myapp/priv/lua/?.lua", "myapp/lua/?/init.lua"]) Now you can use the [Lua require](https://www.lua.org/pil/8.1.html) function to import these scripts > #### Warning {: .warning} > In order to use `Lua.set_lua_paths/2`, the following functions cannot be sandboxed: > * `[:package]` > * `[:require]` > > By default these are sandboxed, see the `:exclude` option in `Lua.new/1` to allow them. """ @spec set_lua_paths(t(), [String.t()] | String.t()) :: t() def set_lua_paths(%__MODULE__{} = lua, paths) when is_list(paths) do set_lua_paths(lua, Enum.join(paths, ";")) end def set_lua_paths(%__MODULE__{} = lua, paths) when is_binary(paths) do set!(lua, ["package", "path"], paths) end @doc """ Sets a table value in Lua. Nested keys will allocate intermediate tables iex> Lua.set!(Lua.new(), [:hello], "World") It can also set nested values iex> Lua.set!(Lua.new(), [:a, :b, :c], []) These table values are availble in Lua scripts iex> lua = Lua.set!(Lua.new(), [:a, :b, :c], "nested!") iex> {result, _} = Lua.eval!(lua, "return a.b.c") iex> result ["nested!"] `Lua.set!/3` can also be used to expose Elixir functions iex> lua = Lua.set!(Lua.new(), [:sum], fn args -> [Enum.sum(args)] end) iex> {[10], _lua} = Lua.eval!(lua, "return sum(1, 2, 3, 4)") Functions can also take a second argument for the state of Lua iex> lua = ...> Lua.set!(Lua.new(), [:set_count], fn args, state -> ...> {[], Lua.set!(state, :count, Enum.count(args))} ...> end) iex> {[3], _} = Lua.eval!(lua, "set_count(1, 2, 3); return count") """ @spec set!(t(), atom() | String.t() | [atom() | String.t()], term()) :: t() def set!(%__MODULE__{}, [], _) do raise Lua.RuntimeException, "Lua.set!/3 cannot have empty keys" end def set!(%__MODULE__{} = lua, keys, func) when is_function(func, 1) or is_function(func, 2) do keys = keys |> List.wrap() |> Enum.map(&to_lua_key/1) {function_name, scope} = List.pop_at(keys, -1) wrapped = wrap_callback(func, function_name, scope) state = do_set_nested(lua.state, keys, wrapped) %{lua | state: state} end def set!(%__MODULE__{} = lua, keys, value) do keys = keys |> List.wrap() |> Enum.map(&to_lua_key/1) value = Display.unwrap(value) {function_name, scope} = List.pop_at(keys, -1) {encoded, state} = if Util.encoded?(value) do {value, lua.state} else Value.encode(value, lua.state, &wrap_callback(&1, function_name, scope)) end state = do_set_nested(state, keys, encoded) %{lua | state: state} end # Sets a value at a nested key path, auto-allocating intermediate tables defp do_set_nested(state, [key], value) do State.set_global(state, key, value) end defp do_set_nested(state, [first | rest], value) do # Get or allocate the table at the first key case State.get_global(state, first) do {:tref, _} = tref -> state = set_in_table(state, tref, rest, value) state nil -> # Allocate intermediate table and recurse {tref, state} = State.alloc_table(state) state = State.set_global(state, first, tref) set_in_table(state, tref, rest, value) _other -> raise Lua.RuntimeException, {:lua_error, {:illegal_index, nil, Enum.join([first | rest], ".")}, state} end end # Sets a value inside nested tables, creating intermediates as needed defp set_in_table(state, tref, [key], value) do State.update_table(state, tref, fn table -> Table.put(table, key, value) end) end defp set_in_table(state, tref, [key | rest], value) do table = State.get_table(state, tref) case Map.get(table.data, key) do {:tref, _} = child_tref -> set_in_table(state, child_tref, rest, value) nil -> {child_tref, state} = State.alloc_table(state) state = State.update_table(state, tref, fn table -> Table.put(table, key, child_tref) end) set_in_table(state, child_tref, rest, value) _other -> raise Lua.RuntimeException, {:lua_error, {:illegal_index, nil, Enum.join([key | rest], ".")}, state} end end @doc """ Gets a table value in Lua iex> state = Lua.set!(Lua.new(), [:hello], "world") iex> Lua.get!(state, [:hello]) "world" When a value doesn't exist, it returns nil iex> Lua.get!(Lua.new(), [:nope]) nil It can also get nested values iex> state = Lua.set!(Lua.new(), [:a, :b, :c], "nested") iex> Lua.get!(state, [:a, :b, :c]) "nested" ### Options * `:decode` - (default `true`) - By default, values are decoded """ @spec get!(t(), [atom() | String.t()], keyword()) :: term() def get!(%__MODULE__{state: state}, keys, opts \\ []) when is_list(keys) do opts = Keyword.validate!(opts, decode: true) keys = Enum.map(keys, &to_lua_key/1) value = do_get_nested(state, keys) if opts[:decode] do Value.decode(value, state) else value end end defp do_get_nested(state, [key]) do State.get_global(state, key) end defp do_get_nested(state, [first | rest]) do case State.get_global(state, first) do {:tref, _} = tref -> get_in_table(state, tref, rest) nil -> raise Lua.RuntimeException, {:lua_error, {:illegal_index, nil, Enum.join([first | rest], ".")}, state} _other -> raise Lua.RuntimeException, {:lua_error, {:illegal_index, nil, Enum.join([first | rest], ".")}, state} end end defp get_in_table(state, tref, [key]) do table = State.get_table(state, tref) Map.get(table.data, key) end defp get_in_table(state, tref, [key | rest]) do table = State.get_table(state, tref) case Map.get(table.data, key) do {:tref, _} = child_tref -> get_in_table(state, child_tref, rest) nil -> raise Lua.RuntimeException, {:lua_error, {:illegal_index, nil, Enum.join([key | rest], ".")}, state} _other -> raise Lua.RuntimeException, {:lua_error, {:illegal_index, nil, Enum.join([key | rest], ".")}, state} end end @doc """ Evaluates the Lua script, returning any returned values and the updated Lua environment iex> {[42], _} = Lua.eval!(Lua.new(), "return 42") `eval!/2` can also evaluate chunks by passing instead of a script. As a performance 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) ### Options * `:decode` - (default `true`) By default, all values returned from Lua scripts are decoded. This may not be desirable if you need to modify a table reference or access a function call. Pass `decode: false` as an option to return encoded values * `:source` - (default `""`) Source name attached to the compiled chunk. Surfaces in runtime errors as `at ::` and in stack traces. Pick a name that identifies where this script came from (e.g. `"my_script.lua"`). When the script is a pre-compiled `t:Lua.Chunk.t/0`, `:source` is accepted but ignored — the chunk already carries the source name it was compiled with. """ @spec eval!(String.t() | Lua.Chunk.t()) :: {[term()], t()} @spec eval!(t() | String.t() | Lua.Chunk.t(), String.t() | Lua.Chunk.t() | keyword()) :: {[term()], t()} @spec eval!(t(), String.t() | Lua.Chunk.t(), keyword()) :: {[term()], t()} def eval!(script) do eval!(new(), script, []) end def eval!(script, opts) when is_binary(script) or is_struct(script, Lua.Chunk) do eval!(new(), script, opts) end def eval!(%__MODULE__{} = lua, script) do eval!(lua, script, []) end def eval!(%__MODULE__{state: state} = lua, script, opts) when is_binary(script) do opts = Keyword.validate!(opts, decode: true, source: "") case Lua.Parser.parse_structured(script) do {:ok, ast} -> case Lua.Compiler.compile(ast, source: opts[:source]) do {:ok, proto} -> {:ok, results, new_state} = Lua.VM.execute(proto, state) results = if opts[:decode] do Value.decode_list(results, new_state) else results end results = Display.wrap_results(results, new_state, opts[:decode]) {results, %{lua | state: new_state}} {:error, msg} -> raise Lua.CompilerException, msg end {:error, parse_errors} -> raise Lua.CompilerException, {:parse_errors, parse_errors, script} end rescue e in [Lua.RuntimeException, Lua.CompilerException] -> reraise e, __STACKTRACE__ # Library bug, not a Lua program error — keep the full Elixir # stack so the failing internal call site is visible. e in [InternalError] -> reraise Lua.RuntimeException, e, __STACKTRACE__ # Pass the VM exception itself (not just its message) so the # `Lua.RuntimeException.exception/1` clause for arbitrary exceptions # picks up `:line`, `:source`, and `:call_stack`. e in [RuntimeError, TypeError, AssertionError] -> reraise Lua.RuntimeException, e, prune_lua_internals(__STACKTRACE__, lua.debug) e -> reraise Lua.RuntimeException, e, prune_lua_internals(__STACKTRACE__, lua.debug) end def eval!(%__MODULE__{state: state} = lua, %Lua.Chunk{prototype: proto}, opts) do # `:source` is accepted for symmetry with the string clause but ignored: a # chunk was already compiled with its source name baked into the prototype, # and that name wins. Pass `:source` at `load_chunk!/2`/compile time to set it. opts = Keyword.validate!(opts, decode: true, source: nil) {:ok, results, new_state} = Lua.VM.execute(proto, state) results = if opts[:decode] do Value.decode_list(results, new_state) else results end results = Display.wrap_results(results, new_state, opts[:decode]) {results, %{lua | state: new_state}} rescue e in [Lua.RuntimeException, Lua.CompilerException] -> reraise e, __STACKTRACE__ # Library bug, not a Lua program error — keep the full Elixir # stack so the failing internal call site is visible. e in [InternalError] -> reraise Lua.RuntimeException, e, __STACKTRACE__ # Pass the VM exception itself (not just its message) so the # `Lua.RuntimeException.exception/1` clause for arbitrary exceptions # picks up `:line`, `:source`, and `:call_stack`. e in [RuntimeError, TypeError, AssertionError] -> reraise Lua.RuntimeException, e, prune_lua_internals(__STACKTRACE__, lua.debug) e -> reraise Lua.RuntimeException, e, prune_lua_internals(__STACKTRACE__, lua.debug) end # Internal-namespace prefixes whose frames are noise to a Lua program # author. We match exact module prefixes (with a trailing dot) so a # user's own `Lua.MyApp.Something` is never collateral damage. The # top-level `Lua.` API surface (e.g. `Lua.eval!/3`) is preserved. @internal_module_prefixes [ "Elixir.Lua.VM.", "Elixir.Lua.Compiler.", "Elixir.Lua.Parser.", "Elixir.Lua.Lexer." ] # Removes internal executor / compiler / parser / lexer frames from a # rescued stacktrace so the user sees only frames they can act on: # their own call site and the public `Lua.eval!` boundary. # # When `debug` is `true` (set via `Lua.new(debug: true)`), pruning is # skipped so the full internal stack is visible for library debugging. defp prune_lua_internals(stacktrace, debug) do if debug do stacktrace else Enum.reject(stacktrace, &internal_frame?/1) end end defp internal_frame?({mod, _fun, _arity, _loc}) when is_atom(mod) do internal_module?(mod) end defp internal_frame?({mod, _fun, _arity, _loc, _meta}) when is_atom(mod) do internal_module?(mod) end defp internal_frame?(_), do: false defp internal_module?(mod) do mod_str = Atom.to_string(mod) Enum.any?(@internal_module_prefixes, &String.starts_with?(mod_str, &1)) end @doc """ Parses a chunk of Lua code into a `t:Lua.Chunk.t/0`, which then can be loaded via `load_chunk!/2` or run via `eval!`. This function is particularly useful for checking Lua code for syntax erorrs and warnings at runtime. If you would like to just load a chunk, use `load_chunk!/1` instead. iex> {:ok, %Lua.Chunk{}} = Lua.parse_chunk("local foo = 1") Errors found during parsing are returned as a `Lua.CompilerException`. Call `Exception.message/1` to render them, or read the formatted list off its `:errors` field: iex> {:error, %Lua.CompilerException{} = error} = Lua.parse_chunk("local foo =;") iex> [msg] = error.errors iex> msg =~ "Expected expression" true """ @spec parse_chunk(String.t()) :: {:ok, Lua.Chunk.t()} | {:error, Lua.CompilerException.t()} def parse_chunk(code) do case Lua.Parser.parse_structured(code) do {:ok, ast} -> case Lua.Compiler.compile(ast) do {:ok, proto} -> {:ok, %Lua.Chunk{prototype: proto}} {:error, msg} -> {:error, Lua.CompilerException.exception(msg)} end {:error, parse_errors} -> {:error, Lua.CompilerException.exception({:parse_errors, parse_errors, code})} end end @doc """ Loads string or `t:Lua.Chunk.t/0` into state so that it can be evaluated via `eval!/2` Strings can be loaded as chunks, which are parsed and loaded iex> {%Lua.Chunk{}, %Lua{}} = Lua.load_chunk!(Lua.new(), "return 2 + 2") Or a pre-compiled chunk can be loaded as well. In the old Luerl-backed implementation, loaded chunks were marked as loaded so they wouldn't be re-loaded on each `eval!/2` call. With the new VM, chunks hold a compiled prototype and don't need a separate loading step. iex> {%Lua.Chunk{}, %Lua{}} = Lua.load_chunk!(Lua.new(), ~LUA[return 2 + 2]c) """ @spec load_chunk!(t(), String.t() | Lua.Chunk.t()) :: {Lua.Chunk.t(), t()} def load_chunk!(%__MODULE__{} = lua, code) when is_binary(code) do case parse_chunk(code) do {:ok, chunk} -> {chunk, lua} {:error, %Lua.CompilerException{} = exception} -> raise exception end end def load_chunk!(%__MODULE__{} = lua, %Lua.Chunk{} = chunk) do {chunk, lua} end @doc """ Calls a function in Lua's state iex> {:ok, [ret], _lua} = Lua.call_function(Lua.new(), [:string, :lower], ["HELLO ROBERT"]) iex> ret "hello robert" iex> lua = Lua.new() iex> lua = Lua.set!(lua, [:double], fn [val] -> [val * 2] end) iex> {:ok, [_ret], _lua} = Lua.call_function(lua, [:double], [5]) References to functions can also be passed iex> {[ref], lua} = Lua.eval!(Lua.new(), "return string.lower", decode: false) iex> {:ok, [ret], _lua} = Lua.call_function(lua, ref, ["FUNCTION REF"]) iex> ret "function ref" iex> {[ref], lua} = Lua.eval!(Lua.new(), "return function(x) return x end", decode: false) iex> {:ok, [ret], _lua} = Lua.call_function(lua, ref, [42]) iex> ret 42 ## Errors When the function raises, `call_function/3` returns `{:error, exception, lua}`, where `exception` is a `Lua.RuntimeException`. Read `:kind` (`:error | :type | :argument | :assertion | :internal`) to discriminate the failure, and `:original` to reach the internal VM exception. Render it yourself: `Exception.message/1` for a plain, single-line, log-safe string; `Lua.format_exception/1` for the rich report (location, stack trace, suggestions, ANSI on a TTY); or `Lua.RuntimeException.to_map/2` for structured data. The boundary does *not* pre-render — `:original` holds the raw VM exception. iex> {[ref], lua} = Lua.eval!(Lua.new(), "return function() error('boom') end", decode: false) iex> {:error, exception, _lua} = Lua.call_function(lua, ref, []) iex> Exception.message(exception) =~ "boom" true The raised Lua value is preserved on `:value` — exactly what `pcall` hands back inside Lua. `error(42)` carries `42`: iex> {[ref], lua} = Lua.eval!(Lua.new(), "return function() error(42) end", decode: false) iex> {:error, exception, _lua} = Lua.call_function(lua, ref, []) iex> exception.value 42 """ @spec call_function(t(), term(), [term()]) :: {:ok, [term()], t()} | {:error, Exception.t(), t()} def call_function(%__MODULE__{} = lua, name, args) do finish_call(lua, resolve_and_call(lua, name, args)) end # `call_function/3` is a programmatic protected-call boundary. Its `:error` # payload is always the public `Lua.RuntimeException`: the internal VM # exception is wrapped so callers pattern-match one type, read the raised Lua # value off `:value` and the category off `:kind`, and render with # `Exception.message/1`. In-Lua `pcall`/`xpcall` still project the raw §6.1 # value via `ProtectedCall.error_value/1` inside the VM; they never reach # this boundary. defp finish_call(%__MODULE__{} = lua, {:ok, results, new_state}) do {:ok, results, %{lua | state: new_state}} end # Already a host-facing exception (a nested eval, or a compiler error from # `load`) — carry it out unchanged rather than re-wrapping. defp finish_call(%__MODULE__{} = lua, {:error, %Lua.RuntimeException{} = e, new_state}) do {:error, e, %{lua | state: new_state}} end defp finish_call(%__MODULE__{} = lua, {:error, %Lua.CompilerException{} = e, new_state}) do {:error, e, %{lua | state: new_state}} end defp finish_call(%__MODULE__{} = lua, {:error, e, new_state}) when is_exception(e) do {:error, Lua.RuntimeException.exception(e), %{lua | state: new_state}} end # Defensive: every `resolve_and_call/3` error path yields an exception today. # If a bare Lua value ever reaches here, wrap it as an `error()` runtime # exception so the boundary's contract stays uniform. defp finish_call(%__MODULE__{} = lua, {:error, reason, new_state}) do exception = Lua.RuntimeException.exception(RuntimeError.exception(value: reason)) {:error, exception, %{lua | state: new_state}} end # Resolves `name`/`func` to a callable and invokes it under protection. # Returns the raw `{:ok, results, new_state} | {:error, exception | reason, # new_state}` (bare `Lua.VM.State`s); callers reattach the state to `lua`. # On error the exception struct is carried out verbatim so each caller can # project it differently (terse value vs. rich raise). defp resolve_and_call(%__MODULE__{} = lua, %Display.Closure{ref: ref}, args) do resolve_and_call(lua, ref, args) end defp resolve_and_call(%__MODULE__{} = lua, %Display.NativeFunc{ref: ref}, args) do resolve_and_call(lua, ref, args) end defp resolve_and_call(%__MODULE__{state: state}, func, args) when is_tuple(func) do do_call_function(func, args, state) end defp resolve_and_call(%__MODULE__{} = lua, name, args) when is_function(name) do {ref, lua} = encode!(lua, name) do_call_function(ref, args, lua.state) end defp resolve_and_call(%__MODULE__{} = lua, name, args) do keys = name |> List.wrap() |> Enum.map(&to_lua_key/1) func = do_get_nested(lua.state, keys) if is_tuple(func) do do_call_function(func, args, lua.state) else # Report the *name that was looked up* — not the resolved `nil` — and # reuse the VM's `attempt to call a value ( '')` # phrasing so the programmatic boundary matches an in-Lua call. error = Executor.call_type_error(func, call_target_hint(name), lua.state) {:error, error, lua.state} end end # Derives a `format_target_hint/1` tag from the name passed to # `call_function/3`. A bare name (`:foo` / `"foo"` / `[:foo]`) reads as a # global; a nested path (`[:string, :lower]`) attributes to the final field, # mirroring how Lua names the field it failed to resolve to a function. defp call_target_hint(name) do case List.wrap(name) do [] -> nil [single] -> {:global, to_string(single)} segments -> {:field, to_string(List.last(segments))} end end defp do_call_function({:native_func, fun}, args, state) do {results, new_state} = fun.(args, state) {:ok, List.wrap(results), new_state} rescue e -> {:error, e, recover_state(e, state)} end defp do_call_function({:lua_closure, proto, upvalues}, args, state) do callee_regs = Tuple.duplicate(nil, max(proto.max_registers, proto.param_count) + 64) callee_regs = args |> Enum.with_index() |> Enum.reduce(callee_regs, fn {arg, i}, regs -> if i < proto.param_count, do: put_elem(regs, i, arg), else: regs end) {results, _regs, new_state} = Executor.execute(proto.instructions, callee_regs, upvalues, proto, state) {:ok, results, new_state} rescue e -> {:error, e, recover_state(e, state)} end defp do_call_function({:compiled_closure, _, _} = closure, args, state) do # Compiled callees route through the dispatcher; same observable # contract as the interpreter branch above. {results, new_state} = Executor.call_function(closure, args, state) {:ok, results, new_state} rescue e -> {:error, e, recover_state(e, state)} end # A resolved tuple that isn't one of the callable shapes above — e.g. a # table or userdata reference. No name is available at this layer, so the # error is reported by type without a name hint. # # This is one place the programmatic boundary does *not* mirror the in-Lua # `:call` opcode: a table carrying a `__call` metamethod is reported as # "attempt to call a table value" here rather than invoking the metamethod. # `call_function/3` is a by-name function-call entry point, not a general # value-application path, so callable tables are out of scope (and the prior # implementation errored on them too). defp do_call_function(other, _args, state) do {:error, Executor.call_type_error(other, nil, state), state} end # This is a protected-call boundary like pcall: trapping the error # unwinds control state, but heap effects the callee made before raising # are kept (Lua 5.3 §2.3). VM exceptions ferry the raise-time state out # on their `:state` field; anything else falls back to the entry state. defp recover_state(%{state: %State{} = raised}, entry), do: State.unwind_to(entry, raised) defp recover_state(_e, entry), do: entry @doc """ The raising variant of `call_function/3` This is also useful for executing Lua function's inside of Elixir APIs ```elixir defmodule MyAPI do use Lua.API, scope: "example" deflua foo(value), state do Lua.call_function!(state, [:string, :lower], [value]) end end ``` """ @spec call_function!(t(), term(), [term()]) :: {[term()], t()} def call_function!(%__MODULE__{} = lua, func, args) do # Unlike `call_function/3`, the raising variant keeps the rich # `ErrorFormatter` render: re-raising the original VM exception through # `Lua.RuntimeException` runs `Exception.message/1` (the terminal renderer) # and copies its `:line` / `:source` / `:call_stack` onto the wrapper. case resolve_and_call(lua, func, args) do {:ok, ret, new_state} -> {ret, %{lua | state: new_state}} {:error, e, _new_state} when is_exception(e) -> raise Lua.RuntimeException, e {:error, reason, new_state} -> raise Lua.RuntimeException, {:lua_error, reason, new_state} end end @doc """ Returns the underlying VM tag tuple for a display struct returned by `Lua.eval!/2` in `decode: false` mode. Returns values unchanged if they are not display structs, so it is safe to apply unconditionally to any value flowing back from `eval`. iex> {[t], _lua} = Lua.eval!(Lua.new(), "return {1, 2, 3}", decode: false) iex> match?({:tref, _}, Lua.unwrap(t)) true iex> {[c], _} = Lua.eval!(Lua.new(), "return function() end") iex> match?({:lua_closure, _, _}, Lua.unwrap(c)) or match?({:compiled_closure, _, _}, Lua.unwrap(c)) true iex> Lua.unwrap(42) 42 Useful when you need to pass an `eval`-returned closure or table reference to a tool that expects the raw VM tag (for example, an internal helper or a custom `deflua`). > #### Tag tuples are not stable API {: .warning} > The `{:tref, _}`, `{:lua_closure, _, _}`, `{:compiled_closure, _, _}`, and > `{:native_func, _}` shapes returned here are VM internals and may change > between releases. Use the `Lua.API` guards (`is_table/1`, `is_lua_func/1`, > `is_erl_func/1`, `is_userdata/1`) to classify unwrapped values rather than > pattern-matching the tuples directly. """ @spec unwrap(term()) :: term() defdelegate unwrap(value), to: Display @doc """ Renders a `Lua.RuntimeException` or `Lua.CompilerException` as a rich, human-readable report — location, source context, stack trace, and suggestions — with ANSI color when `IO.ANSI.enabled?/0` is true. This is the terminal/REPL rendering used by `mix lua.eval`. For a plain, single-line message suitable for `Logger` and error trackers use `Exception.message/1`; for structured data (JSON, a UI) use the exception's `to_map/2`. iex> exception = Lua.RuntimeException.exception("boom") iex> Lua.format_exception(exception) "Lua runtime error: boom" """ @spec format_exception(Lua.RuntimeException.t() | Lua.CompilerException.t()) :: String.t() def format_exception(%Lua.RuntimeException{} = exception), do: Lua.RuntimeException.format(exception) def format_exception(%Lua.CompilerException{} = exception), do: Lua.CompilerException.format(exception) @doc """ Encodes a Lua value into its internal form iex> {encoded, _} = Lua.encode!(Lua.new(), %{a: 1}) iex> match?({:tref, _}, encoded) true """ @spec encode!(t(), term()) :: {term(), t()} # Elixir `nil` maps to Lua `nil`, mirroring `decode!/2` so the round trip is # lossless. Without this clause `nil` falls into the atom head below and # encodes to the string `"nil"`, which is truthy in Lua and silently inverts # `if not value then ...` checks (e.g. `return nil, "not found"` patterns). def encode!(%__MODULE__{} = lua, nil) do {nil, lua} end def encode!(%__MODULE__{} = lua, value) when is_atom(value) and not is_boolean(value) do {Atom.to_string(value), lua} end def encode!(%__MODULE__{state: state} = lua, value) do value = Display.unwrap(value) if Util.encoded?(value) do {value, lua} else {encoded, state} = Value.encode(value, state, &wrap_callback(&1, :anonymous, [])) {encoded, %{lua | state: state}} end rescue _e in [ArgumentError] -> reraise Lua.RuntimeException, "Failed to encode #{inspect(value)}", __STACKTRACE__ _e in [FunctionClauseError] -> reraise Lua.RuntimeException, "Failed to encode #{inspect(value)}", __STACKTRACE__ end @doc """ Encodes a list of values into a list of encoded value Useful for encoding lists of return values iex> {[1, {:tref, _}, true], _} = Lua.encode_list!(Lua.new(), [1, %{a: 2}, true]) """ @spec encode_list!(t(), [term()]) :: {[term()], t()} def encode_list!(%__MODULE__{} = lua, list) when is_list(list) do Enum.map_reduce(list, lua, &encode!(&2, &1)) end @doc """ Decodes a Lua value from its internal form iex> {encoded, lua} = Lua.encode!(Lua.new(), %{a: 1}) iex> Lua.decode!(lua, encoded) [{"a", 1}] """ @spec decode!(t(), term()) :: term() def decode!(%__MODULE__{state: state}, value) do value = Display.unwrap(value) if not Util.encoded?(value) do raise Lua.RuntimeException, "Failed to decode #{inspect(value)}" end Value.decode(value, state) rescue _e in [ArgumentError, KeyError] -> reraise Lua.RuntimeException, "Failed to decode #{inspect(value)}", __STACKTRACE__ end @doc """ Decodes a list of encoded values Useful for decoding all function arguments in a `deflua` iex> {encoded, lua} = Lua.encode_list!(Lua.new(), [1, %{a: 2}, true]) iex> Lua.decode_list!(lua, encoded) [1, [{"a", 2}], true] """ @spec decode_list!(t(), [term()]) :: [term()] def decode_list!(%__MODULE__{} = lua, list) when is_list(list) do Enum.map(list, &decode!(lua, &1)) end @doc """ Loads a Lua file into the environment. Any values returned in the global scope are thrown away. Mimics the functionality of Lua's [dofile](https://www.lua.org/manual/5.3/manual.html#pdf-dofile) """ @spec load_file!(t(), String.t()) :: t() def load_file!(%__MODULE__{} = lua, path) when is_binary(path) do # Add .lua extension if not present full_path = if String.ends_with?(path, ".lua") do path else path <> ".lua" end case File.read(full_path) do {:ok, content} -> {_results, lua} = eval!(lua, content) lua {:error, :enoent} -> raise "Cannot load lua file, #{inspect(full_path)} does not exist" {:error, reason} -> raise "Cannot load lua file #{inspect(full_path)}: #{reason}" end end @doc """ Inject functions written with the `deflua` macro into the Lua runtime. See `Lua.API` for more information on writing api modules ### Options * `:scope` - (optional) scope, overriding whatever is provided in `use Lua.API, scope: ...` * `:data` - (optional) - data to be passed to the Lua.API.install/3 callback """ @spec load_api(t(), module(), keyword()) :: t() def load_api(lua, module, opts \\ []) do opts = Keyword.validate!(opts, [:scope, :data]) funcs = :functions |> module.__info__() |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) scope = opts[:scope] || module.scope() lua = ensure_scope!(lua, scope) lua = Enum.reduce(module.__lua_functions__(), lua, fn {name, with_state?, variadic?}, lua -> arities = Map.get(funcs, name) func = if variadic? do wrap_variadic_function(module, name, with_state?) else wrap_function(module, name, arities, with_state?) end set!(lua, List.wrap(scope) ++ [name], func) end) Lua.API.install(lua, module, scope, opts[:data]) end @doc """ Puts a private value in storage for use in Elixir functions iex> Lua.new() |> Lua.put_private(:api_key, "1234") """ @spec put_private(t(), term(), term()) :: t() def put_private(%__MODULE__{state: state} = lua, key, value) do %{lua | state: State.put_private(state, key, value)} end @doc """ Gets a private value in storage for use in Elixir functions iex> lua = Lua.new() |> Lua.put_private(:api_key, "1234") iex> Lua.get_private(lua, :api_key) {:ok, "1234"} """ @spec get_private(t(), term()) :: {:ok, term()} | :error def get_private(%__MODULE__{state: state}, key) do {:ok, State.get_private(state, key)} rescue KeyError -> :error end @doc """ Gets a private value in storage for use in Elixir functions, raises if the key doesn't exist iex> lua = Lua.new() |> Lua.put_private(:api_key, "1234") iex> Lua.get_private!(lua, :api_key) "1234" """ @spec get_private!(t(), term()) :: term() def get_private!(%__MODULE__{} = lua, key) do case get_private(lua, key) do {:ok, value} -> value :error -> raise "private key `#{inspect(key)}` does not exist" end end @doc """ Deletes a key from private storage iex> lua = Lua.new() |> Lua.put_private(:api_key, "1234") iex> lua = Lua.delete_private(lua, :api_key) iex> Lua.get_private(lua, :api_key) :error """ @spec delete_private(t(), term()) :: t() def delete_private(%__MODULE__{state: state} = lua, key) do %{lua | state: State.delete_private(state, key)} end # Note: These functions are called from load_api and always go through set!/3's # arity-2 clause, which wraps the state as %Lua{} before calling. So `lua` is # already a %Lua{} struct — do NOT call wrap() again. defp wrap_variadic_function(module, function_name, with_state?) do if with_state? do fn args, lua -> execute_function(module, function_name, [args, lua], lua) end else fn args, lua -> execute_function(module, function_name, [args], lua) end end end # credo:disable-for-lines:25 defp wrap_function(module, function_name, arities, with_state?) do if with_state? do fn args, lua -> if (length(args) + 1) in arities do execute_function(module, function_name, args ++ [lua], lua) else arities = Enum.map(arities, &(&1 - 1)) raise Lua.RuntimeException, function: function_name, scope: module.scope(), message: "expected #{Enum.join(arities, " or ")} arguments, got #{length(args)}" end end else fn args, lua -> if length(args) in arities do execute_function(module, function_name, args, lua) else raise Lua.RuntimeException, function: function_name, scope: module.scope(), message: "expected #{Enum.join(arities, " or ")} arguments, got #{length(args)}" end end end end defp execute_function(module, function_name, args, lua) do case apply(module, function_name, args) do # Table-like keyword list [{_, _} | _rest] -> raise Lua.RuntimeException, function: function_name, scope: module.scope(), message: "keyword lists must be explicitly encoded to tables using Lua.encode!/2" # Map map when is_map(map) -> raise Lua.RuntimeException, function: function_name, scope: module.scope(), message: "maps must be explicitly encoded to tables using Lua.encode!/2" {:error, reason} -> raise RuntimeError, value: reason {:error, reason, %Lua{} = returned_lua} -> raise RuntimeError, value: reason, state: returned_lua.state {data, %Lua{} = returned_lua} -> data = List.wrap(data) if not Util.list_encoded?(data) do raise Lua.RuntimeException, function: function_name, scope: module.scope(), message: "deflua functions must return encoded data, got #{inspect(data)}" end {data, returned_lua} data -> data = List.wrap(data) if not Util.list_encoded?(data) do raise Lua.RuntimeException, function: function_name, scope: module.scope(), message: "deflua functions must return encoded data, got #{inspect(data)}" end {data, lua} end catch thrown_value -> {:error, "Value thrown during function '#{function_name}' execution: #{inspect(thrown_value)}"} end defp ensure_scope!(lua, []) do lua end defp ensure_scope!(lua, scope) do set!(lua, scope, %{}) end defp wrap(state), do: %__MODULE__{state: state} # Builds the `{:native_func, closure}` for a user-supplied Elixir callback. # # Both arities speak the documented `t:Lua.t/0` convention at the VM # boundary: the raw `Lua.VM.State` handed in by the executor is wrapped as a # `%Lua{}` for the callback, the returned `%Lua{}` is unwrapped back to its # raw `.state`, and returns are validated as encoded. Sharing one builder # keeps `Lua.set!/3` at a path, functions nested inside a `set!` value, and # `Lua.encode!/2` on the same convention. `function_name`/`scope` feed only # the error message. defp wrap_callback(func, function_name, scope) when is_function(func, 1) do {:native_func, fn args, state -> return = List.wrap(func.(args)) validate_encoded!(return, function_name, scope) {return, state} end} end defp wrap_callback(func, function_name, scope) when is_function(func, 2) do {:native_func, fn args, state -> case func.(args, wrap(state)) do {:error, reason, %__MODULE__{} = returned_lua} -> raise RuntimeError, value: reason, state: returned_lua.state {value, %__MODULE__{} = lua} -> value = List.wrap(value) validate_encoded!(value, function_name, scope) {value, lua.state} value -> value = List.wrap(value) validate_encoded!(value, function_name, scope) {value, state} end end} end defp validate_encoded!(value, function_name, scope) do if not Util.list_encoded?(value) do raise Lua.RuntimeException, function: function_name, scope: scope, message: "deflua functions must return encoded data, got #{inspect(value)}" end end defp to_lua_key(key) when is_atom(key), do: Atom.to_string(key) defp to_lua_key(key) when is_binary(key), do: key defp to_lua_key(key), do: key end