Lua Guide¶
Applies to: Lua 5.4+, Love2D, Neovim, Roblox, Game Development, Embedded Scripting
Core Principles¶
- Simplicity: Minimal, elegant language design
- Tables Are Everything: Tables for arrays, objects, modules
- First-Class Functions: Closures, higher-order functions
- Coroutines: Cooperative multitasking
- Embeddability: Designed for integration with C/C++
Language-Specific Guardrails¶
Lua Version¶
- ✓ Use Lua 5.4+ (or LuaJIT for performance)
- ✓ Specify version in documentation
- ✓ Be aware of version differences (5.1 vs 5.4)
Code Style¶
- ✓ Use
snake_casefor variables and functions - ✓ Use
PascalCasefor classes/modules - ✓ Use
SCREAMING_SNAKE_CASEfor constants - ✓ 2-space indentation
- ✓ Max line length: 80-100 characters
- ✓ Use
localby default (avoid globals)
Tables¶
- ✓ Prefer array-style
{}for sequences - ✓ Use explicit keys for dictionaries
- ✓ Don't mix array and hash parts
- ✓ Use metatables for OOP patterns
Error Handling¶
- ✓ Use
pcall/xpcallfor error handling - ✓ Use
assertfor preconditions - ✓ Return
nil, error_messagefor expected failures - ✓ Provide meaningful error messages
Performance¶
- ✓ Use local variables (faster lookup)
- ✓ Pre-allocate tables when size is known
- ✓ Avoid creating tables in hot loops
- ✓ Use LuaJIT for performance-critical code
Basic Syntax¶
Variables and Types¶
-- Local variables (preferred)
local name = "John"
local age = 25
local is_active = true
local nothing = nil
-- Multiple assignment
local x, y, z = 1, 2, 3
local a, b = b, a -- Swap
-- Type checking
print(type(name)) -- "string"
print(type(age)) -- "number"
print(type(is_active)) -- "boolean"
-- String operations
local greeting = "Hello, " .. name -- Concatenation
local length = #greeting -- Length
local upper = string.upper(name) -- "JOHN"
-- Number operations
local pi = 3.14159
local rounded = math.floor(pi) -- 3
local result = pi ^ 2 -- Power
-- Lua 5.3+ integers
local int = 42 -- Integer
local float = 42.0 -- Float
print(42 // 5) -- Integer division: 8
print(42 % 5) -- Modulo: 2
Tables¶
-- Array (1-indexed!)
local fruits = {"apple", "banana", "cherry"}
print(fruits[1]) -- "apple" (not 0!)
print(#fruits) -- 3 (length)
-- Dictionary
local user = {
name = "John",
age = 25,
email = "john@example.com",
}
-- Access
print(user.name) -- "John"
print(user["name"]) -- "John"
-- Modify
user.age = 26
user["status"] = "active"
-- Iterate array
for i, fruit in ipairs(fruits) do
print(i, fruit)
end
-- Iterate dictionary
for key, value in pairs(user) do
print(key, value)
end
-- Nested tables
local config = {
database = {
host = "localhost",
port = 5432,
},
features = {"auth", "logging"},
}
Control Flow¶
-- If statement
if age >= 18 then
print("Adult")
elseif age >= 13 then
print("Teenager")
else
print("Child")
end
-- Ternary-like (and/or idiom)
local status = is_active and "active" or "inactive"
-- While loop
local i = 1
while i <= 5 do
print(i)
i = i + 1
end
-- For loop (numeric)
for i = 1, 10 do
print(i)
end
for i = 10, 1, -1 do -- Reverse
print(i)
end
-- For loop (generic)
for i, v in ipairs(array) do
print(i, v)
end
-- Repeat until
repeat
local input = io.read()
until input == "quit"
-- Break and return
for i = 1, 100 do
if i > 10 then
break
end
end
Functions¶
Basic Functions¶
-- Simple function
local function greet(name)
return "Hello, " .. name
end
-- Alternative syntax
local greet = function(name)
return "Hello, " .. name
end
-- Multiple return values
local function divide(a, b)
if b == 0 then
return nil, "division by zero"
end
return a / b, a % b
end
local quotient, remainder = divide(10, 3)
-- Variadic functions
local function sum(...)
local total = 0
for _, v in ipairs({...}) do
total = total + v
end
return total
end
print(sum(1, 2, 3, 4, 5)) -- 15
-- Default arguments (pattern)
local function create_user(name, role)
role = role or "user" -- Default value
return {name = name, role = role}
end
-- Named arguments (table pattern)
local function connect(options)
local host = options.host or "localhost"
local port = options.port or 8080
local timeout = options.timeout or 30
-- ...
end
connect({host = "example.com", port = 3000})
Closures¶
-- Counter closure
local function create_counter(start)
local count = start or 0
return function()
count = count + 1
return count
end
end
local counter = create_counter(10)
print(counter()) -- 11
print(counter()) -- 12
-- Memoization
local function memoize(fn)
local cache = {}
return function(arg)
if cache[arg] == nil then
cache[arg] = fn(arg)
end
return cache[arg]
end
end
local expensive = memoize(function(n)
-- Expensive computation
return n * n
end)
Higher-Order Functions¶
-- Map
local function map(tbl, fn)
local result = {}
for i, v in ipairs(tbl) do
result[i] = fn(v)
end
return result
end
local doubled = map({1, 2, 3}, function(x) return x * 2 end)
-- Filter
local function filter(tbl, predicate)
local result = {}
for _, v in ipairs(tbl) do
if predicate(v) then
result[#result + 1] = v
end
end
return result
end
local evens = filter({1, 2, 3, 4, 5}, function(x) return x % 2 == 0 end)
-- Reduce
local function reduce(tbl, fn, initial)
local acc = initial
for _, v in ipairs(tbl) do
acc = fn(acc, v)
end
return acc
end
local sum = reduce({1, 2, 3, 4, 5}, function(a, b) return a + b end, 0)
Object-Oriented Programming¶
Basic Class Pattern¶
-- Class definition
local User = {}
User.__index = User
function User.new(name, email)
local self = setmetatable({}, User)
self.name = name
self.email = email
return self
end
function User:greet()
return "Hello, " .. self.name
end
function User:get_email()
return self.email
end
-- Usage
local user = User.new("John", "john@example.com")
print(user:greet()) -- "Hello, John"
Inheritance¶
-- Base class
local Entity = {}
Entity.__index = Entity
function Entity.new(x, y)
local self = setmetatable({}, Entity)
self.x = x or 0
self.y = y or 0
return self
end
function Entity:move(dx, dy)
self.x = self.x + dx
self.y = self.y + dy
end
-- Derived class
local Player = setmetatable({}, {__index = Entity})
Player.__index = Player
function Player.new(x, y, name)
local self = setmetatable(Entity.new(x, y), Player)
self.name = name
self.health = 100
return self
end
function Player:take_damage(amount)
self.health = math.max(0, self.health - amount)
end
-- Usage
local player = Player.new(100, 200, "Hero")
player:move(10, 20) -- Inherited method
player:take_damage(25) -- Own method
Modules¶
-- mymodule.lua
local M = {}
-- Private (local)
local function helper()
return "private"
end
-- Public
function M.public_function()
return helper() .. " exposed"
end
M.VERSION = "1.0.0"
return M
-- Usage
local mymodule = require("mymodule")
print(mymodule.public_function())
print(mymodule.VERSION)
Error Handling¶
pcall Pattern¶
-- Protected call
local success, result = pcall(function()
-- Code that might error
return risky_operation()
end)
if success then
print("Result:", result)
else
print("Error:", result)
end
-- With xpcall (custom error handler)
local function error_handler(err)
return debug.traceback(err, 2)
end
local success, result = xpcall(function()
error("Something went wrong")
end, error_handler)
if not success then
print(result) -- Includes stack trace
end
Assert Pattern¶
-- Preconditions
local function divide(a, b)
assert(type(a) == "number", "a must be a number")
assert(type(b) == "number", "b must be a number")
assert(b ~= 0, "b cannot be zero")
return a / b
end
-- Custom assert
local function validate(condition, message, ...)
if not condition then
error(string.format(message, ...), 2)
end
return condition
end
validate(age >= 0, "Age must be non-negative, got %d", age)
Return nil, error Pattern¶
local function parse_json(str)
if type(str) ~= "string" then
return nil, "expected string"
end
local success, result = pcall(json.decode, str)
if not success then
return nil, "invalid JSON: " .. result
end
return result
end
-- Usage
local data, err = parse_json(input)
if not data then
print("Parse error:", err)
return
end
Coroutines¶
Basic Coroutines¶
-- Create coroutine
local co = coroutine.create(function()
for i = 1, 3 do
print("Coroutine:", i)
coroutine.yield(i)
end
return "done"
end)
-- Resume coroutine
print(coroutine.resume(co)) -- true, 1
print(coroutine.resume(co)) -- true, 2
print(coroutine.resume(co)) -- true, 3
print(coroutine.resume(co)) -- true, "done"
print(coroutine.resume(co)) -- false, "cannot resume dead coroutine"
-- Status
print(coroutine.status(co)) -- "dead"
Producer-Consumer Pattern¶
local function producer()
return coroutine.create(function()
for i = 1, 10 do
coroutine.yield(i * i)
end
end)
end
local function consumer(prod)
while true do
local ok, value = coroutine.resume(prod)
if not ok or value == nil then
break
end
print("Consumed:", value)
end
end
consumer(producer())
Iterator with Coroutines¶
local function range(from, to, step)
step = step or 1
return coroutine.wrap(function()
for i = from, to, step do
coroutine.yield(i)
end
end)
end
for i in range(1, 10, 2) do
print(i) -- 1, 3, 5, 7, 9
end
Metatables¶
Common Metamethods¶
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({x = x, y = y}, Vector)
end
-- Arithmetic
function Vector.__add(a, b)
return Vector.new(a.x + b.x, a.y + b.y)
end
function Vector.__sub(a, b)
return Vector.new(a.x - b.x, a.y - b.y)
end
function Vector.__mul(a, scalar)
return Vector.new(a.x * scalar, a.y * scalar)
end
function Vector.__eq(a, b)
return a.x == b.x and a.y == b.y
end
-- String representation
function Vector.__tostring(v)
return string.format("Vector(%g, %g)", v.x, v.y)
end
-- Length
function Vector.__len(v)
return math.sqrt(v.x^2 + v.y^2)
end
-- Usage
local v1 = Vector.new(3, 4)
local v2 = Vector.new(1, 2)
print(v1 + v2) -- Vector(4, 6)
print(#v1) -- 5 (magnitude)
Read-Only Table¶
local function readonly(tbl)
return setmetatable({}, {
__index = tbl,
__newindex = function()
error("Attempt to modify read-only table", 2)
end,
__pairs = function() return pairs(tbl) end,
})
end
local config = readonly({
host = "localhost",
port = 8080,
})
print(config.host) -- OK
config.port = 3000 -- Error!
Love2D Game Development¶
Basic Game Structure¶
-- main.lua
local player = {
x = 400,
y = 300,
speed = 200,
radius = 20,
}
function love.load()
love.window.setTitle("My Game")
love.window.setMode(800, 600)
end
function love.update(dt)
-- Movement
if love.keyboard.isDown("left") or love.keyboard.isDown("a") then
player.x = player.x - player.speed * dt
end
if love.keyboard.isDown("right") or love.keyboard.isDown("d") then
player.x = player.x + player.speed * dt
end
if love.keyboard.isDown("up") or love.keyboard.isDown("w") then
player.y = player.y - player.speed * dt
end
if love.keyboard.isDown("down") or love.keyboard.isDown("s") then
player.y = player.y + player.speed * dt
end
-- Keep in bounds
player.x = math.max(player.radius, math.min(800 - player.radius, player.x))
player.y = math.max(player.radius, math.min(600 - player.radius, player.y))
end
function love.draw()
love.graphics.setColor(1, 0.5, 0)
love.graphics.circle("fill", player.x, player.y, player.radius)
end
function love.keypressed(key)
if key == "escape" then
love.event.quit()
end
end
Neovim Configuration¶
Basic Config¶
-- init.lua
vim.g.mapleader = " "
-- Options
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
vim.opt.smartindent = true
vim.opt.wrap = false
vim.opt.termguicolors = true
-- Keymaps
vim.keymap.set("n", "<leader>w", "<cmd>write<cr>", {desc = "Save"})
vim.keymap.set("n", "<leader>q", "<cmd>quit<cr>", {desc = "Quit"})
-- Plugin setup (lazy.nvim example)
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git", "clone", "--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup({
{"nvim-treesitter/nvim-treesitter", build = ":TSUpdate"},
{"neovim/nvim-lspconfig"},
})
Testing¶
Basic Testing¶
-- test.lua
local function test(name, fn)
local ok, err = pcall(fn)
if ok then
print("✓ " .. name)
else
print("✗ " .. name .. ": " .. err)
end
end
local function assert_equal(actual, expected)
if actual ~= expected then
error(string.format("Expected %s, got %s", tostring(expected), tostring(actual)), 2)
end
end
-- Tests
test("addition works", function()
assert_equal(1 + 1, 2)
end)
test("string concatenation", function()
assert_equal("hello" .. " " .. "world", "hello world")
end)
With Busted Framework¶
-- spec/user_spec.lua
describe("User", function()
local User = require("user")
describe("new", function()
it("creates a user with name", function()
local user = User.new("John")
assert.are.equal("John", user.name)
end)
end)
describe("greet", function()
it("returns greeting message", function()
local user = User.new("John")
assert.are.equal("Hello, John", user:greet())
end)
end)
end)
Common Pitfalls¶
Avoid These¶
-- Forgetting local (creates global)
function bad()
x = 10 -- Global!
end
-- 1-based indexing confusion
local arr = {"a", "b", "c"}
print(arr[0]) -- nil, not "a"!
-- Comparing with nil
if x == nil then -- Works but...
end
if not x then -- Also catches false!
end
-- Table reference vs copy
local a = {1, 2, 3}
local b = a
b[1] = 100
print(a[1]) -- 100! (same table)
Do This Instead¶
-- Always use local
local function good()
local x = 10
end
-- Remember 1-based indexing
local arr = {"a", "b", "c"}
print(arr[1]) -- "a"
-- Explicit nil check
if x == nil then
-- Only nil
end
-- Deep copy
local function deep_copy(tbl)
local copy = {}
for k, v in pairs(tbl) do
if type(v) == "table" then
copy[k] = deep_copy(v)
else
copy[k] = v
end
end
return copy
end