← Volver al listado de tecnologías

Capítulo 9: Introspección y Atributos

Por: Artiko
luaintrospectiondebugreflectionmetaprogramming

Capítulo 9: Introspección y Atributos

“El código que puede verse a sí mismo es código poderoso.” — Anónimo

La introspección (o reflexión) es la capacidad de un programa para examinar y modificar su propia estructura en runtime. Lua provee herramientas poderosas para esto, principalmente a través de la biblioteca debug y el concepto de _ENV.

La Biblioteca debug

La biblioteca debug es tu ventana al interior de Lua. Úsala con cuidado: es poderosa pero puede romper encapsulación.

ADVERTENCIA

La librería debug no debería usarse en código de producción excepto para logging/debugging. Puede violar encapsulación y tener impacto en performance.

Información de Funciones

local function greet(name)
    return "Hello, " .. name
end

-- >>> local info = debug.getinfo(greet)
-- >>> for k, v in pairs(info) do
-- >>>     print(k, v)
-- >>> end
-- source    @script.lua
-- short_src    script.lua
-- linedefined    1
-- lastlinedefined    3
-- what    Lua
-- nparams    1
-- isvararg    false

Campos importantes:

Obtener Nombre de Función

local function get_function_name(fn)
    local info = debug.getinfo(fn, "n")
    return info.name or "anonymous"
end

-- >>> print(get_function_name(print))
-- print
-- >>> print(get_function_name(function() end))
-- anonymous

Stack Traces

local function print_stack_trace()
    local level = 2  -- Empezar desde el caller
    while true do
        local info = debug.getinfo(level, "Snl")
        if not info then break end

        print(string.format("  [%d] %s:%d in function '%s'",
            level - 1,
            info.short_src,
            info.currentline or 0,
            info.name or "?"
        ))

        level = level + 1
    end
end

local function foo()
    print_stack_trace()
end

local function bar()
    foo()
end

-- >>> bar()
--   [1] script.lua:15 in function 'foo'
--   [2] script.lua:19 in function 'bar'
--   [3] script.lua:22 in function '?'

Inspeccionar Variables Locales

local function inspect_locals()
    local level = 2  -- El caller
    local i = 1

    while true do
        local name, value = debug.getlocal(level, i)
        if not name then break end

        print(string.format("  %s = %s", name, tostring(value)))
        i = i + 1
    end
end

local function demo(x, y)
    local z = x + y
    inspect_locals()
    return z
end

-- >>> demo(5, 3)
--   x = 5
--   y = 3
--   z = 8

Modificar Variables Locales

local function modify_local(level, name, new_value)
    local i = 1
    while true do
        local var_name, var_value = debug.getlocal(level, i)
        if not var_name then break end

        if var_name == name then
            debug.setlocal(level, i, new_value)
            return true
        end

        i = i + 1
    end
    return false
end

-- >>> local function test()
-- >>>     local x = 10
-- >>>     print("Before:", x)
-- >>>     modify_local(2, "x", 99)  -- Nivel 2 = esta función
-- >>>     print("After:", x)
-- >>> end
-- >>> test()
-- Before:    10
-- After:    99

Upvalues: Inspeccionar Closures

Los upvalues son variables capturadas por closures. Podemos inspeccionarlos:

local function make_counter()
    local count = 0  -- upvalue

    return function()
        count = count + 1
        return count
    end
end

local counter = make_counter()

-- >>> local name, value = debug.getupvalue(counter, 1)
-- >>> print(name, value)
-- count    0

-- >>> counter()
-- >>> name, value = debug.getupvalue(counter, 1)
-- >>> print(name, value)
-- count    1

Modificar Upvalues

-- >>> debug.setupvalue(counter, 1, 100)
-- >>> print(counter())
-- 101

Listar Todos los Upvalues

local function list_upvalues(fn)
    local i = 1
    local upvalues = {}

    while true do
        local name, value = debug.getupvalue(fn, i)
        if not name then break end

        upvalues[name] = value
        i = i + 1
    end

    return upvalues
end

-- >>> local function factory(x, y)
-- >>>     return function() return x + y end
-- >>> end

-- >>> local fn = factory(10, 20)
-- >>> local ups = list_upvalues(fn)
-- >>> for name, value in pairs(ups) do
-- >>>     print(name, value)
-- >>> end
-- x    10
-- y    20

El Global _ENV

_ENV es una variable especial que contiene el ambiente actual (las variables globales).

Inspeccionar Globales

-- >>> for name, value in pairs(_ENV) do
-- >>>     if type(value) == "function" then
-- >>>         print(name)
-- >>>     end
-- >>> end
-- print
-- pairs
-- ipairs
-- tonumber
-- tostring
-- ... (todas las funciones globales)

Crear Ambientes Aislados

local function run_sandboxed(code, sandbox_env)
    sandbox_env = sandbox_env or {}

    -- Crear ambiente con acceso limitado
    local safe_env = {
        print = print,
        tostring = tostring,
        tonumber = tonumber,
        type = type,
        pairs = pairs,
        ipairs = ipairs,
        -- NO incluir: io, os, debug, dofile, loadfile, require
    }

    -- Merge con sandbox_env
    for k, v in pairs(sandbox_env) do
        safe_env[k] = v
    end

    -- Cargar código con ambiente custom
    local fn, err = load(code, "sandbox", "t", safe_env)
    if not fn then
        return nil, err
    end

    return pcall(fn)
end

-- >>> local ok, result = run_sandboxed([[
-- >>>     local x = 10
-- >>>     local y = 20
-- >>>     return x + y
-- >>> ]])
-- >>> print(ok, result)
-- true    30

-- >>> ok, result = run_sandboxed([[
-- >>>     os.execute("rm -rf /")  -- ¡Peligroso!
-- >>> ]])
-- >>> print(ok, result)
-- false    attempt to index a nil value (global 'os')

Caso Práctico: Pretty Printer

Imprimir estructuras de datos de forma legible:

local function pretty_print(value, indent)
    indent = indent or 0
    local indent_str = string.rep("  ", indent)

    if type(value) == "table" then
        print(indent_str .. "{")

        for k, v in pairs(value) do
            if type(k) == "string" then
                io.write(indent_str .. "  " .. k .. " = ")
            else
                io.write(indent_str .. "  [" .. tostring(k) .. "] = ")
            end

            if type(v) == "table" then
                print("")
                pretty_print(v, indent + 1)
            else
                print(tostring(v))
            end
        end

        print(indent_str .. "}")
    else
        print(indent_str .. tostring(value))
    end
end

-- >>> local data = {
-- >>>     name = "Alice",
-- >>>     age = 30,
-- >>>     address = {
-- >>>         city = "Madrid",
-- >>>         country = "Spain"
-- >>>     },
-- >>>     hobbies = {"reading", "coding"}
-- >>> }
-- >>> pretty_print(data)
-- {
--   name = Alice
--   age = 30
--   address =
--   {
--     city = Madrid
--     country = Spain
--   }
--   hobbies =
--   {
--     [1] = reading
--     [2] = coding
--   }
-- }

Versión con Detección de Ciclos

local function pretty_print_safe(value, indent, seen)
    indent = indent or 0
    seen = seen or {}
    local indent_str = string.rep("  ", indent)

    if type(value) == "table" then
        if seen[value] then
            print(indent_str .. "<circular reference>")
            return
        end

        seen[value] = true
        print(indent_str .. "{")

        for k, v in pairs(value) do
            local key_str = type(k) == "string" and k or "[" .. tostring(k) .. "]"
            io.write(indent_str .. "  " .. key_str .. " = ")

            if type(v) == "table" then
                print("")
                pretty_print_safe(v, indent + 1, seen)
            else
                print(tostring(v))
            end
        end

        print(indent_str .. "}")
    else
        print(indent_str .. tostring(value))
    end
end

Caso Práctico: Profiler Simple

Medir tiempo de ejecución de funciones automáticamente:

local Profiler = {}

function Profiler.new()
    local self = {
        stats = {},
        active = true
    }

    -- Hook que se llama en cada llamada a función
    local function hook(event)
        if not self.active then return end

        local info = debug.getinfo(2, "nS")
        if not info or info.what ~= "Lua" then return end

        local name = info.name or (info.source .. ":" .. info.linedefined)

        if event == "call" then
            if not self.stats[name] then
                self.stats[name] = {
                    calls = 0,
                    total_time = 0,
                    start_time = nil
                }
            end

            self.stats[name].calls = self.stats[name].calls + 1
            self.stats[name].start_time = os.clock()

        elseif event == "return" then
            if self.stats[name] and self.stats[name].start_time then
                local elapsed = os.clock() - self.stats[name].start_time
                self.stats[name].total_time = self.stats[name].total_time + elapsed
                self.stats[name].start_time = nil
            end
        end
    end

    function self.start()
        debug.sethook(hook, "cr")
    end

    function self.stop()
        debug.sethook()
    end

    function self.report()
        print("\nProfiling Report:")
        print(string.format("%-30s %10s %15s %15s", "Function", "Calls", "Total Time", "Avg Time"))
        print(string.rep("-", 70))

        local sorted = {}
        for name, stat in pairs(self.stats) do
            table.insert(sorted, {name = name, stat = stat})
        end

        table.sort(sorted, function(a, b)
            return a.stat.total_time > b.stat.total_time
        end)

        for _, item in ipairs(sorted) do
            local avg_time = item.stat.total_time / item.stat.calls
            print(string.format("%-30s %10d %15.6f %15.6f",
                item.name,
                item.stat.calls,
                item.stat.total_time,
                avg_time))
        end
    end

    return self
end

-- Uso:
-- >>> local profiler = Profiler.new()
-- >>> profiler.start()
-- >>> -- Tu código aquí
-- >>> profiler.stop()
-- >>> profiler.report()

Caso Práctico: Debugger Simple

local Debugger = {}

function Debugger.new()
    local self = {
        breakpoints = {},
        step_mode = false
    }

    function self.add_breakpoint(file, line)
        if not self.breakpoints[file] then
            self.breakpoints[file] = {}
        end
        self.breakpoints[file][line] = true
    end

    local function hook(event, line)
        local info = debug.getinfo(2, "Sl")
        if not info then return end

        local should_break = false

        -- Check breakpoint
        if self.breakpoints[info.source] and
           self.breakpoints[info.source][line] then
            should_break = true
        end

        -- Step mode
        if self.step_mode then
            should_break = true
        end

        if should_break then
            print(string.format("\n[DEBUGGER] Stopped at %s:%d",
                info.short_src, line))
            print("Commands: (c)ontinue, (s)tep, (l)ocals, (q)uit")

            while true do
                io.write("> ")
                local cmd = io.read()

                if cmd == "c" then
                    self.step_mode = false
                    break
                elseif cmd == "s" then
                    self.step_mode = true
                    break
                elseif cmd == "l" then
                    local i = 1
                    while true do
                        local name, value = debug.getlocal(2, i)
                        if not name then break end
                        print(string.format("  %s = %s", name, tostring(value)))
                        i = i + 1
                    end
                elseif cmd == "q" then
                    debug.sethook()
                    error("Debugger stopped")
                end
            end
        end
    end

    function self.start()
        debug.sethook(hook, "l")
    end

    function self.stop()
        debug.sethook()
    end

    return self
end

Function Attributes (Lua 5.4+)

En Lua 5.4+, las variables locales pueden tener atributos:

-- Variable constante
local x <const> = 10
-- x = 20  -- Error: attempt to assign to const variable

-- Variable "to-be-closed" (se cierra automáticamente al salir del scope)
do
    local file <close> = io.open("test.txt", "w")
    file:write("Hello")
    -- No necesitas file:close(), se cierra automáticamente
end

DEEP DIVE: Cómo Funciona _ENV

Cada chunk de código en Lua tiene su propio _ENV. Cuando accedes a una variable global, realmente estás accediendo a _ENV.variable:

-- Esto:
print("Hello")

-- Es realmente:
_ENV.print("Hello")

Puedes cambiar _ENV para cambiar el ambiente:

local old_env = _ENV
local new_env = {
    print = function(msg)
        old_env.print("[CUSTOM] " .. msg)
    end
}

_ENV = new_env

-- >>> print("Hello")
-- [CUSTOM] Hello

Warnings y Best Practices

  1. No uses debug en producción excepto para logging
  2. _ENV es poderoso pero peligroso: Úsalo con cuidado
  3. Sandboxing es difícil: Aislar código no confiable requiere cuidado extremo
  4. Performance: Hooks de debug tienen impacto en performance

SOAPBOX: Introspección vs Encapsulación

La biblioteca debug rompe la encapsulación por diseño. En lenguajes como Java, necesitas reflection API. En Python, todo es público de todos modos. Lua te da el poder, pero espera que lo uses responsablemente.


Resumen del Capítulo

Introspección en Lua:

Próximo: Capítulo 10: Metatablas - El Protocolo de Objetos


Ejercicios

  1. Stack Trace Mejorado: Incluir valores de variables locales.
function enhanced_stack_trace()
    -- Tu código aquí
end
  1. Sandbox con Límite de Tiempo: Ejecutar código con timeout.
function run_with_timeout(code, timeout_seconds)
    -- Tu código aquí (pista: usa debug.sethook con contador)
end
  1. Inspector de Closures: Mostrar todos los upvalues de una función.
function inspect_closure(fn)
    -- Tu código aquí
end

-- >>> local function factory(x, y)
-- >>>     local z = x + y
-- >>>     return function() return x, y, z end
-- >>> end
-- >>> inspect_closure(factory(1, 2))
-- Upvalues:
--   x = 1
--   y = 2
--   z = 3

Lecturas Adicionales