Lua (Lua v1.0.1)

View Source

Embed a sandboxed Lua 5.3 scripting runtime in your Elixir application — no NIFs, no C, no Erlang runtime dependency.

Lua is a Lua 5.3 virtual machine implemented entirely in Elixir. The lexer, parser, register-based VM, and standard library all run directly on the BEAM, so there is nothing to compile and no foreign code in your release. It exists to let you safely run untrusted scripts — AI-agent–authored code, game logic, user-defined rules, configuration, plugins — with a small, idiomatic Elixir API for passing data and functions across the boundary. Giving an AI agent a sandboxed runtime where it can only call the Elixir functions you expose is a primary use case. Scripts are sandboxed by default, errors carry source and line information, and each Lua value is plain immutable Elixir state with no shared mutable globals.

Installation

Add lua to your dependencies in mix.exs:

def deps do
  [
    {:lua, "~> 1.0.0"}
  ]
end

Upgrading from 0.4.0 or earlier? See the Migrating to 1.0 guide.

Quickstart

Evaluate Lua with Lua.eval!/2. It returns {results, lua} where results is the list of returned values and lua is the updated state:

iex> {[4], _lua} = Lua.eval!("return 2 + 2")

You can thread state across multiple evaluations, set globals from Elixir, and read them back:

iex> lua = Lua.set!(Lua.new(), [:name], "world")
iex> {[greeting], _lua} = Lua.eval!(lua, ~S[return "hello, " .. name])
iex> greeting
"hello, world"

Tour

Error messages with source and line

Runtime errors raise Lua.RuntimeException, which carries the failing :source and :line so you can report exactly where a script broke:

The ~LUA sigil used below (and elsewhere in this tour) needs import Lua:

import Lua

try do
  Lua.eval!(~LUA"""
  local x = 1
  error("something went wrong")
  """)
rescue
  e in Lua.RuntimeException ->
    e.line    # => 2
    e.source  # => "<eval>" (chunk name)

    #
    #   Lua runtime error: at <eval>:2:
    #
    #     runtime error: something went wrong
    Exception.message(e)
end

Lua-level error handling works too — pcall catches the error and returns it as a value:

iex> {[false, err], _lua} = Lua.eval!(~S[return pcall(function() error("nope") end)])
iex> err =~ "nope"
true

Calling Elixir functions from Lua

The quickest way to expose an Elixir function is Lua.set!/3:

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)")

For richer APIs, define a module with use Lua.API and the deflua macro, then load it with Lua.load_api/2:

defmodule MyAPI do
  use Lua.API

  deflua double(v), do: 2 * v
end

lua = Lua.new() |> Lua.load_api(MyAPI)

{[10], _lua} = Lua.eval!(lua, "return double(5)")

Userdata

Pass an arbitrary Elixir term across the boundary as a {:userdata, term} tuple. It round-trips opaquely — Lua can hold the reference and hand it back, but cannot inspect or dereference it:

iex> lua = Lua.set!(Lua.new(), [:thing], {:userdata, %{secret: 42}})
iex> {[{:userdata, %{secret: 42}}], _lua} = Lua.eval!(lua, "return thing")

Sandboxing

Lua.new/1 sandboxes dangerous stdlib paths by default, including os.execute, os.exit, os.getenv, file I/O (io.*), require, load, and dofile. Calling a sandboxed function raises rather than touching the host:

Lua.eval!(~S[os.execute("rm -rf /")])
# ** (Lua.RuntimeException) Lua runtime error: os.execute(_) is sandboxed

To allow a specific operation, exclude it from the sandbox explicitly:

iex> lua = Lua.new(exclude: [[:os, :getenv]])
iex> {[value], _lua} = Lua.eval!(lua, ~S[return os.getenv("HOME")])
iex> is_binary(value)
true

Resource limits

Sandboxing controls which functions a script may call, but it does not stop a script from spinning forever or recursing without bound. Two options on Lua.new/1 give you deterministic limits without wrapping each evaluation in a host Task plus a wall-clock timeout. Both default to :infinity (no limit) and raise catchable runtime errors, so pcall recovers from them in-band:

  • :max_call_depth caps nested function-call depth; exceeding it raises "stack overflow".

  • :max_instructions caps the number of VM instructions a single evaluation may execute; exceeding it raises "instruction budget exceeded".

    iex> lua = Lua.new(max_instructions: 1000) iex> {[false, message], _lua} = Lua.eval!(lua, ~S[return pcall(function() while true do end end)]) iex> message =~ "instruction budget exceeded" true

See the runnable Sandboxing example, or the Security and sandboxing guide, for details.

Metatables and metamethods

Full metamethod dispatch is supported (__index, __newindex, __call, arithmetic, comparison, length, concatenation, and __tostring), so idiomatic Lua object patterns work as written:

iex> {[result], _lua} = Lua.eval!(~LUA"""
...> local Vec = {}
...> Vec.__index = Vec
...> Vec.__add = function(a, b) return setmetatable({x = a.x + b.x}, Vec) end
...> local a = setmetatable({x = 1}, Vec)
...> local b = setmetatable({x = 2}, Vec)
...> return (a + b).x
...> """)
iex> result
3

Coverage and status

Lua targets Lua 5.3. The lexer, parser, register-based VM, value encoding/decoding, varargs, multiple returns, _G/_ENV, metatables, the string-pattern engine (find/match/gmatch/gsub), and the string, table, math, os, utf8, and debug standard libraries are implemented.

As a sandboxed embedded VM, some standalone-interpreter behavior is a deliberate non-goal rather than a missing feature:

  • Standalone interpreter / os.execute — there is no shell-out to the host.
  • Host filesystem access is deny-by-defaultLua does not read your host filesystem unless you opt in. The io.* library and require/dofile are sandboxed by default and raise rather than touching disk, so out of the box there is no host-OS file or module resolution. When you want it, you enable it explicitly: Lua.set_lua_paths/2 configures where require resolves modules on disk, and Lua.load_file!/2 loads a Lua file from the host.
  • Coroutines, garbage collection / weak tables, and the full debug library.

For the live Lua 5.3 official test-suite pass count and the rationale behind each deferral, see the ROADMAP.md.

Examples

End-to-end, runnable Livebook notebooks live in guides/examples/. Open any of them in Livebook, or read them rendered under Examples on HexDocs:

Documentation

Lua the Elixir library vs Lua the language

When referring to this library, Lua is stylized as a link. References to Lua the language are in plaintext and not linked.

Security and sandboxing

Lua is built to run untrusted scripts. By default, Lua.new/1 installs a sandbox that blocks the dangerous standard-library paths (io, file, os.execute/exit/getenv, package, require, load, …), and the VM guards against allocation-bomb denial-of-service by refusing oversized string.rep, table.unpack/concat/move, and string concatenations before they allocate.

# os.exit is sandboxed by default — calling it raises (catchable)
iex> {[false, message], _} = Lua.eval!(Lua.new(), "return pcall(os.exit)")
iex> message =~ "sandboxed"
true

Capability sandboxing (:sandboxed, :exclude, Lua.sandbox/2), recursion limits (:max_call_depth), the built-in allocation guards, and the host-level pattern for bounding CPU time and total memory are all covered in the Security and sandboxing guide.

Compatibility and credits

Lua started as an ergonomic Elixir wrapper around Robert Virding's Luerl project. As of 1.0.0 it is a full Elixir-native reimplementation of the Lua 5.3 lexer, parser, and virtual machine, with a public API designed to feel idiomatic from Elixir.

Compared to Luerl: Lua is pure Elixir with no shared mutable state (each Lua value is plain immutable state you thread explicitly), ships richer error messages with source and line attribution, and benchmarks competitively. Luerl deserves credit as the prior art that made this possible — its design informed many decisions in the new VM, and we benchmark against it.

License

Released under the Apache-2.0 license. See LICENSE.

Summary

Functions

Calls a function in Lua's state

Decodes a Lua value from its internal form

Decodes a list of encoded values

Deletes a key from private storage

Encodes a Lua value into its internal form

Encodes a list of values into a list of encoded value

Evaluates the Lua script, returning any returned values and the updated Lua environment

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.

Gets a table value in Lua

Gets a private value in storage for use in Elixir functions

Gets a private value in storage for use in Elixir functions, raises if the key doesn't exist

Inject functions written with the deflua macro into the Lua runtime.

Loads string or Lua.Chunk.t/0 into state so that it can be evaluated via eval!/2

Loads a Lua file into the environment. Any values returned in the global scope are thrown away.

Initializes a Lua VM sandbox

Parses a chunk of Lua code into a Lua.Chunk.t/0, which then can be loaded via load_chunk!/2 or run via eval!.

Puts a private value in storage for use in Elixir functions

Sandboxes the given path, swapping out the implementation with a function that raises when called

Sets a table value in Lua. Nested keys will allocate intermediate tables

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

Write Lua code that is parsed at compile-time.

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.

Types

t()

@type t() :: %Lua{debug: term(), state: term()}

Functions

call_function(lua, name, args)

@spec call_function(t(), term(), [term()]) ::
  {:ok, [term()], t()} | {:error, Exception.t(), t()}

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

call_function!(lua, func, args)

@spec call_function!(t(), term(), [term()]) :: {[term()], t()}

The raising variant of call_function/3

This is also useful for executing Lua function's inside of Elixir APIs

defmodule MyAPI do
  use Lua.API, scope: "example"

  deflua foo(value), state do
    Lua.call_function!(state, [:string, :lower], [value])
  end
end

decode!(lua, value)

@spec decode!(t(), term()) :: term()

Decodes a Lua value from its internal form

iex> {encoded, lua} = Lua.encode!(Lua.new(), %{a: 1})
iex> Lua.decode!(lua, encoded)
[{"a", 1}]

decode_list!(lua, list)

@spec decode_list!(t(), [term()]) :: [term()]

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]

delete_private(lua, key)

@spec delete_private(t(), term()) :: t()

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

encode!(lua, value)

@spec encode!(t(), term()) :: {term(), t()}

Encodes a Lua value into its internal form

<!-- Old Luerl implementation returned specific tref IDs: {encoded, _} = Lua.encode!(Lua.new(), %{a: 1}); encoded => {:tref, 14} -->

iex> {encoded, _} = Lua.encode!(Lua.new(), %{a: 1})
iex> match?({:tref, _}, encoded)
true

encode_list!(lua, list)

@spec encode_list!(t(), [term()]) :: {[term()], t()}

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])

eval!(script)

@spec eval!(String.t() | Lua.Chunk.t()) :: {[term()], t()}

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 "<eval>") Source name attached to the compiled chunk. Surfaces in
            runtime errors as `at <source>:<line>:` 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.

eval!(script, opts)

@spec eval!(t() | String.t() | Lua.Chunk.t(), String.t() | Lua.Chunk.t() | keyword()) ::
  {[term()], t()}

eval!(lua, script, opts)

@spec eval!(t(), String.t() | Lua.Chunk.t(), keyword()) :: {[term()], t()}

format_exception(exception)

@spec format_exception(Lua.RuntimeException.t() | Lua.CompilerException.t()) ::
  String.t()

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"

get!(lua, keys, opts \\ [])

@spec get!(t(), [atom() | String.t()], keyword()) :: term()

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

get_private(lua, key)

@spec get_private(t(), term()) :: {:ok, term()} | :error

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"}

get_private!(lua, key)

@spec get_private!(t(), term()) :: term()

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"

load_api(lua, module, opts \\ [])

@spec load_api(t(), module(), keyword()) :: t()

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

load_chunk!(lua, code)

@spec load_chunk!(t(), String.t() | Lua.Chunk.t()) :: {Lua.Chunk.t(), t()}

Loads string or 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)

load_file!(lua, path)

@spec load_file!(t(), String.t()) :: t()

Loads a Lua file into the environment. Any values returned in the global scope are thrown away.

Mimics the functionality of Lua's dofile

new(opts \\ [])

@spec new(keyword()) :: t()

Initializes a Lua VM sandbox

iex> Lua.new()

By default, the following Lua functions are sandboxed.

  • [: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]

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

parse_chunk(code)

@spec parse_chunk(String.t()) ::
  {:ok, Lua.Chunk.t()} | {:error, Lua.CompilerException.t()}

Parses a chunk of Lua code into a 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

put_private(lua, key, value)

@spec put_private(t(), term(), term()) :: t()

Puts a private value in storage for use in Elixir functions

iex> Lua.new() |> Lua.put_private(:api_key, "1234")

sandbox(lua, path)

@spec sandbox(t(), [atom() | String.t()]) :: t()

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])

set!(lua, keys, func)

@spec set!(t(), atom() | String.t() | [atom() | String.t()], term()) :: t()

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")

set_lua_paths(lua, paths)

@spec set_lua_paths(t(), [String.t()] | String.t()) :: t()

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 function to import these scripts

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.

sigil_LUA(code, opts)

(macro)

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

unwrap(value)

@spec unwrap(term()) :: term()

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

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.