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.

REPLlua 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/literalsnil, 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 flowif 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).

Moduleslocal 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 handlingerror("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 primitivesCoroutines, 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/Oio.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 highlightsstring, 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.

Reflectiondebug 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 + sandboxingdebug.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.dumpffi.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.

Bytecodeluac -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 tuningcollectgarbage("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 embeddingsRoblox 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_case for variables/functions in Lua-the-language tradition; PascalCase for “classes” (tables used as types); _PRIVATE convention for “do not touch”; M for 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: local everything (perf and safety); return a module table from required files; use assert and pcall deliberately (errors are values, not exceptions); favor table-as-data; method syntax (obj:m(x)) only when methods need self; keep _G clean.
  • Expert review focus: accidental globals (no local); 1-based vs 0-based indexing bugs at host boundary; pairs non-determinism; #t on tables with holes; coroutine resumes losing errors; integer-vs-float division (/ vs // in 5.3+); UTF-8 mishandling; __index chains creating infinite recursion; missing __close on 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 indexingt[1] is the first element; t[0] is nil (but a valid key). Trips up everyone coming from C/Python.
  • #t is undefined for tables with holes#{1, nil, 3} could return 1 or 3. Use a counter or table.pack/select("#", ...) for varargs.
  • Globals by defaultfunction foo() creates global foo. Always local function foo(). Use luacheck to catch.
  • pairs order is unspecified — never rely on iteration order; use sorted keys for determinism.
  • ipairs stops at first nil{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/2 is 2.5 (float division); 5//2 is 2 (floor division). math.type(x) distinguishes. Mixing in JSON encoders bites.
  • Boolean truthiness — only false and nil are falsy; 0 and "" are truthy (unlike C/Python/JS).
  • local function f() vs local f = function() — recursive references differ; the local function form binds f before the body so recursion works.
  • require caches modules — modifying after first load won’t reload; clear package.loaded[name] = nil and require again.
  • Coroutine errors — an error inside a coroutine bubbles out via coroutine.resume returning false, err; easy to drop on the floor.
  • debug.sethook count 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 incompatibilitiessetfenv/getfenv removed (use _ENV), unpack moved to table.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__gc runs 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 set mode="t" (text only) on load.

Citations