Lua versions drift incompatibly — the runtime you target matters. Lua 5.4 adds integer division
(//), bitwise ops, goto, and a generational GC.
LuaJIT tracks the Lua 5.1 dialect (with selected 5.2 features) but is dramatically faster and ubiquitous in
embedded contexts — Neovim, OpenResty, Wireshark, redis-cli (eval), Roblox, Defold, LÖVE2D. Check
_VERSION before relying on a feature; portability matters.
The only composite. Arrays, maps, modules, objects — all tables.
userdata
Opaque values from C extensions.
thread
Coroutine handle.
Type-checks & coercions
type(x)
Returns the type name as a string.
tonumber("42") / tonumber("ff", 16)
Parse. Returns nil on failure.
tostring(x)
String coerce. Uses __tostring if defined.
10 .. " items"
String concat. Note the space — 10..s is a number-method lookup.
if · for · whileControl flow
if x > 0 then … elseif … then … else … end
Keywords end with end. No braces.
v = cond and a or b
Ternary idiom. Caveat: fails when a is itself falsy.
for i = 1, 10 do … end
Numeric for. Inclusive end.
for i = 10, 1, -1 do … end
Step parameter.
for i, v in ipairs(t) do … end
Array iteration. Stops at first nil.
for k, v in pairs(t) do … end
All keys. Order is undefined.
while cond do … end
Pre-test.
repeat … until cond
Post-test. Note: until not while.
break / goto label / ::label::
Loop exit / labeled jump.
No continue keyword
Use goto continue with ::continue:: at the end of the loop body.
Lua patterns ≠ regexStrings
s = "hello"
Strings are immutable.
[[multi\nline]] / [==[…]==]
Long literals. Bracket levels disambiguate when content contains brackets.
s:len() / #s
Byte length. UTF-8 needs utf8.len (5.3+).
s:upper() / s:lower() / s:reverse()
Method call syntax dispatches to string.
s:sub(1, 3) / s:sub(-3)
Substring. 1-based, inclusive. Negative = from end.
string.format("%5.2f", pi)
printf-style.
s:rep(3, "-")
Repeat with separator.
s:find("pat") / s:match("(%w+)")
Search. Returns indices / captures.
s:gsub("pat", "rep")
Global substitute. Returns new string + count.
for w in s:gmatch("%w+") do … end
Iterator over matches.
Pattern syntax (NOT PCRE)
%a / %A / %d / %D / %s / %S / %w / %W
Letter / non- / digit / non- / space / non- / alnum / non-.
. (dot)
Any byte.
* / + / - / ?
0+ greedy / 1+ greedy / 0+ lazy / 0–1.
^pat$
Anchors.
[abc] / [^abc]
Char class / negated.
()
Capture group.
%1, %2 (in replacement)
Backreferences.
No alternation, no backtracking captures
Lua patterns are intentionally smaller than regex.
The one composite typeTables
t = { 10, 20, 30 }
Array-style. 1-indexed.
t = { name = "Ada", age = 36 }
Hash-style.
t = { 1, 2, 3, label = "primes" }
Mixed. Same table.
t[1] / t.name / t["name"]
Three access forms.
#t
Length of the array part. Stops at first nil — can be ambiguous.
next(t, k)
Low-level iterator. pairs is built on it.
table.insert(t, v) / table.insert(t, 1, v)
Append / insert at index. Shifts.
table.remove(t) / table.remove(t, i)
Pop tail / remove at index.
table.sort(t, cmp)
In-place sort. Optional comparator.
table.concat(t, ", ")
Join array of strings.
t[k] = nil
Remove a key.
{ table.unpack(t) }
Shallow copy of array part.
Worked example
lua
-- Tables are the ONE composite type in Lua.
-- Arrays, hashes, namespaces, objects, modules — all tables.
-- Array-style (1-indexed!)
local fruits = { "apple", "banana", "cherry" }
print(fruits[1]) -- apple
print(#fruits) -- length (3)
-- Hash-style
local user = { name = "Ada", age = 36 }
print(user.name) -- Ada
print(user["name"]) -- same thing
-- Mixed (both at once)
local row = { 1, 2, 3, label = "primes", verified = true }
-- Iterate
for i, v in ipairs(fruits) do -- arrays: stops at first nil
print(i, v)
end
for k, v in pairs(user) do -- any keys, any order
print(k, v)
end
-- Insert / remove
table.insert(fruits, "date") -- append
table.insert(fruits, 1, "apricot") -- insert at index 1
table.remove(fruits, 2) -- remove index 2
-- Sort + concat
table.sort(fruits)
print(table.concat(fruits, ", "))
-- Shallow copy
local copy = { table.unpack(fruits) } -- Lua 5.2+ (or `unpack` in 5.1/LuaJIT)
Closures · multiple returnsFunctions
local function f(a, b) return a + b end
Define. local function hoists the name — recursion works.
local f = function(a, b) return a + b end
Equivalent. Doesn’t hoist — f can’t call itself.
return a, b, c
Multiple return values. First-class.
local x, y = f()
Destructure returns.
local x = (f())
Parens truncate to first value — useful trick.
function f(...) local args = { ... } end
Vararg. select("#", ...) for count, select(n, ...) for tail.
obj:method(arg)
Sugar for obj.method(obj, arg). Injects self.
function obj:method() … end
Mirror sugar on the def side.
Closures capture upvalues by reference
Functions defined inside another see + share its locals.
Hooks · classesMetatables & OOP
setmetatable(t, mt)
Attach a metatable. getmetatable(t) retrieves.
__index = T
Fallback for missing keys. The foundation of OO.
__index = function(t, k) … end
Lazy / computed properties.
__newindex
Hook on assignment to a missing key. Used for read-only tables.
Comparison. __eq only fires when both operands have the same metatable.
__len
Custom #t.
__gc
Finalizer. Runs during garbage collection.
__close
Runs at to-be-closed variable scope exit (5.4).
Worked example
lua
-- Metatables = the hook layer that powers OOP, operator overloading, defaults.
-- 1 · Class via __index — fallback for missing keys
local Vector = {}
Vector.__index = Vector -- when key missing on instance, look here
function Vector.new(x, y)
return setmetatable({ x = x, y = y }, Vector)
end
function Vector:length() -- `:` injects `self`
return math.sqrt(self.x^2 + self.y^2)
end
function Vector:__add(other) -- v + w
return Vector.new(self.x + other.x, self.y + other.y)
end
function Vector:__tostring() -- print(v)
return string.format("Vec(%g, %g)", self.x, self.y)
end
local v = Vector.new(3, 4)
print(v:length()) -- 5.0
print(v + Vector.new(1, 1)) -- Vec(4, 5)
-- 2 · Default-valued table
local function defaults(defaults_tbl)
return setmetatable({}, { __index = defaults_tbl })
end
local config = defaults{ port = 8080, tls = false }
config.tls = true
print(config.port, config.tls) -- 8080 true
Cooperative concurrencyCoroutines
co = coroutine.create(fn)
Create. Not started.
coroutine.resume(co, args…)
Run / continue. Returns ok, ....
coroutine.yield(values…)
Suspend, returning values to the resumer.
coroutine.status(co)
"suspended" | "running" | "dead" | "normal".
coroutine.wrap(fn)
Wraps in a regular function. Errors propagate instead of ok-flag.
The require cache. package.loaded[name] = nil forces a reload.
module … end (5.1)
Legacy Old syntax. Avoid in new code.
_G / _ENV
Global table. Lua 5.2+ uses _ENV as the current env upvalue.
luarocks make / install / list
Build / install / list packages.
rockspec
Per-rock manifest. Like package.json.
pcall · error · assertErrors
error("msg") / error({ code = 42 })
Throw. Any value, not just strings.
error("msg", 2)
Second arg is stack level — report from caller.
assert(cond, "msg")
If cond is falsy, error. Returns cond otherwise — chainable.
local ok, err = pcall(fn, args…)
Protected call. ok is false on error; err is the value passed to error.
local ok, … = xpcall(fn, traceback, args…)
Like pcall but with a message handler. Use debug.traceback for stacks.
No try / catch keywords
Idiomatic Lua wraps risky calls in pcall.
return nil, "reason"
Common convention — return nil-plus-error-string for recoverable failure.
Word counter · ~40 linesEnd-to-end · Word count
Walk a directory via io.popen, tokenize with patterns, accumulate counts in a default-valued table, top-20 by frequency.
Pure stdlib.
lua
-- Save as: word_count.lua
-- Run : lua word_count.lua path/to/dir
--
-- Walk a directory, tally word frequencies across every .txt and .md file,
-- print the top 20. Pure stdlib + `io.popen` for portable directory walk.
local function each_file(root, ext_pat)
local cmd = ('find %q -type f \\( -name "*.txt" -o -name "*.md" \\)'):format(root)
local pipe = assert(io.popen(cmd, "r"))
return function()
local line = pipe:read("*l")
if not line then pipe:close(); return nil end
return line
end
end
local function tokenize(line)
return line:lower():gmatch("[%a']+") -- letters + apostrophe
end
local function topn(map, n)
local list = {}
for word, count in pairs(map) do
list[#list + 1] = { word = word, count = count }
end
table.sort(list, function(a, b) return a.count > b.count end)
for i = 1, math.min(n, #list) do
print(string.format("%6d %s", list[i].count, list[i].word))
end
end
local function main()
local root = arg[1] or "."
local counts = setmetatable({}, { __index = function() return 0 end })
for path in each_file(root) do
local f = io.open(path, "r")
if f then
for line in f:lines() do
for w in tokenize(line) do
counts[w] = counts[w] + 1
end
end
f:close()
end
end
topn(counts, 20)
end
main()
Best practiceGood to know
Declare local everywhere.
Forget the keyword and the binding becomes a global — visible everywhere, slow to access, and prone to clobbering library names. Most lint warnings boil down to this.
Cache method lookups in hot loops.local ins = table.insert before a tight loop avoids the per-iteration table lookup. LuaJIT removes some of this overhead; reference Lua keeps it.
Pick a target dialect early.
Lua 5.4 vs LuaJIT (5.1+) is a fork in the road — integer division, bitwise ops, goto, generational GC vs raw speed and FFI. Embedded contexts (Neovim, OpenResty, game engines) usually pick one for you.
Common trapsWatch out for
Arrays are 1-indexed, but # stops at the first nil.
A "sparse" array with a gap returns one of several lengths. Treat #t as well-defined only for densely-packed arrays.
0 and "" are truthy.
Coming from Python / JS this surprises everyone. Only false and nil are falsy. if x then isn’t the null-check you think it is.
The and / or ternary fails on a falsy true-branch.cond and value or default returns default when value is nil or false — even if cond is true. Use an explicit if when the true branch can be falsy.
Lua is a lightweight, embeddable scripting language widely used for game scripting (Roblox, World of Warcraft add-ons, LÖVE), configuration (NeoVim, Redis scripting, nginx via OpenResty), and embedded systems where a small footprint matters. Its C API makes it easy to embed inside any application.
What is a table in Lua?
A table is Lua's only data structure — it acts as an array, dictionary, object, namespace, and module all at once. Numeric keys form the array part (1-indexed); string or other keys form the hash part. Tables are reference types and the foundation of all OOP in Lua.
What are metatables in Lua?
A metatable is a regular table attached to another table (or userdata) via setmetatable(). Its metamethods (__index, __newindex, __add, __tostring, etc.) let you intercept operations on the parent table. __index enables prototype-based inheritance: if a key is missing, Lua looks it up in the metatable's __index table or calls the __index function.
How do coroutines work in Lua?
Coroutines are cooperative, stackful threads. Create one with coroutine.create(fn) or coroutine.wrap(fn). Resume with coroutine.resume(co, ...) and pause with coroutine.yield(). Unlike OS threads, only one coroutine runs at a time — control transfers explicitly, not preemptively. They are used for async I/O patterns, iterators, and game state machines.
What is the difference between Lua 5.4 and LuaJIT?
Lua 5.4 is the official reference implementation — stable, maintained by PUC-Rio, and ships integers, generational GC, and to-be-closed variables. LuaJIT is a high-performance JIT-compiled fork of Lua 5.1 with a C FFI for calling C libraries directly without binding code. LuaJIT is significantly faster for numeric code but tracks Lua 5.1 semantics, not 5.4.
Is Lua free and open source?
Yes. Lua is MIT-licensed and maintained by PUC-Rio. The reference implementation, LuaJIT, and most libraries are free to use commercially. The Lua runtime is tiny enough to ship inside proprietary products without restriction.