Why Lua Needs Specific AI Coding Rules
Lua is deliberately minimal — tables are the only data structure, metatables provide polymorphism, and the standard library is intentionally small. This minimalism makes Lua fast, embeddable, and predictable. But AI assistants trained on feature-rich languages generate over-engineered Lua: class hierarchies emulated with metatables when plain tables would suffice, complex module patterns when simple ones work, and patterns borrowed from Python or JavaScript that fight Lua's philosophy.
Lua is used in three main contexts: game scripting (Roblox, LOVE2D, game engines), embedded systems and configuration (Nginx, Redis, HAProxy), and editor extensions (Neovim). Each context has different conventions, but the core Lua idioms are shared.
Your CLAUDE.md should specify which Lua context you're in and enforce Lua's minimalist approach — the AI needs to resist its instinct to add complexity.
Rule 1: Tables as the Universal Data Structure
The rule: 'Tables are Lua's only compound data structure — use them for arrays, dictionaries, objects, modules, and namespaces. Don't emulate classes with complex metatable hierarchies unless you genuinely need inheritance. For simple data containers, use plain tables with named fields: local user = { name = "Alice", age = 30 }. For arrays, use sequential integer keys starting at 1.'
For module patterns: 'Define modules as tables with functions: local M = {} function M.greet(name) return "Hello, " .. name end return M. Never use the deprecated module() function. Return the module table at the end of the file.'
The key insight: AI assistants try to recreate Python classes or JavaScript prototypes in Lua. Most of the time, a plain table with functions is simpler, faster, and more idiomatic.
For 90% of Lua code, plain tables and functions are sufficient. AI tries to recreate Python classes or JavaScript prototypes — resist this. A table with functions is simpler, faster, and more idiomatic.
Rule 2: Metatables — Use Sparingly
The rule: 'Use metatables only when you need: operator overloading (__add, __eq, __tostring), property access control (__index, __newindex), or genuine OOP with inheritance. For 90% of Lua code, plain tables and functions are sufficient. When you do use metatables for OOP, use the standard pattern: local Class = {} Class.__index = Class function Class.new() return setmetatable({}, Class) end.'
For __index chains: 'Keep inheritance chains shallow — 2 levels maximum (base class + one child). Deep metatable chains are slow and hard to debug. Prefer composition (storing a reference to another object) over inheritance (metatable chains) for code reuse.'
AI assistants love metatables because they're the most 'interesting' Lua feature. But overusing them is the most common anti-pattern in AI-generated Lua. Simplicity is the Lua way.
AI loves metatables because they're the most 'interesting' Lua feature. But overusing them is the #1 anti-pattern in AI-generated Lua. Keep inheritance to 2 levels max — prefer composition.
Rule 3: Context-Specific Conventions
For Neovim configuration: 'Use vim.api for Neovim API calls. Use vim.keymap.set for keybindings. Use vim.opt for options. Organize config in lua/ directory following Neovim's module resolution. Use lazy.nvim for plugin management. Use vim.lsp for LSP configuration.'
For game scripting (LOVE2D): 'Use love.load, love.update, love.draw lifecycle callbacks. Keep game state in a global state table. Use delta time (dt) for frame-independent updates. Separate game logic from rendering. Use love.graphics.push/pop for coordinate transforms.'
For Roblox: 'Use ModuleScripts for shared code. Use RemoteEvents and RemoteFunctions for client-server communication. Never trust client input — validate everything on the server. Use CollectionService for tagging and component patterns. Follow Roblox naming conventions: PascalCase for instances, camelCase for variables.'
- Neovim: vim.api, vim.keymap.set, lazy.nvim, lua/ directory structure
- LOVE2D: lifecycle callbacks, delta time, state table, separate logic from rendering
- Roblox: ModuleScripts, RemoteEvents, server-side validation, PascalCase instances
- Embedded (Nginx/Redis): minimal footprint, avoid blocking, respect host API conventions
Rule 4: Error Handling and Safety
Lua uses pcall/xpcall for error handling instead of try/catch. AI assistants sometimes generate code that calls error() freely without protection, leading to crashes in embedded contexts where unhandled errors terminate the host application.
The rule: 'Use pcall() or xpcall() for any operation that might fail — file I/O, network calls, user input parsing. Return success, result pattern: local ok, result = pcall(risky_function). For expected errors, return nil, error_message instead of calling error(). Only use error() for programmer mistakes (assert-style), never for expected runtime failures.'
For Neovim: 'Use vim.notify for user-facing messages. Use pcall around plugin calls that might fail. Never let an error in config crash the editor — wrap risky initialization in pcall and log failures.'
Rule 5: Performance Patterns
Lua is often chosen for performance-critical embedding. AI assistants generate idiomatic-looking code that has hidden performance costs: creating tables in hot loops, concatenating strings with .., and using pairs() where ipairs() would be faster.
The rule: 'Localize frequently accessed globals: local insert = table.insert. Pre-allocate tables when size is known. Use table.concat for string building, not .. in loops. Use ipairs for sequential iteration (faster than pairs). Avoid creating closures in hot loops — define functions outside the loop. Cache metatable lookups in performance-critical paths.'
For LuaJIT: 'Prefer FFI over C API bindings for performance. Avoid NYI (Not Yet Implemented) patterns that prevent JIT compilation: pairs on non-table, unpack with many values, string.dump. Keep hot paths simple — LuaJIT optimizes straightforward code best.'
Lua is often chosen for performance-critical embedding. Localize globals, pre-allocate tables, use table.concat for strings, ipairs over pairs. Small habits that compound in hot loops.
Complete Lua Rules Template
Consolidated template for Lua teams. Specify your context (Neovim, game, embedded) in the header.
- Plain tables for data — metatables only for OOP/operators when genuinely needed
- Module pattern: local M = {} ... return M — no deprecated module()
- Shallow metatable chains (2 levels max) — prefer composition over inheritance
- pcall for error handling — return nil, err for expected failures, error() for assertions only
- Localize globals — pre-allocate tables — table.concat for string building
- Context: [Neovim/LOVE2D/Roblox/Embedded] — follow host-specific conventions
- 1-based indexing — respect Lua's convention, don't fight it
- StyLua for formatting — selene for linting