The Moon Language — Fast, Embeddable, Elegant
The most common Lua syntax at a glance. Lua is minimal by design — the entire reference manual fits in ~100 pages.
Global by default; use local for scoping
local name = "Lua"
local version = 5.4
local ready = true
local nothing = nil
if/elseif/else, while, for, repeat
if x > 0 then
print("positive")
elseif x == 0 then
print("zero")
else
print("negative")
end
First-class; closures; multiple returns
local function add(a, b)
return a + b
end
-- anonymous / lambda
local sq = function(x)
return x * x
end
Numeric for, generic for, while, repeat
for i = 1, 10 do
print(i)
end
for k, v in pairs(t) do
print(k, v)
end
The universal data structure
local arr = {1, 2, 3}
local map = {
name = "Lua",
year = 1993,
}
print(arr[1]) -- 1 (1-indexed!)
print(map.name) -- "Lua"
Immutable; concatenate with ..
local s = "hello" .. " world"
print(#s) -- 11 (length)
print(s:upper()) -- HELLO WORLD
print(s:sub(1,5)) -- hello
Lua is dynamically typed. Variables don't have types — values do. There are exactly eight basic types.
| Type | Description | Example |
|---|---|---|
nil | Absence of value; the only type with a single value | nil |
boolean | Two values only | true, false |
number | Integers & floats (both subtypes in 5.4) | 42, 3.14, 0xFF |
string | Immutable byte sequences | "hello", 'world' |
table | Associative array / object / namespace | {1, 2, x=3} |
function | First-class closures | function() end |
userdata | C-allocated memory block | (from C API) |
thread | Coroutine handle | coroutine.create(f) |
x = 10 -- global (avoid this!)
local y = 20 -- local to current block
do
local z = 30 -- local to this do-end block
print(z) -- 30
end
print(z) -- nil (z is out of scope)
-- Multiple assignment
local a, b, c = 1, 2, 3
a, b = b, a -- swap in one line
local. Globals pollute the shared environment, are slower (global table lookup vs register access), and cause subtle bugs in larger programs. Luacheck and other linters will warn about implicit globals.
-- Lua 5.4 distinguishes integers and floats
type(42) -- "number"
type(3.14) -- "number"
math.type(42) -- "integer"
math.type(3.14) -- "float"
-- Integer arithmetic stays integer
7 // 2 -- 3 (floor division)
7 % 2 -- 1 (modulo)
7 / 2 -- 3.5 (true division always returns float)
2 ^ 10 -- 1024.0 (exponent always returns float)
-- Bitwise operators (5.3+)
0xFF & 0x0F -- 15 (AND)
0xFF | 0x0F -- 255 (OR)
~0 -- -1 (NOT)
1 << 4 -- 16 (left shift)
-- Only nil and false are falsy
-- Everything else is truthy, INCLUDING 0 and ""
if 0 then print("truthy!") end -- prints!
if "" then print("truthy!") end -- prints!
if nil then print("nope") end -- doesn't print
if false then print("nope") end -- doesn't print
-- Idiomatic default values
local name = input or "default"
local debug = verbose and true or false
Tables are Lua's only compound data structure. They serve as arrays, dictionaries, records, objects, namespaces, and modules — all at once.
-- Arrays are tables with consecutive integer keys starting at 1
local fruits = {"apple", "banana", "cherry"}
print(fruits[1]) -- "apple" (1-indexed!)
print(#fruits) -- 3 (length operator)
-- Append
fruits[#fruits + 1] = "date"
table.insert(fruits, "elderberry")
-- Remove
table.remove(fruits, 2) -- removes "banana", shifts down
table.remove(fruits) -- removes last element
-- Iterate in order
for i, fruit in ipairs(fruits) do
print(i, fruit)
end
-- Sort
table.sort(fruits)
table.sort(fruits, function(a, b) return a > b end)
local config = {
host = "localhost",
port = 8080,
debug = true,
["content-type"] = "text/html", -- keys with special chars
}
-- Access
print(config.host) -- "localhost"
print(config["content-type"]) -- "text/html"
-- Iterate (unordered!)
for key, val in pairs(config) do
print(key .. " = " .. tostring(val))
end
-- Delete a key
config.debug = nil
-- Check existence
if config.port ~= nil then
print("port is set")
end
-- Tables can mix array and hash parts
local mixed = {
"first", -- [1] = "first"
"second", -- [2] = "second"
name = "mixed", -- hash part
}
-- Nested tables (tree structures, configs, etc.)
local player = {
name = "Luna",
pos = { x = 10, y = 20 },
inventory = {
{ id = "sword", damage = 15 },
{ id = "shield", armor = 8 },
},
}
print(player.pos.x) -- 10
print(player.inventory[1].id) -- "sword"
| Function | Description |
|---|---|
table.insert(t, [pos,] val) | Insert value at position (default: end) |
table.remove(t [, pos]) | Remove and return element (default: last) |
table.sort(t [, comp]) | Sort in-place with optional comparator |
table.concat(t [, sep [, i [, j]]]) | Join array elements into string |
table.unpack(t [, i [, j]]) | Return elements as multiple values |
table.pack(...) | Pack varargs into table with n field |
table.move(a1, f, e, t [, a2]) | Copy elements between tables/positions |
Metatables let you change how tables behave for operations like indexing, arithmetic, comparison, and function calls. They are the foundation of Lua's object system and operator overloading.
local t = {}
local mt = {}
setmetatable(t, mt) -- attach metatable, returns t
getmetatable(t) -- returns mt
-- Shorthand: set in constructor
local t = setmetatable({}, {
__index = function(self, key)
return "default"
end
})
| Metamethod | Trigger | Signature |
|---|---|---|
__index | Accessing missing key | function(table, key) or table |
__newindex | Setting missing key | function(table, key, value) |
__call | Calling as function t() | function(table, ...) |
__tostring | tostring(t) | function(table) |
__len | #t operator | function(table) |
__add | a + b | function(a, b) |
__sub | a - b | function(a, b) |
__mul | a * b | function(a, b) |
__div | a / b | function(a, b) |
__eq | a == b | function(a, b) |
__lt | a < b | function(a, b) |
__le | a <= b | function(a, b) |
__concat | a .. b | function(a, b) |
__gc | Garbage collection | function(table) |
__close | To-be-closed variable (5.4) | function(table, err) |
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({ x = x, y = y }, Vector)
end
function Vector:length()
return math.sqrt(self.x^2 + self.y^2)
end
function Vector.__add(a, b)
return Vector.new(a.x + b.x, a.y + b.y)
end
function Vector.__tostring(v)
return string.format("(%g, %g)", v.x, v.y)
end
local a = Vector.new(3, 4)
local b = Vector.new(1, 2)
print(a + b) -- (4, 6)
print(a:length()) -- 5
-- __index can be a table, enabling prototype chains
local defaults = { color = "blue", size = 10 }
local obj = setmetatable({}, { __index = defaults })
print(obj.color) -- "blue" (found via __index)
print(obj.size) -- 10
obj.color = "red"
print(obj.color) -- "red" (now on obj itself)
print(defaults.color) -- "blue" (unchanged)
Lua doesn't have classes built in, but metatables and __index chains give you everything you need for prototype-based or class-based OOP.
-- The colon is syntactic sugar for passing self
-- These are equivalent:
function obj.method(self, x) end
function obj:method(x) end
-- Calling with colon passes the object as self:
obj:method(42) -- equivalent to obj.method(obj, 42)
local Animal = {}
Animal.__index = Animal
function Animal.new(name, sound)
local self = setmetatable({}, Animal)
self.name = name
self.sound = sound
return self
end
function Animal:speak()
print(self.name .. " says " .. self.sound)
end
local cat = Animal.new("Cat", "meow")
cat:speak() -- "Cat says meow"
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog
function Dog.new(name, breed)
local self = Animal.new(name, "woof")
return setmetatable(self, Dog)
end
function Dog:fetch(item)
print(self.name .. " fetches " .. item)
end
local rex = Dog.new("Rex", "Labrador")
rex:speak() -- "Rex says woof" (inherited)
rex:fetch("ball") -- "Rex fetches ball" (own method)
-- Copy methods from one table to another
local function mixin(target, source)
for k, v in pairs(source) do
target[k] = v
end
return target
end
local Serializable = {}
function Serializable:serialize()
local parts = {}
for k, v in pairs(self) do
parts[#parts + 1] = k .. "=" .. tostring(v)
end
return table.concat(parts, ", ")
end
mixin(Animal, Serializable) -- all animals now serialize
rex:speak(), Lua first checks rex itself, then Dog (via rex's metatable __index), then Animal (via Dog's metatable __index). This is exactly like JavaScript's prototype chain.
Functions are first-class values in Lua. They capture their environment (closures), accept variable arguments, and can return multiple values.
local function divmod(a, b)
return a // b, a % b
end
local q, r = divmod(17, 5)
print(q, r) -- 3 2
-- Capture only the first return value
local q2 = divmod(17, 5)
-- Discard first, capture second with _
local _, remainder = divmod(17, 5)
local function printf(fmt, ...)
io.write(string.format(fmt, ...))
end
local function sum(...)
local total = 0
for _, v in ipairs({...}) do
total = total + v
end
return total
end
-- table.pack preserves nil arguments
local function safe_count(...)
local args = table.pack(...)
return args.n -- includes nils in count
end
local function counter(start)
local n = start or 0
return {
inc = function() n = n + 1; return n end,
dec = function() n = n - 1; return n end,
get = function() return n end,
}
end
local c = counter(10)
print(c.inc()) -- 11
print(c.inc()) -- 12
print(c.get()) -- 12
-- map, filter, reduce patterns
local function map(t, fn)
local result = {}
for i, v in ipairs(t) do
result[i] = fn(v, i)
end
return result
end
local function filter(t, pred)
local result = {}
for _, v in ipairs(t) do
if pred(v) then
result[#result + 1] = v
end
end
return result
end
local nums = {1, 2, 3, 4, 5}
local squares = map(nums, function(x) return x * x end)
local evens = filter(nums, function(x) return x % 2 == 0 end)
Coroutines are collaborative (non-preemptive) threads. They yield control explicitly, making them perfect for state machines, generators, async-style I/O, and cooperative multitasking.
local function producer()
local i = 0
while true do
i = i + 1
coroutine.yield(i) -- pause and return value
end
end
local co = coroutine.create(producer)
print(coroutine.status(co)) -- "suspended"
print(coroutine.resume(co)) -- true, 1
print(coroutine.resume(co)) -- true, 2
print(coroutine.status(co)) -- "suspended"
| State | Meaning |
|---|---|
suspended | Created or yielded, waiting to be resumed |
running | Currently executing |
normal | Resumed another coroutine (itself paused) |
dead | Function returned or errored |
coroutine.wrap-- wrap() returns a function that resumes automatically
local function range(start, stop, step)
step = step or 1
return coroutine.wrap(function()
for i = start, stop, step do
coroutine.yield(i)
end
end)
end
for n in range(1, 5) do
print(n) -- 1, 2, 3, 4, 5
end
-- Fibonacci generator
local function fibonacci(n)
return coroutine.wrap(function()
local a, b = 0, 1
for _ = 1, n do
coroutine.yield(a)
a, b = b, a + b
end
end)
end
for fib in fibonacci(10) do
io.write(fib .. " ") -- 0 1 1 2 3 5 8 13 21 34
end
local co = coroutine.create(function(initial)
local val = initial
while true do
print("Received:", val)
val = coroutine.yield(val * 2) -- send back, receive next
end
end)
local ok, result = coroutine.resume(co, 5)
print("Got:", result) -- Got: 10
ok, result = coroutine.resume(co, 7)
print("Got:", result) -- Got: 14
Lua has its own pattern matching system — lighter than full regex but powerful enough for most tasks. No backtracking, no alternation, but fast and simple.
| Pattern | Matches | Inverse |
|---|---|---|
%a | Letters (a-z, A-Z) | %A |
%d | Digits (0-9) | %D |
%l | Lowercase letters | %L |
%u | Uppercase letters | %U |
%w | Alphanumeric characters | %W |
%s | Whitespace | %S |
%p | Punctuation | %P |
%c | Control characters | %C |
. | Any character | — |
| Symbol | Meaning |
|---|---|
* | Zero or more (greedy) |
+ | One or more (greedy) |
- | Zero or more (lazy) |
? | Zero or one |
^ | Start of string (anchor) |
$ | End of string (anchor) |
%b() | Balanced match between delimiters |
local s = "Hello, World! 123"
-- Find (returns start, end positions)
local i, j = string.find(s, "World") -- 8, 12
local i, j = s:find("%d+") -- 15, 17
-- Match (returns captures)
local word = s:match("(%a+)") -- "Hello"
local nums = s:match("(%d+)") -- "123"
-- Global match (iterate all matches)
for word in s:gmatch("%a+") do
print(word) -- Hello, World
end
-- Global substitution
local result = s:gsub("%a+", string.upper)
-- "HELLO, WORLD! 123"
-- gsub with function
local html = ("<b>bold</b>"):gsub("<(.-)>", function(tag)
return "[" .. tag .. "]"
end)
-- "[b]bold[/b]"
-- Parse key=value pairs
local config = "host=localhost port=8080 debug=true"
for key, val in config:gmatch("(%w+)=(%w+)") do
print(key, val)
end
-- Extract email parts
local email = "user@example.com"
local user, domain = email:match("(.+)@(.+)")
-- Trim whitespace
local function trim(s)
return s:match("^%s*(.-)%s*$")
end
-- Split string
local function split(s, sep)
local result = {}
for part in s:gmatch("([^" .. sep .. "]+)") do
result[#result + 1] = part
end
return result
end
|), backreferences, and lookahead. For complex text processing, use the lpeg library (Parsing Expression Grammars) or the lrexlib binding to PCRE.
Lua modules are just tables returned from files. The require function loads and caches them.
-- mylib.lua
local M = {}
function M.greet(name)
return "Hello, " .. name
end
function M.farewell(name)
return "Goodbye, " .. name
end
-- Private function (not in M)
local function helper()
-- only accessible within this file
end
return M
-- main.lua
local mylib = require("mylib")
print(mylib.greet("Lua")) -- "Hello, Lua"
-- Destructure specific functions
local greet = require("mylib").greet
-- require() caches in package.loaded
local a = require("mylib")
local b = require("mylib")
print(a == b) -- true (same table)
-- Force reload (during development)
package.loaded["mylib"] = nil
local fresh = require("mylib")
-- Lua searches these patterns (? replaced with module name)
print(package.path)
-- ./?.lua;./?/init.lua;/usr/local/share/lua/5.4/?.lua;...
print(package.cpath)
-- ./?.so;/usr/local/lib/lua/5.4/?.so;...
-- Add custom search paths
package.path = package.path .. ";./libs/?.lua"
-- Subdirectory modules use dot notation
local json = require("libs.json") -- loads libs/json.lua
-- Install LuaRocks packages
$ luarocks install luasocket
$ luarocks install lpeg
$ luarocks install cjson
-- List installed packages
$ luarocks list
-- Create a rockspec for your project
$ luarocks write_rockspec
-- Use in code
local socket = require("socket")
local json = require("cjson")
Lua uses pcall and xpcall for protected calls — similar to try/catch but more functional. Errors can be any value, not just strings.
-- error() stops execution with a message
error("something went wrong")
error("bad input", 2) -- level 2 = blame the caller
-- error() can throw any value
error({ code = 404, msg = "not found" })
-- assert() is shorthand for error-on-nil
local f = assert(io.open("data.txt", "r"))
-- If io.open returns nil, err then assert raises err
-- pcall: protected call (like try/catch)
local ok, result = pcall(function()
return risky_operation()
end)
if ok then
print("Success:", result)
else
print("Error:", result) -- result is the error
end
-- pcall with function arguments
local ok, val = pcall(tonumber, "not a number")
-- xpcall: like pcall but with error handler
local ok, result = xpcall(
function()
error("boom")
end,
function(err)
-- error handler gets the error + can add stack trace
return err .. "\n" .. debug.traceback()
end
)
-- Pattern 1: Return nil, error (Lua convention)
local function parse_int(s)
local n = tonumber(s)
if not n then
return nil, "invalid number: " .. s
end
return math.floor(n)
end
local n, err = parse_int("abc")
if not n then print("Error:", err) end
-- Pattern 2: assert wraps nil, error returns
local n = assert(parse_int("42")) -- ok
local n = assert(parse_int("abc")) -- raises error
-- Pattern 3: To-be-closed variables (5.4)
local f <close> = assert(io.open("data.txt"))
-- f:close() called automatically when f goes out of scope
-- even if an error occurs
Lua was designed from the ground up to be embedded. The C API uses a virtual stack for all communication between C and Lua. This is what makes Lua the scripting language of choice for games, editors, and systems.
// Create a Lua state and run a script
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L); // open standard libraries
// Run a Lua script
luaL_dofile(L, "script.lua");
// Or run a string
luaL_dostring(L, "print('Hello from C!')");
lua_close(L);
return 0;
}
// Push values onto the Lua stack
lua_pushnumber(L, 42);
lua_pushstring(L, "hello");
lua_pushboolean(L, 1);
lua_pushnil(L);
// Read values from the stack
double n = lua_tonumber(L, 1);
const char *s = lua_tostring(L, 2);
int b = lua_toboolean(L, 3);
// Call a Lua function from C
lua_getglobal(L, "myfunction"); // push function
lua_pushnumber(L, 10); // push arg 1
lua_pushnumber(L, 20); // push arg 2
lua_call(L, 2, 1); // 2 args, 1 return
double result = lua_tonumber(L, -1);
lua_pop(L, 1);
// A C function callable from Lua
static int l_add(lua_State *L) {
double a = luaL_checknumber(L, 1);
double b = luaL_checknumber(L, 2);
lua_pushnumber(L, a + b);
return 1; // number of return values
}
// Register as a global function
lua_pushcfunction(L, l_add);
lua_setglobal(L, "add");
// Now in Lua: print(add(3, 4)) --> 7
// Register a whole module
static const luaL_Reg mylib[] = {
{"add", l_add},
{"multiply", l_multiply},
{NULL, NULL}
};
int luaopen_mylib(lua_State *L) {
luaL_newlib(L, mylib);
return 1;
}
-- LuaJIT FFI example
local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
unsigned long sleep(unsigned long seconds);
]]
ffi.C.printf("Hello from %s!\n", "FFI")
-- Define C structs
ffi.cdef[[
typedef struct { double x, y; } Point;
]]
local p = ffi.new("Point", { x = 3, y = 4 })
print(p.x, p.y) -- 3 4
Lua's small footprint and embeddability have made it the scripting language of choice across gaming, networking, editors, and embedded systems.
Love2D is a framework for 2D games written entirely in Lua. It handles graphics, audio, physics, and input.
-- main.lua (Love2D entry point)
local player = { x = 400, y = 300, speed = 200 }
function love.update(dt)
if love.keyboard.isDown("right") then
player.x = player.x + player.speed * dt
end
if love.keyboard.isDown("left") then
player.x = player.x - player.speed * dt
end
end
function love.draw()
love.graphics.setColor(0.2, 0.6, 1)
love.graphics.circle("fill", player.x, player.y, 20)
end
Neovim uses Lua as its primary configuration and plugin language, replacing the older VimScript.
-- init.lua (Neovim config)
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
-- Key mappings
vim.keymap.set("n", "<leader>w", "<cmd>w<cr>", { desc = "Save" })
vim.keymap.set("n", "<leader>q", "<cmd>q<cr>", { desc = "Quit" })
-- Autocommands
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*.lua",
callback = function()
vim.lsp.buf.format()
end,
})
-- Plugin manager (lazy.nvim)
require("lazy").setup({
{ "nvim-treesitter/nvim-treesitter", build = ":TSUpdate" },
{ "neovim/nvim-lspconfig" },
{ "hrsh7th/nvim-cmp" },
})
Redis embeds Lua for atomic server-side scripting. Scripts run without interruption (atomic).
-- Rate limiter in Redis Lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = tonumber(redis.call("GET", key) or "0")
if current >= limit then
return 0 -- rate limited
end
redis.call("INCR", key)
if current == 0 then
redis.call("EXPIRE", key, window)
end
return limit - current - 1
-- Call from redis-cli:
-- EVAL "..." 1 "user:123:rate" 10 60
OpenResty embeds LuaJIT into nginx, enabling high-performance web apps at the edge.
-- nginx.conf with lua-nginx-module
location /api {
content_by_lua_block {
local cjson = require("cjson")
-- Read request body
ngx.req.read_body()
local body = ngx.req.get_body_data()
local data = cjson.decode(body)
-- Access Redis
local redis = require("resty.redis")
local red = redis:new()
red:connect("127.0.0.1", 6379)
-- Respond with JSON
ngx.say(cjson.encode({
status = "ok",
message = "processed"
}))
}
}
WoW has used Lua for its addon API since 2004, making it one of the largest Lua ecosystems.
-- MyAddon.lua (WoW addon)
local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("CHAT_MSG_SAY")
frame:SetScript("OnEvent", function(self, event, ...)
if event == "PLAYER_LOGIN" then
print("|cff00ff00MyAddon loaded!|r")
elseif event == "CHAT_MSG_SAY" then
local message, sender = ...
print(sender .. " said: " .. message)
end
end)
-- Slash command
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function(msg)
print("You typed: " .. msg)
end
Defold, Roblox (Luau), Solar2D, Garry's Mod, PICO-8, Tabletop Simulator. Many AAA engines (CryEngine, Cocos2d-x) also use Lua for gameplay scripting.
Wireshark (packet dissectors), nmap (NSE scripts), HAProxy, Envoy (via proxy-wasm). Network tools love Lua's sandboxability.
Neovim, Hammerspoon (macOS automation), mpv (media player scripting), Pandoc (document filters), TeX (LuaTeX).
NodeMCU (ESP8266/ESP32), Tarantool (database), LuaRT (Windows desktop apps). Lua's tiny footprint (<200KB) makes it ideal for constrained environments.
Lua's generic for loop works with any iterator function. You can create stateful and stateless iterators.
-- Built-in iterators
for i, v in ipairs({"a", "b", "c"}) do end -- array order
for k, v in pairs(t) do end -- all keys (unordered)
-- Stateless iterator
local function squares(max, current)
current = current + 1
if current > max then return nil end
return current, current * current
end
for i, sq in squares, 5, 0 do
print(i, sq) -- 1 1, 2 4, 3 9, 4 16, 5 25
end
-- Stateful iterator (closure-based)
local function values(t)
local i = 0
return function()
i = i + 1
return t[i]
end
end
for val in values({"x", "y", "z"}) do
print(val) -- x, y, z
end
In Lua 5.2+ every function has an upvalue named _ENV that determines its global environment. This enables powerful sandboxing.
-- Create a sandbox environment
local function sandbox(code)
local env = {
print = print,
pairs = pairs,
ipairs = ipairs,
math = math,
string = string,
table = table,
-- no io, os, debug, load, dofile, etc.
}
local fn, err = load(code, "sandbox", "t", env)
if not fn then return nil, err end
return pcall(fn)
end
sandbox('print(math.sqrt(144))') -- works: 12
sandbox('os.execute("rm -rf /")') -- blocked!
| Feature | Lua 5.4 | LuaJIT 2.1 | Luau (Roblox) |
|---|---|---|---|
| Performance | Interpreted (fast) | JIT compiled (very fast) | Custom VM (fast) |
| Base language | 5.4 spec | 5.1 + extensions | 5.1 + extensions |
| Types | Dynamic only | Dynamic only | Optional type annotations |
| FFI | Via C API | Built-in FFI library | No (sandboxed) |
| Integers | Native 64-bit | No (all numbers are doubles) | No (doubles) |
| Goto statement | Yes | Yes | No |
| Generalized for | Yes (5.2+) | No | Yes (custom) |
| Primary use | General embedding | Perf-critical embedding | Roblox game scripts |