Dynamic features such as type, pairs, load, and environments already give Lua a degree of reflection. The debug library goes further: it can inspect stack frames, local variables, upvalues, and execution events.
This power has a cost. Debug operations can be slow and can break normal lexical boundaries, so application logic should not depend on them unless there is a strong reason.
Function and stack information
1 | local function describe(fn) |
Passing a number instead of a function inspects a stack level. Request only the fields needed through the option string, because collecting line or function data has extra cost.
Locals and upvalues
debug.getlocal(level, index) returns the name and value of a local variable. debug.setlocal can replace it. For a function’s captured variables, use debug.getupvalue and debug.setupvalue:
1 | local function findUpvalue(fn, wanted) |
Most inspection functions can also receive a coroutine as their first argument, allowing a suspended coroutine’s stack to be examined.
Hooks and profiling
debug.sethook registers a callback for calls, returns, line changes, or an instruction count:
1 | local calls = 0 |
Hooks are the foundation of debuggers, tracers, profilers, and instruction-budget guards. They add overhead, and an instruction limit is only one part of sandboxing.
Security boundary
Do not expose the debug library to untrusted scripts. It can inspect or mutate values that ordinary Lua code cannot reach and can undermine an otherwise restricted environment. A sandbox should start from a small allowlist of functions, exclude debug, restrict loading and I/O, and enforce resource limits outside the script where possible.