Lua Internals, Version History and Best Practices

52.1 How Lua Works Internally

Lua 5.0 introduced the register-based VM (earlier versions were stack-based), which led to faster execution.

Inspect bytecode with luac -l -l file.lua. The source code (lvm.c, lparser.c, ltable.c, lgc.c) is famously readable.

Source → Lexer/Parser (single pass, no AST) → Bytecode (register-based VM, 32-bit instructions) → VM interpreter loop (lvm.c)
Values: tagged unions (TValue); strings interned (short strings); tables = array part + hash part
Closures: prototypes + upvalues (open upvalues point to the stack; closed when the scope ends)
GC: tri-color incremental mark & sweep; generational mode (5.4); finalizers and weak tables

52.2 Version History

Created at Tecgraf, PUC-Rio Fallbacks (precursor of metatables), closures Multiple states, new C API

Metatables, coroutines, lexical scoping (full closures), register VM

Module system (require/package), incremental GC, varargs ..., long strings [==[; the basis of LuaJIT, Luau, Redis, WoW, Neovim

_ENV, goto, bit32, ephemeron tables, yield across pcall/metamethods, table.pack/unpack

Integer subtype (64-bit), bitwise operators, //, utf8 library, string.pack

<const> and <close>, generational GC, warn, new math.random (xoshiro), integer for loop semantics, coroutine.close; 5.4.x releases continue (5.4.7 in 2024)

Expected: global declarations, more compact arrays, and other changes (betas/release candidates appeared in 2025; check lua.org)

52.3 Best Practices

✅ Use local everywhere. Make modules return tables. Never create accidental globals (run luacheck).

✅ Follow conventions: snake_case or camelCase consistently, UPPER_CASE for constants (with <const> in 5.4), and _ for unused variables.

✅ Return nil, err for expected failures, and use error() for programming errors. Wrap boundaries with pcall/xpcall.

✅ Use table.concat for building strings, and #t + 1 for appends.

✅ Document with LuaLS annotations. Test with busted. Format with StyLua.

✅ Know your target version (5.1/LuaJIT vs 5.4) and write compatible code when needed (use compat53 for 5.3 features on 5.1).

✅ Keep metatable-based classes simple. Prefer composition.

❌ Relying on # for tables with holes, or on pairs ordering

❌ Modifying a table (adding keys) while iterating it with pairs

❌ Implicit string/number coercion in logic ❌ a and b or c when b can be false/nil

❌ Deep __index chains in hot code, and excessive debug library use

❌ Loading untrusted code without a sandbox ❌ Forgetting that indices start at 1 (and LuaJIT FFI arrays start at 0)