Lua — Reference
Source: https://www.lua.org/manual/
Lua
- Created: 1993 by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, Waldemar Celes at PUC-Rio
- Latest stable: Lua 5.4.8 (June 4, 2025); Lua 5.5.0 (December 22, 2025) is the most recent release line
- Paradigms: Multi-paradigm: procedural, functional, prototype-based OO via tables/metatables, data-description
- Typing: Dynamic, strong; eight base types (nil, boolean, number, string, function, userdata, thread, table)
- Memory: Garbage collected; incremental GC (default) and generational GC mode (5.4+); reference semantics for tables/functions/userdata
- Compilation: Compiled to bytecode for a register-based VM (since 5.0); LuaJIT is a separate trace-JIT implementation
- Primary domains: Embedded scripting (games, Redis, Wireshark, Nginx via OpenResty), config DSLs, plugin engines, mods
- Official docs: https://www.lua.org/manual/
At a glance
Lua is a small, fast, embeddable scripting language designed at PUC-Rio. The reference implementation is ~30k lines of clean ANSI C. Its hallmark: a single composite data structure (the table) plus metatables for protocol overloading, giving you arrays, hashmaps, objects, classes, namespaces, and modules from one primitive. The Lua C API is the gold standard for embedding scripting in host applications. LuaJIT (Mike Pall) is an independent implementation tracking Lua 5.1 + selected later features that often outperforms compiled languages.
Getting started
Install — From source: make all test against the official tarball at https://www.lua.org/. Package managers: brew install lua, apt install lua5.4, winget install lua. Verify: lua -v.
Hello world (hello.lua):
print("Hello, world!")Run: lua hello.lua.
Project — Lua has no canonical project layout. Conventional:
my_app/
src/
init.lua -- exports
foo.lua
spec/ -- busted tests
rockspec -- LuaRocks package
*.rockspec
LuaRocks (luarocks install <pkg>) is the de facto package manager. rockspec files describe deps. LUA_PATH and LUA_CPATH env vars control require search.
REPL — lua with no args opens an interactive prompt; expressions print automatically when prefixed with = (5.3+: bare expressions print). Use luap or lua-repl for readline support, or run inside rlwrap lua.
Basics
Types/literals — nil, true/false, numbers (Lua 5.3+ has both 64-bit integer and double subtypes — automatic conversion; before 5.3 only doubles), strings (immutable, 8-bit clean — Unicode handled by libraries), tables ({}), functions, userdata (C objects), threads (coroutines). Strings: "abc", 'abc', [[multi-line]], [==[with ==]==] for nesting.
Variables/scoping — Globals by default(!) — assigning x = 1 at top level creates a global. local x = 1 for lexical scope. Block scope is by do ... end, if, for, while, function bodies. Lua 5.4 added <const> and <close> attributes: local x <const> = 5, local f <close> = io.open(...) (closes on scope exit). Lua 5.5 adds optional global declarations.
Control flow — if cond then ... elseif ... else ... end, while cond do ... end, repeat ... until cond, numeric for i = 1, 10, 2 do, generic for k, v in pairs(t) do. No switch — use tables of functions or chained if. break exits loops; goto label since 5.2.
Functions — First-class: local f = function(x) return x * 2 end. Sugar: local function f(x) return x * 2 end. Multiple returns: function divmod(a,b) return a//b, a%b end. Varargs: function(...) local args = {...}; ... end. Method syntax: t:method(x) is sugar for t.method(t, x).
Strings — Immutable. "a" .. "b" to concat. #s for length (in bytes). string library (or : method form on any string): string.format, string.sub, string.find, string.match, string.gmatch, string.gsub — Lua patterns (NOT regex; simpler grammar with character classes and captures). UTF-8 helpers in utf8 (5.3+).
Collections — Tables do everything. Array part: t = {10, 20, 30} (1-indexed!). Hash part: t = {name="x", age=42}. Mixed: {1, 2, x=10}. #t returns length of array part (undefined behavior with holes). pairs(t) iterates all keys, ipairs(t) only the contiguous integer prefix. Lua 5.5 introduces “more compact arrays” for memory efficiency.
Intermediate
Type system depth — Truly dynamic; no static checker in core. Optional checkers: Teal (typed dialect transpiling to Lua), Luau (Roblox’s strict-optional type system, separate impl), LuaCATS annotations consumed by LSPs (---@param x number).
Modules — local M = {}; function M.foo() end; return M is the convention. require "my.module" searches LUA_PATH. Built-in libraries: string, table, math, io, os, coroutine, package, debug, utf8.
Error handling — error("msg", level) raises; error{code=1, msg="x"} raises a value (any type). pcall(f, args...) runs in protected mode, returns (true, results) or (false, err). xpcall(f, handler, args...) adds a custom handler invoked inside the error context (better tracebacks). assert(v, msg) is shorthand. No exceptions in the C++/Java sense.
Concurrency primitives — Coroutines, not threads: coroutine.create(f), coroutine.resume(co, args...), coroutine.yield(values...). Cooperative, single-threaded. For real parallelism use host embeddings (lua-lanes, luaproc) or LuaJIT FFI to call into pthreads. os.execute, io.popen for OS-level concurrency.
I/O — io.open(filename, mode) returns a file handle with :read, :write, :close, :lines, :seek. io.lines(file) for iteration. Default streams io.stdin, io.stdout, io.stderr. print writes to stdout with newline; io.write doesn’t.
Stdlib highlights — string, table (insert, remove, concat, sort, unpack/table.unpack), math (huge, pi, random, floor, tointeger, type for int-vs-float), os (time, date, getenv, execute), coroutine, io, package (loader internals), debug (introspection — production-dangerous), utf8 (5.3+).
Advanced
Memory/GC — Incremental mark-and-sweep by default. Lua 5.4 added a generational GC mode: collectgarbage("generational") separates young/old, drastically cuts pause time for typical allocation patterns. Tunable via collectgarbage("setpause", n) and ("setstepmul", n). Lua 5.5 makes “major garbage collections done incrementally”. Weak tables (__mode = "k"|"v"|"kv") for caches and observer patterns. Finalizers via __gc metamethod (5.2+).
Concurrency deep dive — Coroutines compile down to setjmp/longjmp style stack switching in the VM (cheap — sub-microsecond). Stackful (asymmetric): coroutine.resume and coroutine.yield form a callee-caller pair. LuaJIT supports symmetric coroutines as “async” via FFI in some forks. Embeddings often use one Lua state per OS thread (states are not thread-safe to share); message passing via the C API.
FFI — Standard Lua: write a C extension (luaopen_<modname>(L)), build a .so/.dll, place on LUA_CPATH. LuaJIT FFI is dramatically more productive: local ffi = require("ffi"); ffi.cdef[[ int printf(const char*, ...); ]]; ffi.C.printf("hi\n") — no glue code, declares C signatures inline, calls JIT-compile to direct native calls. https://luajit.org/ext_ffi.html.
Reflection — debug library: debug.traceback(), debug.getinfo(level), debug.getlocal/setlocal, debug.getupvalue/setupvalue, debug.sethook(hook, mask, count) for VM hooks (“call”/“return”/“line”/“count”). Also getmetatable, setmetatable, rawget, rawset, rawequal, rawlen. Dangerous: debug.* lets you peek into other coroutines and break sandboxing.
Performance tools — LuaJIT’s jit.dump, jit.v modules show trace compilation. Standard Lua: os.clock() for wall time; luaprofiler, LuaTrace, MobDebug, ZeroBrane Studio profilers. Disassemble with luac -l file.lua (shows VM instructions). LuaJIT has -jdump=+rs for traces with bytecode/SSA/machine code.
God mode
Metatables and metamethods — Tables get behavior via setmetatable(t, mt) where mt contains metamethod fields. Full catalog: __index (key lookup fallback — table or function), __newindex (assignment fallback), __call (make table callable), __tostring, __metatable (lock the metatable), __mode (weak refs), __gc (finalizer), __close (5.4+, for <close> locals), __name (5.3+); arithmetic: __add, __sub, __mul, __div, __mod, __pow, __unm, __idiv (5.3+), __band, __bor, __bxor, __bnot, __shl, __shr (5.3+); comparison: __eq, __lt, __le; misc: __len, __concat. OOP idiom: Class.__index = Class; setmetatable(obj, Class).
Debug library + sandboxing — debug.sethook for tracing/limiting. To sandbox untrusted Lua: load with load(src, "name", "t", env) (5.2+) where env is a custom _ENV table containing only safe APIs. Strip debug, os.execute, io, package, loadfile, dofile. LuaJIT requires extra care since FFI can call any C function. See sandbox examples at http://lua-users.org/wiki/SandBoxes.
LuaJIT FFI + jit.dump — ffi.cdef[[ ... ]] declares C types/functions; ffi.new("T[?]", n) allocates. jit.opt.start("3") cranks optimization; local jit_dump = require("jit.dump"); jit_dump.start("+rs", "trace.txt") records trace compilation events for tuning hot loops. LuaJIT reaches near-C performance on numeric code.
Coroutine internals — Each coroutine is a lua_State (full VM state with its own stack). coroutine.resume performs a stack switch (saves/restores registers); on yield, control returns to resumer with yielded values. Stackful means you can yield from any depth without rewriting the call chain — a feature missing from Python async/yield.
_ENV and lexical environments — Lua 5.2+ replaced 5.1’s setfenv/getfenv with the _ENV upvalue. Globals are sugar: x = 1 is _ENV.x = 1. Sandbox by passing a fresh _ENV to load. Each chunk has an implicit local _ENV.
Custom VM embedding (Lua C API) — lua_State *L = luaL_newstate(); luaL_openlibs(L); luaL_dofile(L, "x.lua"); lua_close(L);. Push/pop value stack: lua_pushnumber, lua_call, lua_pcall, lua_tostring. Userdata for opaque C objects with metatables. The C API is famously stable — old extensions still compile.
Bytecode — luac -l -l file.lua lists VM instructions twice (second pass shows constants, locals, lines). Bytecode is portable across machines of the same word size and endianness in the same Lua version. luac -o out.luac file.lua dumps; loadfile("out.luac") loads.
Continuations (5.2+) — When a C function calls lua_pcallk/lua_callk/lua_yieldk with a continuation function, Lua can yield through C frames and resume into the continuation. Solves the “cannot yield across C boundary” limitation.
Garbage collector tuning — collectgarbage("count") returns memory in KB. collectgarbage("collect") forces full cycle. 5.4 generational mode: collectgarbage("generational", minor_mul, major_mul). collectgarbage("setpause", 200) (default) waits for 2x growth before next cycle. Profile before tuning.
Notable embeddings — Roblox uses Luau (gradually typed Lua fork, sandboxed); Defold embeds 5.1; LOVE2D (love.run) embeds 5.1/LuaJIT for game dev; Garry’s Mod; World of Warcraft addons (5.1); Neovim uses LuaJIT-compatible Lua 5.1 for its config and plugins; Redis uses Lua 5.1 for EVAL; Nginx/OpenResty embeds LuaJIT for high-throughput HTTP scripting; Wireshark dissector scripting; Adobe Lightroom plugins.
Idioms & style
- Naming:
snake_casefor variables/functions in Lua-the-language tradition;PascalCasefor “classes” (tables used as types);_PRIVATEconvention for “do not touch”;Mfor module table. - Formatter: StyLua (https://github.com/JohnnyMorganz/StyLua) — Rust-based, fast, the modern default. lua-fmt (older).
- Linter: luacheck (https://github.com/lunarmodules/luacheck) — catches undefined globals (the #1 Lua bug), unused locals, shadowing.
- Idiomatic:
localeverything (perf and safety); return a module table fromrequired files; useassertandpcalldeliberately (errors are values, not exceptions); favor table-as-data; method syntax (obj:m(x)) only when methods needself; keep_Gclean. - Expert review focus: accidental globals (no
local); 1-based vs 0-based indexing bugs at host boundary;pairsnon-determinism;#ton tables with holes; coroutine resumes losing errors; integer-vs-float division (/vs//in 5.3+); UTF-8 mishandling;__indexchains creating infinite recursion; missing__closeon resource locals.
Ecosystem
- Game engines: LOVE2D (2D), Defold, Solar2D (formerly Corona), Roblox/Luau, Garry’s Mod (LuaJIT), CryEngine scripting, RPG Maker MV+.
- Web/HTTP: OpenResty (Nginx + LuaJIT, used at scale by Cloudflare, GoDaddy), Lapis (web framework on OpenResty), Sailor, Pegasus.
- Editors/tools: Neovim configs (init.lua), TextAdept, ZeroBrane Studio (Lua IDE in Lua), Lite-XL.
- Embedded: NodeMCU (Lua on ESP8266/ESP32), eLua, Mako Server.
- DBs/middleware: Redis (
EVAL), Tarantool (Lua-as-app-server with built-in DB), HAProxy Lua, Wireshark dissectors, Snort, Nmap NSE scripts. - Testing: busted (BDD), luaunit, telescope.
- Docs: LDoc (LuaDoc successor) generates HTML from doc comments.
- Package management: LuaRocks (https://luarocks.org/), Hererocks (LuaRocks env manager).
- Notable users: Adobe Lightroom, Cisco IOS, MediaWiki Scribunto (Wikipedia templates), Cloudflare (LuaJIT in nginx), Blizzard (WoW), Square Enix, Bethesda, Rovio (Angry Birds), Snapchat.
Gotchas
- 1-based indexing —
t[1]is the first element;t[0]isnil(but a valid key). Trips up everyone coming from C/Python. #tis undefined for tables with holes —#{1, nil, 3}could return 1 or 3. Use a counter ortable.pack/select("#", ...)for varargs.- Globals by default —
function foo()creates globalfoo. Alwayslocal function foo(). Use luacheck to catch. pairsorder is unspecified — never rely on iteration order; use sorted keys for determinism.ipairsstops at firstnil—{1, 2, nil, 4}iterates 1, 2 only.- String concat in loops is O(n²) — accumulate in a table and
table.concat(t, sep). - Integer/float split (5.3+) —
5/2is2.5(float division);5//2is2(floor division).math.type(x)distinguishes. Mixing in JSON encoders bites. - Boolean truthiness — only
falseandnilare falsy;0and""are truthy (unlike C/Python/JS). local function f()vslocal f = function()— recursive references differ; thelocal functionform bindsfbefore the body so recursion works.requirecaches modules — modifying after first load won’t reload; clearpackage.loaded[name] = nilandrequireagain.- Coroutine errors — an error inside a coroutine bubbles out via
coroutine.resumereturningfalse, err; easy to drop on the floor. debug.sethookcount mode is non-deterministic across implementations; for security limits, prefer instruction count via JIT or instrument by hand.- Lua 5.1 vs 5.2/5.3/5.4 incompatibilities —
setfenv/getfenvremoved (use_ENV),unpackmoved totable.unpack, integer subtype added in 5.3,<const>/<close>only 5.4+. LuaJIT tracks 5.1 with a few 5.2/5.3 features behind compile flags. - Userdata garbage collection ordering —
__gcruns in reverse construction order; do not assume references are still alive. - Sandbox escape via
string.dump+load— pre-5.2 lets you craft arbitrary bytecode; always setmode="t"(text only) onload.
Citations
- Lua reference manuals (all versions): https://www.lua.org/manual/
- Lua 5.4 reference manual: https://www.lua.org/manual/5.4/
- Lua 5.5 reference manual: https://www.lua.org/manual/5.5/
- Lua versions/history: https://www.lua.org/versions.html
- Programming in Lua (book, free 1st ed): https://www.lua.org/pil/
- Lua-users wiki: http://lua-users.org/wiki/
- LuaRocks: https://luarocks.org/
- LuaJIT: https://luajit.org/
- LuaJIT FFI tutorial: https://luajit.org/ext_ffi_tutorial.html
- LuaJIT FFI semantics: https://luajit.org/ext_ffi_semantics.html
- StyLua formatter: https://github.com/JohnnyMorganz/StyLua
- luacheck linter: https://github.com/lunarmodules/luacheck
- busted testing: https://lunarmodules.github.io/busted/
- Luau (Roblox): https://luau.org/
- Sandboxing guide: http://lua-users.org/wiki/SandBoxes
- OpenResty: https://openresty.org/
- LDoc: https://lunarmodules.github.io/ldoc/