Skip to content

Lua Cheatsheet

Lightweight, embeddable scripting language.

01

Getting Started

Basics & Variables

Lua variables are global by default; use 'local' for scoped variables. Lua supports multiple assignment. Comments use -- (single line) and --[[ ]] (multi-line).

lua
-- comments start with --
print("Hello, World!")

-- variables (global by default)
x = 10
name = "Alice"

-- local variables
local y = 20
local z = x + y

-- multiple assignment
local a, b, c = 1, 2, 3
print(a, b, c)  -- 1  2  3

Comments

Single-line comments start with --. Multi-line comments use --[[ ... ]]. Long-bracket comments support a level number like --[==[ ]==] so they can contain ]] without ending early.

lua
-- single line comment

--[[ multi-line
     comment block ]]
print("hi")

-- long-bracket comments can have = signs for nesting
--[==[ this is a comment with ]] inside ]==]
print("still runs")

Print & Output

print() adds a newline and separates arguments with tabs; io.write() writes raw text with no separator or newline. print() shows nil as 'nil', unlike most operations that error on nil.

lua
print("Hello")           -- prints with a trailing newline
io.write("no newline")   -- writes without a newline

-- print separates multiple args with tabs
print("name", "age")     -- name    age

-- print converts all types (nil included)
print(42, true, nil)     -- 42  true  nil

-- io.stdout / io.stderr are file handles
io.stderr:write("warning\n")

Multiple Assignment

All right-hand values are evaluated before any assignment, enabling clean swaps. Missing values become nil; extra values are silently dropped. Lists adjust to the number of variables.

lua
local a, b = 1, 2
print(a, b)        -- 1  2

a, b = b, a        -- swap without a temp variable
print(a, b)        -- 2  1

local x, y, z = 10          -- y, z become nil
local m, n = 1, 2, 3        -- 3 is discarded
print(m, n)        -- 1  2

Interactive Mode

lua -i launches the interactive interpreter. Prefixing an expression with '=' in the REPL evaluates and prints it, similar to Python's REPL. Scripts run with 'lua file.lua' execute top to bottom.

lua
-- run the REPL:        lua -i
-- run a file:          lua script.lua
-- show the version:    lua -v

-- inside the REPL:
-- > print("hi")
-- hi
-- > = 2 + 3            -- '=' prints an expression value
-- 5
-- > = string.rep("ab", 3)
-- ababab

nil & Variable Deletion

nil is Lua's only 'no value' type. Assigning nil to a table key deletes it. Accessing undeclared globals returns nil rather than raising an error, which can mask typos—use strict.lua to catch them.

lua
local x = 10
print(x)    -- 10
x = nil     -- delete the value
print(x)    -- nil

if x == nil then
  print("x is nil")
end

-- reading an undeclared name returns nil (no error)
print(undefined_var)   -- nil

-- assign nil to let the GC collect a table entry
local t = {a = 1}
t.a = nil
02

Data Types

Eight Basic Types

Lua is dynamically typed; variables have no type, only values do. type() returns a lowercase string naming the value's type. The 8 types are nil, boolean, number, string, table, function, thread, and userdata.

lua
-- Lua has 8 basic types:
print(type(nil))      -- nil
print(type(true))     -- boolean
print(type(42))       -- number
print(type("hi"))     -- string
print(type({}))       -- table
print(type(print))    -- function
print(type(coroutine.create(function() end)))  -- thread
print(type(io.stdin)) -- userdata

-- type() always returns a string
print(type(type(42))) -- string

Numbers

Both integers and floats share the type 'number'; math.type() tells them apart (Lua 5.3+). '/' always does true division (float), while '//' does floor division preserving the operand type.

lua
-- Lua 5.3+ distinguishes integers and floats
local i = 10          -- integer
local f = 3.14        -- float
print(type(i), math.type(i))  -- number  integer
print(type(f), math.type(f))  -- number  float

local hex = 0xFF      -- 255
local exp = 1e3       -- 1000.0 (float)
print(hex, exp)

-- integer / integer with no fraction stays integer
print(7 / 2)   -- 3.5  (true division, always float)
print(7 // 2)  -- 3    (floor division)

Strings

Single and double quotes are equivalent. Long brackets [[ ]] (or [==[ ]==]) create raw multi-line strings where escape sequences are not processed. # gives the byte length, not character count.

lua
local s1 = "double quoted"
local s2 = 'single quoted'   -- identical to double
local s3 = [[
a multi-line
string, escapes ignored]]
print(#s1)          -- 14 (# is the length operator)
print(s1:upper())   -- DOUBLE QUOTED
-- strings are immutable: operations return new strings

Booleans & Truthiness

Lua has only two falsy values: nil and false. Everything else—including 0 and the empty string—is truthy. This differs from C/Python and is a common source of bugs when porting code.

lua
local flag = true
local done = false

-- ONLY nil and false are falsy
-- 0, "", and {} are all truthy (unlike many languages)
if 0 then print("0 is truthy") end
if "" then print("empty string is truthy") end

-- logical operators short-circuit and return a value
local x = nil or "default"   -- "default"
local y = true and "yes"     -- "yes"
print(x, y)

Tables

Tables are associative arrays and the single data structure in Lua, used for arrays, records, objects, and modules. Array indices start at 1 by convention. A table can mix sequence and hash parts.

lua
-- the table is Lua's only composite data structure
-- it serves as array, dictionary, object, and module
local arr = {10, 20, 30}            -- array (1-indexed)
local dict = {name = "Lua", ver = 5.4}
local mixed = {1, 2, x = 3}

print(arr[1])      -- 10  (indices start at 1, not 0)
print(dict.name)   -- Lua
print(mixed.x)     -- 3
print(#arr)        -- 3

Type Checks

type() is the standard way to branch on a value's type. It returns 'nil' for nil rather than erroring. For finer checks use math.type (int vs float) or rawequal for identity comparison.

lua
local function describe(v)
  local t = type(v)
  if t == "table" then
    return "a table"
  elseif t == "string" then
    return "a string: " .. v
  elseif t == "nil" then
    return "nothing"
  end
  return "a " .. t
end

print(describe({}))      -- a table
print(describe("hi"))    -- a string: hi
print(describe(nil))     -- nothing
03

Operators

Arithmetic Operators

Lua supports +, -, *, /, %, unary -, and ^. Note ^ is exponentiation (returns a float), not bitwise XOR. There is no increment (++) operator; use x = x + 1.

lua
print(10 + 3)   -- 13
print(10 - 3)   -- 7
print(10 * 3)   -- 30
print(10 / 3)   -- 3.3333... (true division)
print(10 % 3)   -- 1   (modulo)
print(2 ^ 10)   -- 1024.0 (^ is exponent, NOT xor)
print(-5)       -- -5  (unary minus)

Floor Division & Modulo

// (Lua 5.3+) floors toward negative infinity, unlike C's truncating division. The % operator is defined as a - floor(a/b)*b, so its result always takes the sign of the divisor—useful for wrapping angles and indices.

lua
-- Lua 5.3+ integer floor division
print(10 // 3)    -- 3   (integer)
print(-10 // 3)   -- -4  (floors toward -inf)
print(10.0 // 3)  -- 3.0 (float floor div)

-- modulo result takes the sign of the divisor
print(10 % 3)     -- 1
print(-10 % 3)    -- 2
print(10 % -3)    -- -2

Comparison Operators

Inequality is written ~= (not !=). Comparing values of different types with == or ~= always returns false/true (except nil == nil); ordering comparisons across types raise an error. Strings compare by byte order.

lua
print(3 == 3)    -- true  (equality)
print(3 ~= 4)    -- true  (inequality: ~=, not !=)
print(3 < 4)     -- true
print(3 > 4)     -- false
print(3 <= 3)    -- true
print(3 >= 4)    -- false

print("abc" < "abd")  -- true (lexicographic)
print(1 == "1")       -- false (different types never equal)

Logical Operators

and returns its first operand if falsy, else the second; or returns its first operand if truthy, else the second. They return operands, not booleans. The (c) and x or y idiom breaks if x is false or nil.

lua
-- and / or / not short-circuit and return a value (not a bool)
print(true and 10)       -- 10
print(false or "x")      -- "x"
print(nil or "default")  -- "default"
print(not nil)           -- true

-- idiom for defaults
local name = nil
local display = name or "anonymous"

-- ternary-like (beware falsy 'false' values!)
local v = (cond) and "yes" or "no"

String Concatenation

.. is the only concatenation operator; it always builds a new string (strings are immutable). Numbers are coerced to strings automatically. For building large strings, collect parts in a table and use table.concat—it is far faster than repeated concatenation.

lua
local name = "Lua"
local v = 5.4
print("Hello " .. name .. " " .. v)  -- Hello Lua 5.4

-- .. always creates a new string
local s = "a" .. "b" .. "c"
print(s)  -- abc

-- numbers are auto-converted when concatenated
print("n=" .. 42)             -- n=42

-- repeat a string with string.rep
print(string.rep("ab", 3))    -- ababab

Length Operator

# returns the byte length of a string or the length of a table's sequence part. It is only well-defined for tables without holes (nil gaps). For UTF-8 character counts use lua-utf8 or the utf8 library (Lua 5.3+).

lua
-- # works on strings and the sequence part of tables
print(#"hello")        -- 5
print(#{10, 20, 30})   -- 3

-- caution: # only counts the contiguous sequence part
-- the result is UNDEFINED if the table has holes
local t = {1, 2, nil, 4}
print(#t)   -- 2 or 4 (implementation-defined)

-- # on a string returns its byte length, not codepoints
print(#"cafe")   -- 4
print(#"café")   -- 5 (é is 2 bytes in UTF-8)
04

Control Structures

if-elseif-else

Conditions use then ... end with optional parentheses. There is no switch statement—chain elseif instead. Every if, while, for, and function body must be closed with end.

lua
local score = 85
if score >= 90 then
  print("A")
elseif score >= 80 then
  print("B")
elseif score >= 70 then
  print("C")
else
  print("F")
end
-- keywords: if / then / elseif / else / end
-- parentheses around the condition are optional

while Loop

while do ... end runs zero or more times. The body is a do block, so locals declared inside are scoped to each iteration. Use break to exit the innermost loop.

lua
local i = 1
while i <= 5 do
  print(i)
  i = i + 1   -- Lua has no ++ operator
end

-- the condition is checked before each iteration
local n = 0
while n < 3 do
  print("loop", n)
  n = n + 1
end

repeat-until

repeat-until is a post-test loop that executes the body at least once and exits when the condition is true (the opposite of while). Unlike other blocks, locals declared in the body are visible to the until condition.

lua
-- repeat-until runs the body at least once (like do-while)
-- the loop STOPS when the condition becomes true
local i = 1
repeat
  print(i)
  i = i + 1
until i > 5

-- the condition can see locals declared in the body
local x
repeat
  x = math.random(1, 10)
until x == 7

Numeric for

The numeric for loops i from start to stop inclusive, adding step each time (default 1). The loop variable is fresh and local to each iteration—safe for closures. The limit and step are evaluated once up front.

lua
-- for start, stop[, step]
for i = 1, 5 do
  print(i)        -- 1 2 3 4 5
end

for i = 10, 1, -2 do
  print(i)        -- 10 8 6 4 2
end

-- step defaults to 1; the loop variable is local to the loop
for i = 1, 3 do print(i) end
-- print(i)  -- i is out of scope here

Generic for

ipairs iterates the sequence part and stops at the first nil; pairs iterates every key/value pair in unspecified order. Any iterator function (returning next values or nil to stop) can drive a generic for.

lua
local arr = {"a", "b", "c"}
-- ipairs iterates the array (sequence) part, 1..n
for i, v in ipairs(arr) do
  print(i, v)    -- 1 a / 2 b / 3 c
end

local dict = {x = 1, y = 2}
-- pairs iterates ALL key/value pairs (order not guaranteed)
for k, v in pairs(dict) do
  print(k, v)
end

break & goto

break exits only the innermost loop; there is no continue. Use goto with a label to skip iterations, or restructure with a function and return. goto cannot jump into a local's scope.

lua
-- break exits the innermost loop only
for i = 1, 10 do
  if i == 5 then break end
  print(i)   -- 1 2 3 4
end

-- Lua has NO continue; emulate it with goto
for i = 1, 5 do
  if i % 2 == 0 then goto continue end
  print(i)        -- 1 3 5
  ::continue::
end
-- goto jumps to a label ::name:: within the same scope
05

Strings

String Literals & Escapes

Double and single quotes are equivalent. Escape sequences (newline \n, tab \t) work in quoted strings only. Long brackets [[ ]] or [==[ ]==] produce raw strings where backslashes are literal—handy for regex-like patterns and file paths.

lua
local s1 = "double 'quoted'"
local s2 = 'single "quoted"'
local s3 = "line1\nline2\ttabbed"
local s4 = "percent: %d %%"

-- long brackets: escapes are NOT processed
local s5 = [[raw \n string, \t kept literal]]
print(s3, s5)

Length & Indexing

# returns the byte length. string.sub uses 1-based indices; negative indices count from the end. string.byte returns a numeric code, string.char builds a string from codes.

lua
local s = "hello"
print(#s)              -- 5 (byte length)

-- the string library is 1-indexed
print(s:sub(1, 3))     -- "hel"
print(s:sub(-2))       -- "lo" (negative = from end)
print(s:byte(1))       -- 104 (ASCII of 'h')
print(string.char(104, 105))  -- "hi"

Long Strings

Long-bracket strings preserve embedded newlines and ignore escape sequences. To include ]] inside, use a matching level like [==[ ]==]; the close marker must have the same number of = signs.

lua
-- [[ ]] preserves newlines exactly
local text = [[Line 1
Line 2
Line 3]]
print(text)

-- match bracket level to allow ]] inside
local code = [==[
  if a then ]] -- this ]] is literal
end ]==]
print(code)

String Conversion

tonumber returns nil (not an error) on failure, optionally taking a base (2-36). tostring uses the __tostring metamethod if present. Always check tonumber's result for nil before using it as a number.

lua
local n = tonumber("42")       -- 42 (number)
local f = tonumber("3.14")     -- 3.14
local bad = tonumber("abc")    -- nil (no error raised)
local s = tostring(42)         -- "42"
local b = tostring(true)       -- "true"
local nil_s = tostring(nil)    -- "nil"

print(tonumber("0x1F"))        -- 31 (hex)
print(tonumber("11", 2))       -- 3  (base 2)

String Formatting

string.format mirrors C's printf: %d integer, %f float, %s string, %x hex, %o octal, %% literal percent. Width and precision (.N) work as in C. It is the standard way to embed values into strings.

lua
-- string.format uses C-style format strings
print(string.format("%d + %d = %d", 2, 3, 5))   -- 2 + 3 = 5
print(string.format("%.2f", 3.14159))           -- 3.14
print(string.format("%5d", 42))                 -- "   42"
print(string.format("%-10s|", "hi"))            -- "hi        |"
print(string.format("%x %o", 255, 8))           -- ff 10

String Methods (method syntax)

Any string can call string-library functions via the colon: s:upper() is shorthand for string.upper(s). The method syntax works because strings have a metatable linking them to the string library.

lua
local s = "Hello, World"
-- s:method(x) is sugar for string.method(s, x)
print(s:upper())          -- HELLO, WORLD
print(s:lower())          -- hello, world
print(s:rep(2))           -- Hello, WorldHello, World
print(s:reverse())        -- dlroW ,olleH
print(s:sub(1, 5))        -- Hello
print(s:find("World"))    -- 8  13 (start and end indices)
06

Tables

Creating Tables

Tables are associative arrays. Literal items with no key go into the sequence part starting at index 1; name = value items become hash entries. A single table freely mixes both.

lua
-- array (sequence)
local arr = {10, 20, 30, 40}
-- dictionary (hash)
local p = {name = "Lua", version = 5.4}
-- mixed: both sequence and hash entries
local m = {1, 2, 3, lang = "Lua"}

print(arr[1])        -- 10  (1-indexed!)
print(p.name)        -- Lua
print(p["version"])  -- 5.4
print(#arr)          -- 4

Accessing Elements

t.name is exactly t['name']—use dots for identifier-shaped keys and brackets for keys with spaces, special characters, or computed names. Both forms read and write.

lua
local t = {name = "Lua", ["full name"] = "Lua 5.4"}

-- dot notation works only for valid identifiers
print(t.name)           -- Lua
-- bracket notation works for any string key
print(t["full name"])   -- Lua 5.4

-- bracket form accepts variables
local key = "name"
print(t[key])           -- Lua

t.name = "LuaLang"
t["new key"] = 99

Array Operations

table.insert appends when called with one value, or shifts elements to insert at a given index. table.remove pops the last (or a specified index) and shifts subsequent elements down, returning the removed value.

lua
local t = {10, 20, 30}
table.insert(t, 40)         -- append: {10,20,30,40}
table.insert(t, 1, 5)       -- insert at index 1: {5,10,20,30,40}
print(#t)                   -- 5

local removed = table.remove(t)    -- pop last: 40
local first = table.remove(t, 1)   -- remove index 1: 5
print(removed, first, #t)   -- 40  5  3

-- table.move(src, f, e, t[, dst]) (Lua 5.3+)
local src = {1, 2, 3, 4, 5}
table.move(src, 2, 4, 1)    -- copy elements 2..4 to position 1

Sorting

table.sort sorts the sequence part in place; it is not stable. The optional comparator returns true when the first argument should come first. Omit it for default ascending order (using <).

lua
local nums = {5, 2, 8, 1, 9}
table.sort(nums)
print(table.concat(nums, ","))  -- 1,2,5,8,9

-- custom comparator (return true if a should come before b)
local people = {
  {name = "Bob", age = 30},
  {name = "Ann", age = 25},
}
table.sort(people, function(a, b) return a.age < b.age end)
for _, p in ipairs(people) do print(p.name, p.age) end
-- Ann 25 / Bob 30

Iteration

ipairs walks indices 1, 2, ... until a nil value, so it only sees the array part. pairs visits all keys (array and hash) in an unspecified order. For deterministic order, sort the keys first.

lua
local t = {10, 20, 30, name = "Lua"}

-- ipairs: sequence part, stops at the first nil
for i, v in ipairs(t) do
  print(i, v)    -- 1 10 / 2 20 / 3 30
end

-- pairs: every key/value pair (order not guaranteed)
for k, v in pairs(t) do
  print(k, v)    -- includes name Lua (order varies)
end

-- numeric loop over the sequence
for i = 1, #t do print(t[i]) end

Table Length & Nested Tables

Tables nest naturally—matrix[r][c] indexes a row then a column. # on a nested table gives that row's length. table.concat efficiently joins a sequence of strings (and numbers) with a separator.

lua
local matrix = {
  {1, 2, 3},
  {4, 5, 6},
  {7, 8, 9},
}
print(matrix[2][3])   -- 6 (row 2, col 3)
print(#matrix)        -- 3 (number of rows)
print(#matrix[1])     -- 3 (number of cols)

-- table.concat joins an array of strings/numbers
print(table.concat({"a", "b", "c"}, "-"))  -- a-b-c
print(table.concat({1, 2, 3}, ""))         -- 123
07

Functions

Function Definition

Prefer 'local function' to avoid polluting globals. Functions are values, so they can be assigned, passed, and stored in tables. 'local function name' also allows safe recursion, unlike 'local name = function'.

lua
-- named (global) function
function greet(name)
  return "Hello, " .. name
end

-- local function (preferred)
local function add(a, b)
  return a + b
end

print(greet("Lua"))   -- Hello, Lua
print(add(2, 3))      -- 5

-- functions are first-class values
local f = add
print(f(10, 20))      -- 30

Multiple Return Values

A function can return multiple values. They are adjusted to context: only the last expression in a list expands to multiple values; extra values are dropped and missing ones become nil.

lua
local function minmax(arr)
  local lo, hi = arr[1], arr[1]
  for _, v in ipairs(arr) do
    if v < lo then lo = v end
    if v > hi then hi = v end
  end
  return lo, hi
end

local mn, mx = minmax({3, 1, 4, 1, 5})
print(mn, mx)   -- 1  5

-- values are adjusted: extras dropped, missing become nil
local a = minmax({2, 7})   -- a = 2 (mx discarded)

Variadic Functions

'...' collects all extra arguments. Wrap it in a table ({...}) to iterate, but beware nil holes—use select('#', ...) for the true count and table.pack (5.2+) to preserve n.

lua
local function sum(...)
  local total = 0
  for _, v in ipairs({...}) do
    total = total + v
  end
  return total
end
print(sum(1, 2, 3, 4))   -- 10

-- select('#', ...) = count; select(n, ...) = args from n
local function info(...)
  print(select("#", ...))     -- number of args
  print(select(2, ...))       -- args from position 2 onward
end

Anonymous Functions

Anonymous functions (function ... end) are values used inline. They are central to functional patterns: callbacks, comparators, and closures. Use semicolons/commas to separate them in table literals.

lua
-- functions are first-class: assign to a variable
local double = function(x) return x * 2 end
print(double(21))   -- 42

-- as table fields
local ops = {
  add = function(a, b) return a + b end,
  mul = function(a, b) return a * b end,
}
print(ops.add(2, 3), ops.mul(2, 3))  -- 5  6

-- passed inline to higher-order functions
table.sort({3, 1, 2}, function(a, b) return a > b end)

Functions as Arguments

Functions accepting other functions (map, filter, sort) are higher-order. Lua has no built-in map/filter, but they are trivial to write. Returning #out+1 appends to the end of a sequence.

lua
local function map(arr, fn)
  local result = {}
  for i, v in ipairs(arr) do
    result[i] = fn(v)
  end
  return result
end

local doubled = map({1, 2, 3}, function(x) return x * 2 end)
print(table.concat(doubled, ","))   -- 2,4,6

local function filter(arr, pred)
  local out = {}
  for _, v in ipairs(arr) do
    if pred(v) then out[#out + 1] = v end
  end
  return out
end

Named Arguments via Table

Pass a single table to simulate named/optional arguments. The call syntax f{...} (no parentheses) makes it read like named parameters. Default values come from 'opts.key or default'.

lua
-- Lua has no native named args; pass a table instead
local function create(opts)
  opts = opts or {}
  local name = opts.name or "anonymous"
  local age = opts.age or 0
  return {name = name, age = age}
end

-- call f{...} is sugar for f({...})
local p = create{name = "Alice", age = 30}
print(p.name, p.age)   -- Alice  30
08

Closures

Closure Basics

A closure is a function plus the local variables (upvalues) it captured from its enclosing scope. Each call to make_counter creates a fresh count, so counters are independent and the state is private.

lua
local function make_counter()
  local count = 0
  return function()
    count = count + 1
    return count
  end
end

local c = make_counter()
print(c())   -- 1
print(c())   -- 2
print(c())   -- 3
-- the inner function captures 'count' (an upvalue)

State Encapsulation

Closures provide private state without metatables. balance is an upvalue shared by the methods but inaccessible from outside, giving you encapsulation similar to private fields in OOP.

lua
local function make_account(balance)
  return {
    deposit = function(n) balance = balance + n end,
    withdraw = function(n) balance = balance - n end,
    get = function() return balance end,
  }
end

local acc = make_account(100)
acc.deposit(50)
acc.withdraw(30)
print(acc.get())   -- 120
-- 'balance' is private: only the methods can touch it

Generators via Closures

A closure that mutates captured state on each call acts as a generator. Each call resumes where the last left off. This is simpler than coroutines for stateful iterators without suspension.

lua
local function fibgen()
  local a, b = 0, 1
  return function()
    a, b = b, a + b
    return a
  end
end

local next_fib = fibgen()
for i = 1, 8 do
  io.write(next_fib(), " ")   -- 1 1 2 3 5 8 13 21
end
print()

Memoization

memoize wraps a function with a table-based cache stored as an upvalue. Repeated calls with the same argument return the cached result. Note: nil results need a sentinel because cache[x]==nil also means 'not cached'.

lua
local function memoize(fn)
  local cache = {}
  return function(x)
    if cache[x] == nil then
      cache[x] = fn(x)
    end
    return cache[x]
  end
end

local function slow_square(n)
  -- pretend this is expensive
  return n * n
end
local fast_square = memoize(slow_square)
print(fast_square(5))   -- 25 (computed)
print(fast_square(5))   -- 25 (cached)

Upvalues & Scope

An upvalue is a local variable captured by a nested function. Each call to adder produces a new independent x. do-end blocks create local scope without a control structure, useful for limiting variable lifetime.

lua
local function adder(x)
  -- 'x' is an upvalue for the returned function
  return function(y) return x + y end
end

local add5 = adder(5)
local add10 = adder(10)
print(add5(3))    -- 8
print(add10(3))   -- 13
-- each call to adder creates a separate upvalue

-- block scope with do-end
do
  local tmp = "hidden"
end
-- print(tmp)  -- error: tmp is out of scope
09

Metatables & Metamethods

Setting Metatables

A metatable is a regular table whose special keys (like __index, __add) define behavior for another table. setmetatable attaches one; getmetatable retrieves it. Each table can have at most one metatable.

lua
local t = {}
local mt = {}
setmetatable(t, mt)
print(getmetatable(t) == mt)  -- true

-- setmetatable returns the table, so chain inline
local t2 = setmetatable({key = 1}, {
  __tostring = function(self) return "MyTable" end,
})
print(tostring(t2))   -- MyTable

__index for Lookup

__index fires when a key is absent from the table. If it is a table, Lua looks the key up there (enabling inheritance/defaults). If it is a function, Lua calls it with the table and key.

lua
local defaults = {color = "red", size = 10}
local obj = {color = "blue"}
setmetatable(obj, {__index = defaults})

print(obj.color)   -- blue  (own key wins)
print(obj.size)    -- 10    (falls back to defaults)
print(obj.shape)   -- nil   (not found anywhere)

-- __index can also be a function: __index = function(t, k) ...

__newindex for Assignment

__newindex fires when assigning a key that does not yet exist. Use rawset inside it to store the value without re-triggering __newindex (which would loop). rawget/rawset are the metamethod-bypassing equivalents.

lua
local log = {}
local t = setmetatable({}, {
  __newindex = function(t, k, v)
    log[k] = v        -- record the assignment
    rawset(t, k, v)   -- actually store it (avoid recursion)
  end,
})

t.x = 5    -- triggers __newindex
print(log.x, t.x)   -- 5  5
-- rawset/rawget bypass metamethods

Arithmetic Metamethods

Arithmetic metamethods (__add, __sub, __mul, __div, __mod, __pow, __unm, __idiv, __concat) let tables participate in operators. Lua looks up the metamethod on either operand if the operation isn't natively defined.

lua
local Vec = {}
Vec.__index = Vec
Vec.__add = function(a, b) return Vec.new(a.x + b.x, a.y + b.y) end
Vec.__mul = function(a, n) return Vec.new(a.x * n, a.y * n) end

function Vec.new(x, y)
  return setmetatable({x = x, y = y}, Vec)
end

local v1 = Vec.new(1, 2)
local v2 = Vec.new(3, 4)
local v3 = v1 + v2         -- uses __add
local v4 = v1 * 10         -- uses __mul
print(v3.x, v3.y)          -- 4  6

Comparison Metamethods

Comparison metamethods are __eq, __lt, and __le (Lua derives the others). They only apply when both operands share the same metamethod, and __eq requires both values to be of the same type.

lua
local Money = {}
Money.__index = Money
Money.__lt = function(a, b) return a.cents < b.cents end
Money.__le = function(a, b) return a.cents <= b.cents end
Money.__eq = function(a, b) return a.cents == b.cents end

local function money(cents)
  return setmetatable({cents = cents}, Money)
end

print(money(100) < money(200))   -- true
print(money(100) == money(100))  -- true

__call & __tostring

__call makes a table callable as a function (great for functors and factories). __tostring controls how tostring() and print() render the table. Both dramatically improve the ergonomics of custom types.

lua
local Greeter = {}
Greeter.__index = Greeter
Greeter.__call = function(self, who)
  return "Hi " .. who .. ", I am " .. self.name
end
Greeter.__tostring = function(self)
  return "Greeter(" .. self.name .. ")"
end

local g = setmetatable({name = "Bot"}, Greeter)
print(g("Alice"))      -- Hi Alice, I am Bot
print(tostring(g))     -- Greeter(Bot)
-- __call lets a table be invoked like a function
10

Object-Oriented Programming

Class Simulation

Lua has no classes; objects are tables whose metatable's __index points back to the class table. Methods are looked up via __index. 'Animal.new' is a factory; instances get Animal as their metatable.

lua
local Animal = {}
Animal.__index = Animal

function Animal.new(name)
  local self = setmetatable({}, Animal)
  self.name = name
  return self
end

function Animal:speak()
  return self.name .. " makes a sound"
end

local a = Animal.new("Cat")
print(a:speak())   -- Cat makes a sound

Object Creation & self

The colon syntax defines and calls methods with an implicit self. 'function C:m() ... end' is 'function C.m(self) ... end', and 'obj:m()' is 'C.m(obj)'. This is Lua's OOP idiom.

lua
local Counter = {}
Counter.__index = Counter

function Counter.new()
  return setmetatable({count = 0}, Counter)
end

-- the colon ':' adds an implicit 'self' parameter
function Counter:inc()
  self.count = self.count + 1
end

local c = Counter.new()
c:inc()           -- sugar for Counter.inc(c)
c:inc()
print(c.count)    -- 2

Constructors

Constructors are conventionally named 'new' and return a freshly built instance. 'x or 0' gives named defaults. Method calls use the colon so self refers to the receiver.

lua
local Point = {}
Point.__index = Point

function Point.new(x, y)
  local obj = setmetatable({}, Point)
  obj.x = x or 0
  obj.y = y or 0
  return obj
end

function Point:distance(other)
  local dx, dy = self.x - other.x, self.y - other.y
  return math.sqrt(dx*dx + dy*dy)
end

local p1 = Point.new(0, 0)
local p2 = Point.new(3, 4)
print(p1:distance(p2))   -- 5

Inheritance via Metatables

A subclass is a table whose metatable's __index points to the parent class, so missing methods fall through to the parent. Instances get the subclass as their metatable, forming a lookup chain.

lua
local Animal = {}
Animal.__index = Animal
function Animal.new(name) return setmetatable({name = name}, Animal) end
function Animal:speak() return self.name .. " speaks" end

local Dog = setmetatable({}, {__index = Animal})
Dog.__index = Dog

function Dog.new(name)
  return setmetatable(Animal.new(name), Dog)
end

function Dog:bark() return self.name .. " barks!" end

local d = Dog.new("Rex")
print(d:speak())  -- Rex speaks (inherited from Animal)
print(d:bark())   -- Rex barks! (Dog's own method)

Method Overriding

A subclass overrides a method by defining its own with the same name; the subclass version shadows the parent's. To call the parent implementation explicitly, invoke Parent.method(self, ...).

lua
local Shape = {}
Shape.__index = Shape
function Shape.new() return setmetatable({}, Shape) end
function Shape:area() return 0 end

local Circle = setmetatable({}, {__index = Shape})
Circle.__index = Circle
function Circle.new(r) return setmetatable({r = r}, Circle) end
function Circle:area() return math.pi * self.r * self.r end  -- override

local c = Circle.new(2)
print(c:area())   -- 12.566...
-- call the parent method explicitly: Shape.area(c)

Class-based Inheritance Pattern

A small Class() helper standardizes creation and inheritance: new() builds an instance and calls init if present. This pattern is the basis of many Lua OOP libraries (middleclass, classic, etc.).

lua
local function Class(base)
  local cls = {}
  cls.__index = cls
  if base then setmetatable(cls, {__index = base}) end
  cls.new = function(...)
    local obj = setmetatable({}, cls)
    if obj.init then obj:init(...) end
    return obj
  end
  return cls
end

local Vehicle = Class()
function Vehicle:init(wheels) self.wheels = wheels end
function Vehicle:desc() return self.wheels .. " wheels" end

local Bike = Class(Vehicle)
function Bike:init() Vehicle.init(self, 2) end

print(Bike.new():desc())  -- 2 wheels
11

Coroutines

Creating Coroutines

coroutine.create wraps a function in a thread object (initially suspended). The body does not run until resumed. Coroutines are cooperative—control transfers only at yield/resume points.

lua
local co = coroutine.create(function(a, b)
  print("start", a, b)
  local c = coroutine.yield(a + b)
  print("resumed with", c)
  return "done"
end)

-- coroutine.create returns a thread
print(type(co))            -- thread
print(coroutine.status(co))  -- suspended

Resume & Yield

resume runs the coroutine until the next yield (or return), returning true plus any yielded values. When the function returns, the coroutine is dead and resume returns true with the return values.

lua
local co = coroutine.create(function()
  for i = 1, 3 do
    coroutine.yield(i)
  end
end)

print(coroutine.resume(co))  -- true  1
print(coroutine.resume(co))  -- true  2
print(coroutine.resume(co))  -- true  3
print(coroutine.resume(co))  -- true  (no more values)
print(coroutine.status(co))  -- dead

Coroutine States

A coroutine is suspended when paused at a yield, running when active, normal while another coroutine it resumed is running, and dead after its function returns. coroutine.running() identifies the current thread.

lua
-- states: suspended, running, normal, dead
local co = coroutine.create(function()
  coroutine.yield()
end)
print(coroutine.status(co))  -- suspended
coroutine.resume(co)
print(coroutine.status(co))  -- suspended (yielded)
coroutine.resume(co)
print(coroutine.status(co))  -- dead

-- coroutine.running() returns the current coroutine (nil in main)
print(coroutine.running())   -- nil (main thread)

Bidirectional Value Passing

Values flow both ways: the first resume's arguments reach the function's parameters, subsequent resume arguments are returned by yield, and yield's arguments come back out of resume. This enables generators and cooperative pipelines.

lua
local co = coroutine.create(function(x)
  local y = coroutine.yield(x * 2)   -- sends x*2, receives y
  local z = coroutine.yield(y * 3)   -- sends y*3, receives z
  return z
end)

print(coroutine.resume(co, 10))  -- true  20   (x=10, yields 20)
print(coroutine.resume(co, 5))   -- true  15   (y=5, yields 15)
print(coroutine.resume(co, 7))   -- true  7    (z=7, returns 7)

coroutine.wrap

coroutine.wrap returns a plain function that resumes the coroutine each call—more convenient than create+resume and perfect for iterators. The trade-off: errors raise instead of returning (ok, err), so there's no built-in error flag.

lua
-- wrap returns a function (not a thread); it auto-resumes
-- and propagates errors instead of returning ok
local gen = coroutine.wrap(function()
  for i = 1, 5 do
    coroutine.yield(i * i)
  end
end)

print(gen())  -- 1
print(gen())  -- 4
print(gen())  -- 9

-- ideal for iterator-style use
for v in coroutine.wrap(function()
  coroutine.yield("a"); coroutine.yield("b")
end) do
  print(v)   -- a, then b
  break
end
12

Modules

Defining Modules

The standard pattern: build a local table M, attach functions and fields, then return it. The returned table IS the module. Keeping M local avoids leaking names into the global namespace.

lua
-- file: mymath.lua
local M = {}

function M.square(n) return n * n end
function M.cube(n) return n * n * n end

M.pi = 3.14159

return M
-- a module is just a table returned by the chunk

Using require

require loads a module once and caches it in package.loaded, so the chunk runs only once. Dotted module names map to nested paths. require also handles C extensions and the module's searchers.

lua
-- require loads the module and caches it in package.loaded
local mymath = require("mymath")

print(mymath.square(5))   -- 25
print(mymath.cube(3))     -- 27
print(mymath.pi)          -- 3.14159

-- subsequent requires return the cached table
local m2 = require("mymath")
print(m2 == mymath)       -- true

-- dotted names map to paths: a.b.c -> a/b/c.lua

Module Search Path

package.path is a semicolon-separated list of templates where ? stands for the module name. require tries each template in order. Adjust package.path at startup to point at your library directories.

lua
-- package.path controls where require looks for Lua modules
print(package.path)
-- patterns like ./?.lua;./?/init.lua;? is the module name

-- add a custom directory
package.path = "./libs/?.lua;" .. package.path

-- require("foo") then tries ./libs/foo.lua

-- package.cpath is the equivalent for compiled C modules
-- (?.so on Unix, ?.dll on Windows)

Reloading Modules

Setting package.loaded[name] = nil forces the next require to re-run the chunk, enabling hot reloading during development. Existing references to the old module table remain stale, so re-fetch the module where needed.

lua
-- require caches in package.loaded; clear it to reload
package.loaded["mymath"] = nil
local mymath = require("mymath")   -- re-runs the chunk

-- a hot-reload helper
local function reload(name)
  package.loaded[name] = nil
  return require(name)
end

-- warning: existing references to the old table
-- are NOT updated after a reload

Module Patterns

The modern idiom is to build a local table and return it. Avoid the legacy module() function (it pollutes globals and is removed in 5.2+). Exporting only selected names keeps the public API clean.

lua
-- pattern 1: return a table (most common)
local M = {}
function M.fn() end
return M

-- pattern 2: keep helpers local, export a clean table
local function helper() end
local function main_fn() helper() end
return { main = main_fn }

-- pattern 3: module() (legacy, Lua 5.1) -- deprecated
-- module("foo"); function _M.fn() end
13

Error Handling

error()

error() raises an error, normally with a string message. The optional level controls where the error points: 1 (default) is the error() call; 2 blames the caller—useful inside libraries. Non-string errors are allowed and useful for typed exceptions.

lua
local function divide(a, b)
  if b == 0 then
    error("division by zero")   -- raises an error
  end
  return a / b
end

-- the level argument controls the reported position
error("msg", 0)  -- no position info added
error("msg", 1)  -- points to the caller of error (default)
error("msg", 2)  -- points to the caller's caller

-- error() can throw non-string objects too
error({code = 42, msg = "x"})

pcall (protected call)

pcall runs a function in protected mode, returning true plus results on success, or false plus the error on failure. It is the primary tool for safe error handling since Lua lacks try/catch.

lua
local function risky(x)
  if x < 0 then error("negative") end
  return math.sqrt(x)
end

local ok, result = pcall(risky, -1)
if ok then
  print("result:", result)
else
  print("error:", result)   -- error: negative
end

local ok2, r2 = pcall(risky, 16)
print(ok2, r2)   -- true  4

xpcall & traceback

xpcall is like pcall but takes an error handler that can build a full traceback (via debug.traceback) before the stack unwinds. From Lua 5.3 it accepts arguments after the handler.

lua
local function handler(err)
  return debug.traceback("Error: " .. tostring(err), 2)
end

local function f() error("boom") end

-- xpcall takes a handler; Lua 5.3+ also accepts arguments
local ok, err = xpcall(f, handler)
print(ok, err)   -- false  Error: boom\nstack traceback:...

-- xpcall with arguments (Lua 5.3+)
local function risky(x) if x < 0 then error("x") end return x end
local ok2, r2 = xpcall(risky, handler, 9)
print(ok2, r2)   -- true  9

assert

assert returns its first argument if it is not nil/false, otherwise raises an error with the given (or default) message. It is the idiomatic way to check results of functions like io.open that return nil on failure.

lua
-- assert(v, msg) raises if v is nil or false
local function load_config(path)
  local f = assert(io.open(path, "r"))  -- raises if file missing
  return f:read("*a")
end

local n = assert(tonumber("42"), "not a number")  -- 42
-- assert(nil, "bad input")  -- raises: bad input

-- common idiom: assert on results that return nil on failure
local line = assert(f:read("*L"))

Error Levels

The error level tells Lua which call frame to report. Libraries should use level 2 so the user sees their own call site, not the internal error() line. Level 0 omits file/line info entirely.

lua
local function check(x)
  if type(x) ~= "number" then
    -- level 2 blames the caller of check, not check itself
    error("expected number, got " .. type(x), 2)
  end
  return x * 2
end

-- level 0: add no position information
-- level 1: point at the error() call (default)
-- level 2: point at who called the function containing error()
14

File I/O

Simple I/O

The simple I/O model operates on default streams and is convenient for one-off reads/writes. io.read's mode controls what is returned: '*l' for a line, '*a' for everything, '*n' for a number, or a number for that many bytes.

lua
-- the simple model uses default input/output streams
io.write("Enter your name: ")
local name = io.read()        -- read a line
print("Hi " .. name)

-- io.read modes:
-- "*n"  a number
-- "*a"  the entire remaining input
-- "*l"  a line (default, no newline)
-- "*L"  a line including the newline
local n = io.read("*n")
local all = io.read("*a")

Writing Files

io.output sets the default output file; subsequent io.write calls go there. For append or read/write modes, use io.open to get a handle. Always close files to flush buffers and free resources.

lua
io.output("out.txt")       -- set the default output file
io.write("line one\n")
io.write("line two\n")
io.close()                 -- close the default output

-- append mode needs io.open
local f = io.open("out.txt", "a")
f:write("appended\n")
f:close()

File Handles

io.open returns nil plus an error message on failure (not an exception), so check the first return value. The handle's methods mirror io functions but operate on that specific file: read, write, lines, seek, close.

lua
-- io.open returns a file handle (or nil + error message)
local f, err = io.open("data.txt", "r")
if not f then
  error("could not open: " .. err)
end

local content = f:read("*a")   -- read the entire file
print(content)
f:close()                      -- always close handles

-- handle methods: f:read, f:write, f:lines, f:seek, f:close

Reading Line by Line

f:lines() returns an iterator for the handle's lines—you must close f yourself. io.lines(filename) is a convenience that opens, iterates, and closes the file automatically, ideal for one-off line processing.

lua
local f = assert(io.open("data.txt", "r"))
for line in f:lines() do
  print(line)
end
f:close()   -- f:lines does NOT close the handle

-- io.lines(path) opens, iterates, and closes automatically
for line in io.lines("data.txt") do
  print(line)
end

File Modes

Modes mirror C's fopen: 'r', 'w', 'a' for read/write/append, optionally '+' for read-write and 'b' for binary. On most systems 'b' has no effect because Lua treats files as binary by default.

lua
-- io.open(path, mode) modes:
-- "r"  read (default); file must exist
-- "w"  write; truncate or create
-- "a"  append; create if missing
-- "r+" read/write; file must exist
-- "w+" read/write; truncate or create
-- "a+" read/append; create if missing
-- append "b" for binary on some systems: "rb", "wb"

local f = io.open("log.txt", "a")
f:write(os.date(), " started\n")
f:close()

File Positioning

f:seek moves the read/write position. 'set' is absolute from the start, 'cur' is relative to the current position, 'end' is from the end. seek('end') with no offset returns the file size.

lua
local f = assert(io.open("data.txt", "r"))

-- f:seek(whence, offset)
-- "set": from the start, "cur": from current, "end": from end
print(f:seek("end"))      -- file size (position at end)
f:seek("set", 0)          -- rewind to the start

local first_line = f:read("*l")
print(first_line)
f:close()
15

Pattern Matching

Basic Patterns (Lua patterns, not regex)

Lua patterns look like regex but are simpler and use % for escapes and character classes (not \). They have no alternation (|) or non-greedy quantifiers beyond '-', but support captures and are sufficient for most parsing tasks.

lua
-- Lua patterns are NOT regular expressions; they use % not \
local s = "hello world"
print(s:find("wor"))       -- 7  9  (start and end indices)
print(s:find("xyz"))       -- nil

-- . matches any single character
print(("abc"):match("."))  -- a

-- string.find / gmatch / gsub / match all use patterns
print(("x = 42"):match("%d+"))   -- 42

Character Classes

Predefined classes start with %: %a, %d, %s, %w, %p, %l, %u, %c, %x, and their uppercase negations. Custom sets use [...] like regex. % is also the escape for magic characters: %., %%, %$.

lua
-- %a letters, %d digits, %s whitespace, %w alnum
-- %p punctuation, %l lower, %u upper, %c control
-- uppercase = negation: %A non-letters, %D non-digits
print(("a1 b2"):match("%a%d"))   -- a1
print(("x = 42"):match("%d+"))   -- 42

-- custom sets: [abc], [a-z], [^0-9]
print(("a-b"):match("[a-z]"))    -- a
print(("phone 555-1234"):match("%d+-%d+"))  -- 555-1234

Captures

Parentheses in a pattern capture the matched text; match returns them in order, and gmatch yields them per iteration. An empty capture () returns the numeric position in the string where it matched.

lua
-- parentheses () create captures
local s = "name=Alice age=30"
local k, v = s:match("(%w+)=(%w+)")
print(k, v)   -- name  Alice

-- gmatch iterates over all matches
for key, val in ("a=1 b=2 c=3"):gmatch("(%w+)=(%w+)") do
  print(key, val)   -- a 1 / b 2 / c 3
end

-- an empty capture () returns the current position
print(("abc"):match("()b"))   -- 2

gsub Replacement

gsub returns the new string and the count of replacements. The replacement can be a string (with %0/%1.. for captures), a table (looked up by the capture), or a function (called per match). A fourth argument caps the count.

lua
local s = "hello world"
print(s:gsub("o", "0"))     -- hell0 w0rld  2  (string, count)

-- replacements can reference captures %1, %2
print(("2024-01-15"):gsub("(%d+)-(%d+)-(%d+)", "%3/%2/%1"))  -- 15/01/2024  1

-- a function receives captures and returns the replacement
print(("1 2 3"):gsub("%d", function(d) return d * 2 end))  -- 2 4 6  3

-- a 4th argument limits the number of replacements
print(("aaaa"):gsub("a", "b", 2))   -- bbaa  2

Anchors & Pattern Items

Anchors ^ and $ tie a match to the string's start or end. '*' and '+' are greedy, '-' is the lazy (non-greedy) variant, and '?' makes the previous item optional. Use '-' to match the shortest possible text.

lua
-- ^ anchors to the start, $ anchors to the end
print(("hello"):match("^he"))     -- he
print(("hello"):match("lo$"))     -- lo
print(("hello"):match("^hello$")) -- hello

-- pattern items:
-- *  zero or more (greedy)
-- +  one or more (greedy)
-- -  zero or more (lazy)
-- ?  zero or one
print(("aXXXb"):match("a(%a+)b"))       -- XXX (greedy)
print(("<!--c-->"):match("<!--(.-)-->")) -- c (lazy)
16

Standard Library

string library

The string library covers searching (find/match/gmatch/gsub), formatting (format), and manipulation (sub/rep/upper/lower/reverse/byte/char). Lua 5.3+ also adds string.pack/unpack for binary data and the utf8 library.

lua
local s = "Lua"
print(s:upper(), s:lower(), s:len())   -- LUA  lua  3
print(s:rep(3))                          -- LuaLuaLua
print(s:reverse())                       -- auL
print(s:sub(1, 2))                       -- Lu
print(s:byte(1), string.char(76))        -- 76  L
print(string.format("%s %d", "v", 5))    -- Lua v 5
-- full set: find, gmatch, gsub, match, format,
-- rep, sub, byte, char, len, upper, lower, pack, unpack

table library

The table library operates on the sequence part: insert, remove, sort, concat, and (5.3+) move. table.pack wraps varargs into a table with an n field (preserving nils); table.unpack is the reverse, expanding a table into a list.

lua
local t = {1, 2, 3}
table.insert(t, 4)          -- {1,2,3,4}
table.insert(t, 1, 0)       -- {0,1,2,3,4}
table.remove(t, 1)          -- removes 0, returns it
table.sort(t)               -- sort in place
print(table.concat(t, ",")) -- 1,2,3,4

-- Lua 5.2+: table.pack / table.unpack
local args = table.pack(1, 2, 3)  -- {1,2,3, n=3}
print(table.unpack(args))         -- 1 2 3

-- Lua 5.3+: table.move(src, f, e, t[, dst])

math library

math provides constants (pi, huge), rounding (floor/ceil), trigonometry in radians, random numbers (seed with randomseed), and max/min/abs/sqrt. math.type distinguishes integer from float on Lua 5.3+.

lua
print(math.pi, math.huge)        -- 3.1415926535898  inf
print(math.max(1, 5, 3))         -- 5
print(math.floor(3.7), math.ceil(3.2))  -- 3  4
print(math.abs(-5))              -- 5
print(math.sqrt(16))             -- 4.0
print(math.sin(math.pi / 2))     -- 1.0 (radians)
print(math.random(1, 100))       -- random int in [1,100]
math.randomseed(os.time())       -- seed the generator
print(math.type(3), math.type(3.0))  -- integer  float

io library

io provides the simple model (io.read/io.write on default streams) and the full model (io.open handles with f:read/f:write/f:lines/f:seek). io.stdin, io.stdout, io.stderr are predefined handles; io.tmpfile creates a temporary file.

lua
-- default input/output streams
io.write("to stdout\n")
local line = io.read("*l")     -- read a line

-- file handles
local f = io.open("x.txt", "w")
f:write("data\n")
f:close()

-- io.stdin, io.stdout, io.stderr are handles
io.stderr:write("an error\n")

-- io.tmpfile() returns a handle to an auto-deleted temp file
local tmp = io.tmpfile()
tmp:write("scratch")
tmp:seek("set", 0)

os library

os covers time/date (time, date, difftime, clock), the environment (getenv), and process control (execute, exit, tmpname, rename, remove). io.popen runs a command and returns a handle to read its output.

lua
print(os.time())              -- current timestamp (seconds)
print(os.date("%Y-%m-%d %H:%M"))  -- formatted date
local t = os.date("*t")       -- table: year, month, day, hour...
print(t.year, t.month, t.day)

print(os.getenv("PATH"))      -- an environment variable (or nil)
os.execute("ls")              -- run a shell command
local h = io.popen("date")    -- capture command output
print(h:read("*a"))
h:close()

Debug library

The debug library offers introspection: traceback for stacks, getinfo for function/frame metadata, getlocal/setlocal for variables, and setmetatable/getmetatable that bypass metamethod restrictions. Use sparingly—it can break invariants.

lua
-- debug.traceback returns a call-stack string
local function f() error("x") end
print(xpcall(f, function(e) return debug.traceback(e) end))

-- debug.getinfo returns info about a function or stack frame
local info = debug.getinfo(print)
print(info.what, info.name)   -- C  print

-- debug.getlocal / setlocal inspect stack locals
local function locals()
  local a = 1
  local name, value = debug.getlocal(1, 1)
  print(name, value)          -- a  1
end
locals()
17

Lua & C Interop

C API Overview

Embedding Lua in C: create a lua_State with luaL_newstate, open the standard libraries, then run code with luaL_dostring/do_file. Every state is independent and must be closed to release memory.

lua
// Lua is embedded via a C library; the core object is lua_State
// Headers: lua.h, lauxlib.h, lualib.h
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"

int main(void) {
  lua_State *L = luaL_newstate();    // create a state
  luaL_openlibs(L);                  // open the standard libs
  luaL_dostring(L, "print('hi')");   // run Lua source
  lua_close(L);                      // free the state
  return 0;
}

Stack-based API

The C API is stack-based: push values to pass them in, read results off the stack, and pop to clean up. Indices are 1-based from the bottom or negative from the top (-1 is the top). lua_gettop returns the stack size.

lua
// all communication with Lua goes through a stack
lua_State *L = luaL_newstate();

// push values
lua_pushinteger(L, 42);
lua_pushstring(L, "hello");
// stack now: bottom[42]["hello"]top

// inspect (1-based from bottom, negative from top)
int top = lua_gettop(L);            // 2
lua_Integer n = lua_tointeger(L, 1); // 42
const char *s = lua_tostring(L, 2);  // "hello"

lua_pop(L, 2);                      // pop 2 values

Calling Lua from C

To call a Lua function from C: push it with lua_getglobal, push the arguments, then call lua_pcall with the argument and result counts. lua_pcall returns a status; on error the message is on top of the stack.

lua
// script.lua:  function add(a, b) return a + b end
luaL_dofile(L, "script.lua");

lua_getglobal(L, "add");   // push the function 'add'
lua_pushinteger(L, 3);     // arg 1
lua_pushinteger(L, 4);     // arg 2

// lua_pcall(L, nargs, nresults, msgh)
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
  fprintf(stderr, "error: %s\n", lua_tostring(L, -1));
} else {
  lua_Integer sum = lua_tointeger(L, -1);  // 7
  lua_pop(L, 1);                           // pop the result
}

Exposing C Functions to Lua

A C function callable from Lua reads its arguments from the stack (using luaL_check* for type-checked reads), pushes results, and returns the count of results. Register it via lua_pushcfunction + lua_setglobal.

lua
// a Lua-callable C function has signature: int fn(lua_State *L)
static int l_square(lua_State *L) {
  lua_Integer n = luaL_checkinteger(L, 1);  // read arg 1, type-checked
  lua_pushinteger(L, n * n);                // push the result
  return 1;                                 // number of results
}

// register it so Lua can call it
lua_pushcfunction(L, l_square);
lua_setglobal(L, "square");

// now in Lua: print(square(5))  ->  25

Module Registration

A C module is a shared library exporting luaopen_<modname>. luaL_newlib builds a table from a luaL_Reg array. Lua's require finds it via package.cpath and the luaopen_ entry point, then returns the table as the module.

lua
// register a table of C functions as a Lua module
static const luaL_Reg mylib[] = {
  {"square", l_square},
  {"cube",   l_cube},
  {NULL, NULL}    // sentinel marks the end
};

int luaopen_mylib(lua_State *L) {
  luaL_newlib(L, mylib);   // create a table and register the funcs
  return 1;                // return that table (the module)
}

// in Lua: local mylib = require("mylib"); print(mylib.square(5))
// compiled as a shared lib (.so/.dll) found via package.cpath
18

LuaJIT & FFI

LuaJIT Overview & jit.status

LuaJIT is a trace-based JIT for Lua 5.1 that can be ~10-100x faster. The global 'jit' table is present only under LuaJIT, so 'if jit then' cleanly detects it. jit.version, jit.status, jit.os, and jit.arch describe the build.

lua
-- LuaJIT is a Just-In-Time compiler for Lua 5.1, much faster
-- detect LuaJIT at runtime
if jit then
  print(jit.version)        -- e.g. LuaJIT 2.1.0-beta3
  print(jit.status())       -- true if JIT compilation is on
  print(jit.os, jit.arch)   -- e.g. Linux  x64
end

-- 'jit' is nil on standard PUC Lua, so this guard is safe
if not jit then
  print("running plain Lua")
end

JIT Control

jit.on/off/flush control compilation globally; passing a function targets just that function. Disabling JIT for a specific hot path can help diagnose miscompilations or measure interpreter-only performance.

lua
jit.on()     -- enable JIT compilation globally
jit.off()    -- disable it (fall back to the interpreter)
jit.flush()  -- flush the compiled-code cache

-- control compilation per-function
local function hot()
  local s = 0
  for i = 1, 1e6 do s = s + i end
  return s
end
jit.off(hot)   -- never compile this function
-- jit.on(hot) to re-enable
-- useful for debugging or working around compiler bugs

FFI: Loading a C Library

The FFI library lets LuaJIT call C functions with no binding code. ffi.C accesses the default C namespace (typically libc); ffi.load loads a named shared library. Calls are JIT-compiled to native code, so they are very fast.

lua
local ffi = require("ffi")

-- ffi.C is the default C namespace (libc on most systems)
-- ffi.load("name") loads a shared library explicitly
local C = ffi.C
local libc = ffi.load("c")

-- many standard C functions are available right away
print(ffi.C.time(nil))     -- current Unix timestamp
-- ffi.load("m") for libm, ffi.load("ssl") for OpenSSL, etc.

FFI: Type Declarations (cdef)

ffi.cdef declares C types, structs, and function prototypes (similar to a header). ffi.new allocates a C object; ffi.C (or a loaded library) calls the declared functions. C structs are usable as Lua values with near-C speed.

lua
local ffi = require("ffi")

-- declare C types and prototypes with ffi.cdef
ffi.cdef[[
  typedef struct { int x, y; } Point;
  int printf(const char *fmt, ...);
]]

-- create and use a C struct directly
local p = ffi.new("Point", {x = 1, y = 2})
print(p.x, p.y)            -- 1  2
ffi.C.printf("p = (%d, %d)\n", p.x, p.y)  -- calls libc printf

FFI: Calling C Functions

After cdef declares a prototype, ffi.C.name calls the C function. C numbers map to Lua numbers; C strings (char*) must be converted with ffi.string to become Lua strings. Under the JIT these calls have minimal overhead.

lua
local ffi = require("ffi")

ffi.cdef[[
  double sqrt(double x);
  int abs(int x);
  const char *getenv(const char *name);
]]

-- call C's math functions directly (very fast under JIT)
local root = ffi.C.sqrt(16.0)   -- 4.0
local pos = ffi.C.abs(-7)       -- 7
print(root, pos)

-- ffi.string converts a C string (const char *) to a Lua string
local path = ffi.string(ffi.C.getenv("PATH"))
print(#path)

FFI: Callbacks from C to Lua

ffi.cast turns a Lua function into a C function pointer usable as a callback. C arrays are zero-indexed. Callbacks are expensive and must be freed with :free() when no longer needed (they pin Lua objects).

lua
local ffi = require("ffi")

ffi.cdef[[
  typedef int (*cmp_fn)(const void *, const void *);
  void qsort(void *base, size_t n, size_t sz, cmp_fn cmp);
]]

-- create a C callback that calls back into Lua
local arr = ffi.new("int[?]", 5, {5, 3, 1, 4, 2})
local cmp = ffi.cast("cmp_fn", function(a, b)
  return ffi.cast("int*", a)[0] - ffi.cast("int*", b)[0]
end)

ffi.C.qsort(arr, 5, ffi.sizeof("int"), cmp)
for i = 0, 4 do io.write(arr[i], " ") end   -- 1 2 3 4 5
cmp:free()   -- free the callback when done
19

Lua in Redis & Nginx

Redis EVAL

Redis executes Lua scripts atomically—no other command runs during the script. KEYS and ARGV pass data in; the return value is converted to a Redis reply. Use redis.call for hard failures, redis.pcall to handle errors in Lua.

lua
-- Redis runs Lua scripts via EVAL; keys are KEYS, args are ARGV
-- the script returns a single value to Redis

-- run in redis-cli:
-- EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey hello

local key = KEYS[1]
local val = ARGV[1]
redis.call('SET', key, val)
return redis.call('GET', key)   -- "hello"

-- redis.call:  errors abort the script
-- redis.pcall: errors are returned as a table

Redis KEYS & ARGV

Pass key names through KEYS so Redis Cluster can route them correctly; non-key data goes in ARGV. Scripts are cached server-side, so loading once with SCRIPT LOAD and re-running with EVALSHA avoids resending the source.

lua
-- KEYS[1..N] = keys passed to EVAL (N is EVAL's 2nd argument)
-- ARGV[1..M] = additional arguments
-- EVAL "..." numkeys key1 key2... arg1 arg2...

-- example: sum the values at multiple keys
local total = 0
for i = 1, #KEYS do
  local v = tonumber(redis.call('GET', KEYS[i]))
  if v then total = total + v end
end
return total

-- use SCRIPT LOAD + EVALSHA to cache and reuse scripts

OpenResty / Nginx overview

OpenResty bundles Nginx with LuaJIT and the ngx_lua module, letting you write request handlers in Lua. Directives like content_by_lua_block, access_by_lua, and rewrite_by_lua hook Lua into Nginx's request phases.

lua
-- OpenResty embeds LuaJIT in Nginx via ngx_http_lua_module
-- content_by_lua_block runs Lua to generate the response

-- nginx.conf:
-- location /hello {
--   content_by_lua_block {
--     ngx.say("hello from lua")
--   }
-- }

-- ngx.say writes to the response body (with a newline)
-- ngx.print writes without a newline
ngx.status = 200
ngx.say("Hello, ", ngx.var.arg_name)

ngx API

ngx.var reads Nginx variables (uri, args, http_* headers); ngx.req exposes the request (method, headers, body, uri args). ngx.header sets response headers. Call ngx.say/ngx.print to emit the body.

lua
-- ngx.var.* accesses Nginx variables
local uri = ngx.var.uri
local ua = ngx.var.http_user_agent

-- ngx.req: the request API
ngx.req.read_body()
local body = ngx.req.get_body_data()

-- request headers (table, case-insensitive keys)
local h = ngx.req.get_headers()
print(h["Content-Type"])

-- set response headers, then write the body
ngx.header["X-Custom"] = "yes"
ngx.say("uri=", uri)

ngx.location.capture

ngx.location.capture issues a non-blocking subrequest to another Nginx location and returns status, header, and body. capture_multi runs several in parallel. Subrequests don't go to the client—they're an internal composition tool.

lua
-- issue subrequests to other Nginx locations
local res = ngx.location.capture("/api/users")
if res.status == 200 then
  ngx.say(res.body)
end

-- capture_multi: parallel subrequests
local res1, res2 = ngx.location.capture_multi{
  {"/api/a"},
  {"/api/b"},
}
ngx.say(res1.body, res2.body)
-- subrequests are internal; great for composing upstream services

Shared Dict (ngx.shared)

Shared dicts (lua_shared_dict) are thread-safe, in-memory stores shared across Nginx worker processes. They support atomic ops like incr and add, making them ideal for rate limiting, caching, and locks in OpenResty apps.

lua
-- in nginx.conf:  lua_shared_dict cache 10m;
-- access a dict shared across worker processes
local cache = ngx.shared.cache
cache:set("user:1", "Alice", 60)   -- key, value, ttl in seconds
local name = cache:get("user:1")   -- "Alice"

cache:incr("hits", 1, 0)           -- atomic increment, init 0
cache:delete("old")

-- safe for concurrent access across Nginx workers
-- common uses: rate limiting, caching, distributed locks
local locked = cache:add("lock:job", 1, 30)  -- set only if absent

Was this helpful?