Skip to content

Lua

Lua is a small scripting language designed to be embedded inside other programs. The entire reference implementation is about 30,000 lines of portable C and compiles to a few hundred kilobytes, which is why it keeps turning up inside game engines, databases, web servers and editors rather than as a standalone application platform.

Lua (“moon” in Portuguese, created in 1993 at PUC-Rio in Brazil) is a dynamically typed, garbage-collected language with:

  • One composite data structure. Tables. Arrays, dictionaries, objects, namespaces and modules are all tables. There is no separate list, dict, class, struct or object type.
  • First-class functions with lexical scoping. Closures are a core feature, not an add-on.
  • Coroutines. Cooperative, stackful coroutines built into the language — the basis for most Lua async libraries.
  • Metatables. A small hook system that lets you override what indexing, arithmetic, comparison and calling mean for a value. Object orientation is built on top of this, not into the language.
  • A deliberately tiny standard library. String, table, math, os, io, coroutine, and a debug library. No sockets, no JSON, no HTTP, no regex engine.

That last point is the key to understanding Lua. It is not a batteries-included language, and it is not trying to be. It is a language kernel that a host application embeds and then extends with whatever the host already knows how to do.

Lua is almost always the scripting half of a larger program:

Host What Lua does there Version
Neovim Configuration, plugins, and most of the modern plugin ecosystem LuaJIT (5.1 + some 5.2)
Redis Server-side scripts run atomically via EVAL / FUNCTION Lua 5.1
OpenResty / nginx Request handling, routing, auth, rate limiting (also under Kong, APISIX) LuaJIT
Roblox Game logic Luau, a typed Lua 5.1 derivative
LÖVE (love2d) The whole game — LÖVE is a 2D game framework that is a Lua host LuaJIT
World of Warcraft Addons and UI Lua 5.1
Wireshark, mpv, VLC, Nmap, HAProxy Plugins, dissectors, filters, scripts varies
Awesome WM, Conky, Hammerspoon Desktop configuration and automation varies

Standalone Lua exists too (lua on the command line, plus the LuaRocks package manager), but embedding is the dominant use.

Lua does not promise backwards compatibility between minor versions. 5.1, 5.3 and 5.4 are meaningfully different languages, and the version is decided by whatever program is embedding Lua — not by you.

Version Released What matters
5.1 2006 Still everywhere because LuaJIT froze here. No goto, no integers, unpack is global, modules via module(), setfenv/getfenv exist.
5.2 2011 Added goto, _ENV (replacing setfenv), table.unpack, package.searchers.
5.3 2015 Added an integer subtype and bitwise operators (&, |, ~, <<, >>), // floor division, string.pack.
5.4 2020 Generational GC, <const> and <close> variable attributes, warn(), coroutine.close, stricter numeric for. Current stable line.
LuaJIT 2.1 ongoing A tracing JIT compiler for the 5.1 language plus a few 5.2 features (notably goto). Adds the ffi, bit and jit libraries. Often 10–50× faster than the reference interpreter on numeric code. All numbers are doubles — there is no integer subtype.

Find out what you have:

Terminal window
lua -v
# => Lua 5.4.6 Copyright (C) 1994-2023 Lua.org, PUC-Rio
print(_VERSION) -- => Lua 5.4
print(jit and jit.version) -- => LuaJIT 2.1.x, or nil on standard Lua

Install it from your package manager (lua5.4, lua, luajit) or from lua.org — building from source is a two-minute make.

Terminal window
# Interactive REPL
lua
# Run a script
lua hello.lua
# Run a one-liner
lua -e 'print(("hi"):upper())'
# => HI
# Load a file, then drop into the REPL with its globals available
lua -i setup.lua
# Pass arguments — they land in the global table `arg`
lua script.lua one two
script.lua
print(arg[0]) -- => script.lua (the script's own name)
print(arg[1]) -- => one
print(#arg) -- => 2

In the REPL, Lua 5.3+ prints the value of a bare expression:

$ lua
Lua 5.4.6 Copyright (C) 1994-2023 Lua.org, PUC-Rio
> 2 + 3
5
> ("abc"):rep(2)
abcabc

There is also luac, which compiles a script to bytecode (luac -o out.luac in.lua). Bytecode is version- and platform-specific and is not a security boundary — never load() untrusted bytecode.

Enough of the language to recognize its shape:

wordcount.lua
local counts = {}
for line in io.lines(arg[1]) do
for word in line:lower():gmatch("%a+") do
counts[word] = (counts[word] or 0) + 1
end
end
-- Tables are unordered, so collect the keys and sort them.
local words = {}
for word in pairs(counts) do
words[#words + 1] = word
end
table.sort(words, function(a, b) return counts[a] > counts[b] end)
for i = 1, math.min(#words, 5) do
print(string.format("%5d %s", counts[words[i]], words[i]))
end
Terminal window
lua wordcount.lua /usr/share/dict/words

Almost every Lua trait is visible here: local everywhere, one table doing duty as both a dictionary and a list, counts[word] or 0 standing in for a default value, a gmatch pattern instead of a regex, an anonymous comparator function, and 1-based indexing.

The whole point of Lua is the C API. A host program creates a lua_State, opens whichever libraries it wants to expose, and exchanges values with Lua through a virtual stack. Embedding is roughly this much code:

host.c
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main(void) {
lua_State *L = luaL_newstate(); /* a fresh, independent interpreter */
luaL_openlibs(L); /* expose the standard library */
if (luaL_dofile(L, "plugin.lua") != LUA_OK)
fprintf(stderr, "lua error: %s\n", lua_tostring(L, -1));
lua_close(L);
return 0;
}
Terminal window
cc host.c $(pkg-config --cflags --libs lua5.4) -o host

Three properties make this practical, and they explain most of Lua’s design:

  • A lua_State is fully self-contained. No globals in the C library, so a program can run many independent interpreters, one per request or per game entity.
  • Errors never escape into C. They unwind to the nearest protected call, so a broken script cannot take down the host.
  • Everything is a value the host can inspect and control. The host chooses which libraries exist — Redis and most game engines simply do not load io and os.

That is why the standard library is small: anything the host already does well (networking, threads, rendering, file layout) is the host’s job to expose, not the language’s job to duplicate.

Lua’s authors describe the guiding rule as “mechanisms, not policies”, and the consequences are everywhere:

  • One data structure. Rather than list, dict, set, object and record types, you get the table, and conventions on top. Less to implement, less to learn, less to keep compatible.
  • No built-in object system. Metatables let you build the class model you want — prototypes, single inheritance, multiple inheritance, traits — and every framework does.
  • Cooperative concurrency only. Coroutines give you the control flow; the host supplies the scheduler and the I/O.
  • Portability over features. The reference implementation is ISO C, with no dependencies beyond libc, which is why Lua runs on microcontrollers, consoles and mainframes alike.
  • Freedom to break compatibility. Minor versions change the language. That is unusual and occasionally painful, but it is why Lua has stayed small for thirty years instead of accreting.

Performance follows from the same minimalism: a register-based VM, an incremental (5.4: optionally generational) garbage collector, interned short strings, and tables with a dense array part. Standard Lua is among the fastest interpreted languages; LuaJIT is faster still, often within a small factor of C on numeric code.

LuaRocks is the package manager. Modules are pure Lua or C extensions, and the notable ones are small and focused:

Terminal window
luarocks install lpeg # parsing expression grammars, by Lua's author
luarocks install penlight # general-purpose "missing" stdlib
luarocks install luasocket # TCP/UDP/HTTP
luarocks install busted # test framework
luarocks install luacheck # static linter -- catches accidental globals

Add --local to install into ~/.luarocks instead of system-wide, then run eval "$(luarocks path)" so package.path picks it up.

Inside a host application, ignore all of this: use the host’s own module system and whatever it bundles. Neovim, OpenResty and Roblox each have their own conventions and their own standard library extensions.

Four ideas carry most of Lua:

  1. Everything composite is a table, and tables have an array part and a hash part that you never see directly.
  2. Values are dynamically typed; variables are not typed at all. local x can hold anything, including a function or a table.
  3. Metatables are the extension point. When an operation on a value doesn’t have a default meaning, Lua looks for a metamethod. That single rule gives you OOP, operator overloading, read-only tables, default values, and proxies.
  4. Multiple return values are a real thing, not a tuple. local a, b = f() and f() returning three values in the middle of a list are different situations with different rules.

Lua is also unusual in what it doesn’t have: no continue, no switch, no exceptions with try/catch (you get pcall), no classes, no +=, no ternary operator, no built-in string interpolation, and 1-based indexing.

  • Basics — the eight types, dynamic typing, local vs global (and why it matters more here than elsewhere), nil, numbers and the integer/float split, truthiness.
  • Control flow and functions — conditionals, all four loop forms, goto, operators and the and/or idioms, multiple returns, varargs, closures, and the : method syntax.
  • Tables — the one data structure: arrays, dictionaries, the # operator and its holes, iteration order, metatables and every metamethod worth knowing, OOP and inheritance, the module pattern.
  • Strings and patterns — the string library, string.format, and Lua patterns in depth. Patterns look like regex and are not regex; this page is mostly about that.
  • Standard library and idiomstable, math, os, io; error handling with error/assert/pcall/xpcall; require and the module search path; coroutines; and the gotcha list.

Read them in order the first time. Tables and patterns are where Lua differs most from what you already know.

  • Lua is a ~250 KB embeddable language kernel, not an application platform. The host program supplies the rest.
  • The version is chosen by the host. 5.1/LuaJIT is still extremely common; 5.4 is the current reference line.
  • Tables are the only composite type; metatables are the only extension mechanism.
  • Check _VERSION and jit before assuming which dialect you are writing.