Lua language guardrails, patterns, and best practices for AI-assisted development.
Use when working with Lua files (.lua), or when the user mentions Lua/LuaJIT/Neovim/Love2D.
Provides table patterns, metatable guidelines, coroutine usage,
and embedding conventions specific to this project's coding standards.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Lua language guardrails, patterns, and best practices for AI-assisted development.
Use when working with Lua files (.lua), or when the user mentions Lua/LuaJIT/Neovim/Love2D.
Provides table patterns, metatable guidelines, coroutine usage,
and embedding conventions specific to this project's coding standards.
Tables Are Everything: Arrays, maps, objects, modules, and namespaces -- master them
Local by Default: Always declare variables local; globals are a performance and correctness hazard
Explicit Error Handling: Use pcall/xpcall for recoverable errors; error() for programmer mistakes
Minimal Metatables: Use metatables for genuine OOP needs, not as decoration on simple data
Embed-Friendly Design: Lua exists to be embedded; keep the host/script boundary clean and narrow
Guardrails
Code Style
Use local for every variable and function unless it must be global
Naming: snake_case for variables/functions, PascalCase for class-like tables, UPPER_SNAKE_CASE for constants
Indent with 2 spaces; one statement per line; avoid semicolons
Use [[ ... ]] long strings for multi-line text and SQL/HTML templates
Prefer #tbl over table.getn() for sequence length
Tables
Arrays are 1-based; for i = 1, #arr not for i = 0, #arr - 1
Use ipairs for sequential iteration, pairs for hash-map iteration
Do not mix array indices and string keys in the same table (undefined # behavior)
Use table.insert / table.remove for array ops; avoid manual index gaps
Freeze config tables by setting a __newindex metamethod that errors
Error Handling
Use pcall(fn, ...) to catch errors; xpcall(fn, handler, ...) for tracebacks
Return nil, err_msg from functions that can fail (idiomatic two-value return)
Reserve error("msg", level) for violated preconditions (programmer errors)
Never silently swallow errors; always log or propagate
localfunctionread_config(path)local f, err = io.open(path, "r")
ifnot f thenreturnnil, "cannot open config: " .. err endlocal content = f:read("*a")
f:close()
return content
endlocal ok, result = xpcall(dangerous_operation, debug.traceback)
ifnot ok thenlog.error("failed: %s", result) end
Performance
Localize hot functions: local insert = table.insert
Avoid closures inside hot loops (allocates every iteration)
Use table.concat instead of .. concatenation in loops
LuaJIT: avoid pairs() in hot paths (not JIT-compiled); prefer arrays with ipairs
LuaJIT: use FFI (ffi.new, ffi.cast) for C struct access instead of Lua tables
Embedding
Keep the Lua-to-host API surface small (<20 registered functions)
Validate all arguments from Lua in C/host bindings
Set memory limits via lua_setallocf or lua_gc configuration
Use debug.sethook instruction-count hooks for untrusted scripts
Key Patterns
Module Pattern
local M = {}
local TIMEOUT_MS = 5000localfunctionvalidate(data)assert(type(data) == "table", "expected table, got " .. type(data))
assert(data.name, "missing required field: name")
endfunctionM.process(data)
validate(data)
return { status = "ok", name = data.name }
endreturn M
OOP via Metatables
local Animal = {}
Animal.__index = Animal
functionAnimal.new(name, sound)returnsetmetatable({ name = name, sound = sound }, Animal)
endfunctionAnimal:speak()returnstring.format("%s says %s", self.name, self.sound)
end-- Inheritancelocal Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog
functionDog.new(name)returnsetmetatable(Animal.new(name, "woof"), Dog)
endfunctionDog:fetch(item)returnstring.format("%s fetches the %s", self.name, item)
end
Coroutines
localfunctionproducer(items)returncoroutine.wrap(function()for _, item inipairs(items) docoroutine.yield(item)
endend)
endlocalfunctionfilter(predicate, iter)returncoroutine.wrap(function()for item in iter doif predicate(item) thencoroutine.yield(item) endendend)
endlocal nums = producer({ 1, 2, 3, 4, 5, 6 })
local evens = filter(function(n)return n % 2 == 0end, nums)
for v in evens doprint(v) end--> 2, 4, 6
Custom Iterator
localfunctionrange(start, stop, step)
step = step or1local i = start - step
returnfunction()
i = i + step
if i <= stop thenreturn i endendendfor n in range(1, 10, 2) doprint(n) end--> 1, 3, 5, 7, 9
Neovim Lua API
local api, keymap = vim.api, vim.keymap
local M = {}
functionM.setup(opts)
opts = vim.tbl_deep_extend("force", { enabled = true, width = 80 }, opts or {})
ifnot opts.enabled thenreturnendlocal group = api.nvim_create_augroup("MyPlugin", { clear = true })
api.nvim_create_autocmd("BufWritePre", {
group = group, pattern = "*.lua",
callback = function(ev)locallines = api.nvim_buf_get_lines(ev.buf, 0, -1, false)
for i, line inipairs(lines) dolines[i] = line:gsub("%s+$", "") end
api.nvim_buf_set_lines(ev.buf, 0, -1, false, lines)
end,
})
keymap.set("n", "<leader>mp", function()
vim.notify("MyPlugin activated", vim.log.levels.INFO)
end, { desc = "Activate MyPlugin" })
endreturn M
Testing
Busted (Recommended)
local mymodule = require("mymodule")
describe("mymodule.process", function()
it("returns ok for valid input", function()local result = mymodule.process({ name = "test" })
assert.are.equal("ok", result.status)
end)
it("raises on missing name", function()assert.has_error(function() mymodule.process({}) end, "missing required field: name")
end)
end)
Testing Standards
Test files: spec/*_spec.lua (busted) or test_*.lua (luaunit)
Test names describe behavior: it("returns nil when file not found")
Coverage: >80% for library modules, >60% overall
Test edge cases: nil, empty tables, boundary values, type mismatches