Lua.ex
Try it
Our story · since 2012

A library, three decades
in the making.

Lua.ex didn't start in 2026. It started in 1986 with Erlang, kept going in 2012 when Robert Virding gave the BEAM an imperative language, and arrived here after thirteen years of Luerl quietly powering everything from game servers to spaceships.

Dave Lucia and Robert Virding at Code BEAM Europe 2024
Watch the talk
Dave Lucia & Robert Virding · Code BEAM Europe 2024
1986 → 2012

The tour of paradigms.

Erlang itself began as a Prolog interpreter for a telephony algebra in 1985–86 at Ericsson, before being rewritten as the JAM bytecode machine once Prolog proved too slow. So logic programming was literally in Erlang's DNA from day one.

After Erlang, Robert kept building languages on the BEAM as side projects. He wrote Erlog, a Prolog interpreter in Erlang, and then LFE (Lisp Flavored Erlang), prototyped in 2007 and first released to erlang-questions in March 2008.

With logic programming covered and a functional Lisp covered, the obvious missing paradigm was imperative. He picked Lua — small, clean, embeddable — and had at it. Luerl shipped on February 18, 2012.

"I wrote a Prolog on Erlang, that was fun. Then I wrote a functional Lisp in Erlang, that was fun too. But then I realized that it would be neat to implement an imperative or object-oriented language on the BEAM, so I picked Lua and had at it. That's how Luerl was born."
RV
Robert Virding
co-creator of Erlang · author of Luerl
The name is "Lua" + "Erl" — Robert confirmed it himself on Hacker News. See the original Feb 18, 2012 erlang-questions announcement.
The puzzle

Mutable global state, on an immutable runtime.

The core challenge was philosophical as much as engineering: how do you build a language whose entire semantic model is mutable global state on top of a language whose entire semantic model is immutable local state?

The answer, it turns out, is what you'd do in Erlang anyway — thread the Lua state through every call as a value. No process dictionary, no ETS escape hatch. Straight Erlang.

Thread the state
A single #luerl{} record passed through every call, idiomatic Erlang style.
Layered GC
Luerl's own collector reclaims dead table indices, sitting on top of BEAM's GC.
Erlang-shaped API
{ok, _}/{error, _} for safe calls, raw exceptions for do/call.
2024 · TV Labs

A scripting language for streaming devices.

At TV Labs, we run vision-based integration tests against real Roku, Fire TV, Apple TV, and game-console hardware. To describe a test — "tap right, wait for the home screen, take a screenshot, assert the carousel" — I needed a scripting language I could embed, sandbox, and run thousands of times a day inside an Elixir release.

Luerl was already the answer. It was the mature, battle-tested Lua VM on the BEAM, and Robert had been quietly shipping it for twelve years. The only thing missing was an Elixir-shaped API.

So in 2024 I built one — the tv-labs/lua package on Hex — wrapping Luerl with the deflua macro, the ~LUA sigil for compile-time syntax checking, and a Lua.API behaviour for registering Elixir modules with a Lua VM.

tv_test.exs
defmodule RemoteAPI do
  use Lua.API

  deflua tap(direction), state do
    Device.tap(state.device, direction)
    {[], state}
  end

  deflua screenshot(), state do
    {[Vision.capture!(state.device)], state}
  end
end

Lua.new()
|> Lua.load_api(RemoteAPI)
|> Lua.eval!("""
  tap("right")
  local img = screenshot()
  assert(vision.contains(img, "Home"))
""")
Code BEAM EU · 2024

Lua on the BEAM, together.

At Code BEAM Europe 2024 we ended up giving a talk together — Lua on the BEAM — covering Luerl's history, the Elixir wrapper, and what a next-generation Lua VM on the BEAM could look like.

Backstage, I asked Robert the obvious question: why bother writing a Lua interpreter in Erlang in the first place? That's when he told me the tour-of-paradigms story above. We spent the rest of the conference talking about what a Luerl 2.0 could look like — better error messages, real stack traces, memory sandboxing, all the things that Luerl's age made hard to retrofit.

Watch the talk · YouTube
Lua on the BEAM — Code BEAM Europe 2024
Robert Virding & Dave Lucia · 40 min
2026 · the rewrite

From wrapper to native VM.

The wrapper got us most of the way there. But as the embedded surface area grew — more tools, more user scripts, more agents — the cracks showed. Error messages were terse. Stack traces stopped at the Luerl boundary. There was no way to see the bytecode the VM was actually running.

What started as a planned Luerl 2.0 became something else: a full rewrite, in Elixir, top to bottom. A new lexer, a new parser, a new register-based VM, a new compiler. Lua.ex 1.0 ships as a pure-Elixir implementation of Lua 5.3 — no Luerl in the dependency graph, no NIFs, no shelling out. Just Elixir.

The goal wasn't to outperform Luerl. It was to give Elixir developers the same developer experience they get with Phoenix or Ecto — beautiful errors, compile-time validation, code you can read when something goes wrong.

Real stack traces

Lua frames and Elixir frames in one trace, blamed by the callee's name — none of that "attempt to call a nil value" nonsense.

Compile-time validation

The ~LUA sigil catches syntax errors at compile time. Add c and ship a pre-compiled chunk in your release.

Bytecode you can read

Every chunk compiles to a register-based opcode stream you can step through in the playground.

Luerl is still the right choice for Erlang projects, and Robert is still actively shipping it. Lua.ex is the Elixir-native sibling — born from the same conversations, aimed at the same problem from the other side of the BEAM.

Standing on the shoulders

Thanks, Robert.

This library exists because Robert Virding wrote Luerl first. Thirteen years of design decisions, edge cases, and stdlib implementations live inside Lua.ex's lineage. We learned from all of them.

Credit also to the long-time Luerl contributors who shaped the API we ported from — Henning Diedrich, Cees de Groot, Heinz Gies, and dozens of others on the commit log. And to the Lua team in Rio for designing a language small enough to fit in your head and powerful enough to run Roblox.

Lua.ex is built at TV Labs, where we use it every day to script vision-based integration tests against real streaming devices.

Now go write some Lua.

The story is nice, but the VM is more fun. Open the playground and watch the opcodes flow.