Tech Guides
🌙

Lua

The Moon Language — Fast, Embeddable, Elegant

Lua 5.4 LuaJIT 2.1 First released 1993
01

Quick Reference

The most common Lua syntax at a glance. Lua is minimal by design — the entire reference manual fits in ~100 pages.

Variables

Global by default; use local for scoping

local name = "Lua"
local version = 5.4
local ready = true
local nothing = nil

Control Flow

if/elseif/else, while, for, repeat

if x > 0 then
  print("positive")
elseif x == 0 then
  print("zero")
else
  print("negative")
end

Functions

First-class; closures; multiple returns

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

-- anonymous / lambda
local sq = function(x)
  return x * x
end

Loops

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

Tables

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"

Strings

Immutable; concatenate with ..

local s = "hello" .. " world"
print(#s)           -- 11 (length)
print(s:upper())    -- HELLO WORLD
print(s:sub(1,5))  -- hello
Design Philosophy Lua has only 8 types, 22 keywords, and one data structure (the table). This extreme simplicity makes it fast to learn, fast to embed, and fast to run — LuaJIT is one of the fastest dynamic language runtimes ever built.
02

Variables & Types

Lua is dynamically typed. Variables don't have types — values do. There are exactly eight basic types.

The Eight Types

Type Description Example
nilAbsence of value; the only type with a single valuenil
booleanTwo values onlytrue, false
numberIntegers & floats (both subtypes in 5.4)42, 3.14, 0xFF
stringImmutable byte sequences"hello", 'world'
tableAssociative array / object / namespace{1, 2, x=3}
functionFirst-class closuresfunction() end
userdataC-allocated memory block(from C API)
threadCoroutine handlecoroutine.create(f)

Variable Scoping

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
Best Practice Always use 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.

Numbers in Lua 5.4

-- 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)

Truthiness

-- 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
03

Tables

Tables are Lua's only compound data structure. They serve as arrays, dictionaries, records, objects, namespaces, and modules — all at once.

Arrays (Sequences)

-- 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)

Dictionaries (Hash Maps)

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

Mixed Tables & Nesting

-- 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"

Table Library

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
04

Metatables & Metamethods

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.

Setting & Getting Metatables

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
})

Common Metamethods

Metamethod Trigger Signature
__indexAccessing missing keyfunction(table, key) or table
__newindexSetting missing keyfunction(table, key, value)
__callCalling as function t()function(table, ...)
__tostringtostring(t)function(table)
__len#t operatorfunction(table)
__adda + bfunction(a, b)
__suba - bfunction(a, b)
__mula * bfunction(a, b)
__diva / bfunction(a, b)
__eqa == bfunction(a, b)
__lta < bfunction(a, b)
__lea <= bfunction(a, b)
__concata .. bfunction(a, b)
__gcGarbage collectionfunction(table)
__closeTo-be-closed variable (5.4)function(table, err)

Practical Example: A Vector Type

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 Chaining (Prototype Lookup)

-- __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)
05

OOP Patterns

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 Syntax

-- 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)

Basic Class Pattern

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"

Inheritance

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)

Mixins

-- 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
Lookup Chain When you call 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.
06

Functions & Closures

Functions are first-class values in Lua. They capture their environment (closures), accept variable arguments, and can return multiple values.

Multiple Return 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)

Varargs

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

Closures

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

Higher-Order Functions

-- 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)
07

Coroutines

Coroutines are collaborative (non-preemptive) threads. They yield control explicitly, making them perfect for state machines, generators, async-style I/O, and cooperative multitasking.

Coroutine Lifecycle

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"

Coroutine States

State Meaning
suspendedCreated or yielded, waiting to be resumed
runningCurrently executing
normalResumed another coroutine (itself paused)
deadFunction returned or errored

Generator Pattern with 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

Bidirectional Communication

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
08

String Patterns

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.

Character Classes

Pattern Matches Inverse
%aLetters (a-z, A-Z)%A
%dDigits (0-9)%D
%lLowercase letters%L
%uUppercase letters%U
%wAlphanumeric characters%W
%sWhitespace%S
%pPunctuation%P
%cControl characters%C
.Any character

Quantifiers & Anchors

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

String Functions

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]"

Practical Pattern Examples

-- 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
Not Regex Lua patterns lack alternation (|), backreferences, and lookahead. For complex text processing, use the lpeg library (Parsing Expression Grammars) or the lrexlib binding to PCRE.
09

Modules & Require

Lua modules are just tables returned from files. The require function loads and caches them.

Creating a Module

-- 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

Using Modules

-- 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")

Package Search Path

-- 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

LuaRocks (Package Manager)

-- 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")
10

Error Handling

Lua uses pcall and xpcall for protected calls — similar to try/catch but more functional. Errors can be any value, not just strings.

Raising Errors

-- 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 & xpcall

-- 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
)

Error Handling Patterns

-- 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
11

C API & Embedding

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.

Embedding Lua in C

// 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;
}

Stack-Based Communication

// 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);

Registering C Functions in Lua

// 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 LuaJIT provides an FFI (Foreign Function Interface) that lets you call C functions and use C data structures directly from Lua, without writing any C glue code. It's both simpler and faster than the traditional C API.
-- 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
12

Ecosystems & Use Cases

Lua's small footprint and embeddability have made it the scripting language of choice across gaming, networking, editors, and embedded systems.

Game Development: Love2D

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 Configuration

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 Scripting

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

nginx / OpenResty

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"
        }))
    }
}

World of Warcraft Addons

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

Other Notable Embeddings

Game Engines

Defold, Roblox (Luau), Solar2D, Garry's Mod, PICO-8, Tabletop Simulator. Many AAA engines (CryEngine, Cocos2d-x) also use Lua for gameplay scripting.

Networking

Wireshark (packet dissectors), nmap (NSE scripts), HAProxy, Envoy (via proxy-wasm). Network tools love Lua's sandboxability.

Editors & Tools

Neovim, Hammerspoon (macOS automation), mpv (media player scripting), Pandoc (document filters), TeX (LuaTeX).

Embedded / IoT

NodeMCU (ESP8266/ESP32), Tarantool (database), LuaRT (Windows desktop apps). Lua's tiny footprint (<200KB) makes it ideal for constrained environments.

Iterators & Custom Iteration

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

Environments & Sandboxing

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!

Lua vs LuaJIT vs Luau

Feature Lua 5.4 LuaJIT 2.1 Luau (Roblox)
PerformanceInterpreted (fast)JIT compiled (very fast)Custom VM (fast)
Base language5.4 spec5.1 + extensions5.1 + extensions
TypesDynamic onlyDynamic onlyOptional type annotations
FFIVia C APIBuilt-in FFI libraryNo (sandboxed)
IntegersNative 64-bitNo (all numbers are doubles)No (doubles)
Goto statementYesYesNo
Generalized forYes (5.2+)NoYes (custom)
Primary useGeneral embeddingPerf-critical embeddingRoblox game scripts