← Volver al listado de tecnologías

Capítulo 21: Code Generation

Por: Artiko
luacode-generationdslmetaprogramming

Capítulo 21: Code Generation

La capacidad de generar y ejecutar código en runtime es una de las características más potentes de Lua. Permite crear DSLs (Domain Specific Languages), plantillas dinámicas, sistemas de configuración avanzados, y mucho más.

1. Fundamentos de Code Loading

load y loadstring

-- basics.lua - Carga básica de código

-- load (Lua 5.2+) acepta strings y funciones reader
local code = [[
    local x = 10
    return x * 2
]]

local func, err = load(code)
if func then
    local result = func()
    print(result)  -- 20
else
    print("Error:", err)
end

-- Código con variables del entorno
local code_with_env = [[
    return name .. " is " .. age .. " years old"
]]

local func = load(code_with_env, "greeting", "t", {
    name = "Alice",
    age = 30
})

print(func())  -- "Alice is 30 years old"

-- loadstring (Lua 5.1) / load con string (5.2+)
local expr = "return 2 + 2"
local f = load(expr)
print(f())  -- 4

-- Cargar desde archivo
local function loadfile_safe(filename)
    local file, err = io.open(filename, "r")
    if not file then
        return nil, err
    end

    local content = file:read("*a")
    file:close()

    return load(content, "@" .. filename)
end

-- Uso
local chunk, err = loadfile_safe("config.lua")
if chunk then
    chunk()  -- Ejecutar configuración
end

Entornos Aislados (Sandboxing)

-- sandbox.lua - Ejecución segura de código

-- Sandbox básico
local function create_sandbox()
    return {
        -- Funciones seguras
        print = print,
        pairs = pairs,
        ipairs = ipairs,
        next = next,
        tonumber = tonumber,
        tostring = tostring,
        type = type,

        -- Bibliotecas seguras
        string = {
            byte = string.byte,
            char = string.char,
            find = string.find,
            format = string.format,
            gmatch = string.gmatch,
            gsub = string.gsub,
            len = string.len,
            lower = string.lower,
            match = string.match,
            rep = string.rep,
            reverse = string.reverse,
            sub = string.sub,
            upper = string.upper,
        },

        table = {
            concat = table.concat,
            insert = table.insert,
            remove = table.remove,
            sort = table.sort,
        },

        math = {
            abs = math.abs,
            ceil = math.ceil,
            floor = math.floor,
            max = math.max,
            min = math.min,
            random = math.random,
            sqrt = math.sqrt,
        },
    }
end

-- Ejecutar código en sandbox
local function run_sandboxed(code, timeout)
    local env = create_sandbox()

    local func, err = load(code, "sandbox", "t", env)
    if not func then
        return nil, err
    end

    -- Opcional: timeout con debug hooks
    if timeout then
        local start = os.clock()
        debug.sethook(function()
            if os.clock() - start > timeout then
                error("timeout exceeded")
            end
        end, "", 1000)  -- Cada 1000 instrucciones
    end

    local success, result = pcall(func)

    if timeout then
        debug.sethook()  -- Limpiar hook
    end

    if success then
        return result
    else
        return nil, result
    end
end

-- Ejemplo de uso
local user_code = [[
    local sum = 0
    for i = 1, 10 do
        sum = sum + i
    end
    return sum
]]

local result, err = run_sandboxed(user_code, 1.0)
if result then
    print("Result:", result)  -- 55
else
    print("Error:", err)
end

-- Sandbox con API personalizada
local function create_game_sandbox(player)
    local env = create_sandbox()

    -- API del juego
    env.player = {
        getName = function() return player.name end,
        getHealth = function() return player.health end,
        getPosition = function() return player.x, player.y end,
    }

    env.game = {
        log = function(msg)
            print("[Game]", msg)
        end,

        spawn = function(entity, x, y)
            -- Validar y spawnear
            if entity == "coin" or entity == "enemy" then
                -- Lógica de spawn
                print("Spawned", entity, "at", x, y)
                return true
            end
            return false
        end
    }

    return env
end

-- Ejecutar script de mod
local mod_code = [[
    game.log("Mod loaded for " .. player.getName())

    local x, y = player.getPosition()
    game.spawn("coin", x + 10, y)
]]

local player = { name = "Hero", health = 100, x = 50, y = 50 }
local env = create_game_sandbox(player)
local func = load(mod_code, "mod", "t", env)
func()

2. Generación Dinámica de Código

Template Engine

-- template.lua - Motor de plantillas

local Template = {}
Template.__index = Template

function Template.new(template_string)
    local self = setmetatable({}, Template)
    self.template = template_string
    return self
end

-- Renderizar plantilla simple
function Template:render(data)
    local code = 'return [[' .. self.template .. ']]'

    -- Reemplazar variables: ${name}
    code = code:gsub('%$%{([%w_]+)%}', function(var)
        return ']] .. tostring(' .. var .. ') .. [['
    end)

    local func = load(code, "template", "t", data)
    return func()
end

-- Plantilla con expresiones: #{expr}
function Template:render_expressions(data)
    local code = 'local _out = {}\n'

    local last_pos = 1
    for start_pos, expr, end_pos in self.template:gmatch('()#%{(.-)%}()') do
        -- Agregar texto literal
        local literal = self.template:sub(last_pos, start_pos - 1)
        code = code .. '_out[#_out + 1] = [[' .. literal .. ']]\n'

        -- Agregar expresión
        code = code .. '_out[#_out + 1] = tostring(' .. expr .. ')\n'

        last_pos = end_pos
    end

    -- Agregar texto final
    local final = self.template:sub(last_pos)
    code = code .. '_out[#_out + 1] = [[' .. final .. ']]\n'
    code = code .. 'return table.concat(_out)'

    local func = load(code, "template", "t", data)
    return func()
end

-- Plantilla con control de flujo
function Template:render_advanced(data)
    local code = {}
    code[#code + 1] = 'local _out = {}'

    for line in self.template:gmatch('[^\n]+') do
        local control = line:match('^%s*@%s*(.+)')

        if control then
            -- Línea de control: @if, @for, @end
            code[#code + 1] = control
        else
            -- Línea de contenido
            local processed = line:gsub('#%{(.-)%}', function(expr)
                return ']] .. tostring(' .. expr .. ') .. [['
            end)
            code[#code + 1] = '_out[#_out + 1] = [[' .. processed .. ']]'
        end
    end

    code[#code + 1] = 'return table.concat(_out, "\\n")'

    local func = load(table.concat(code, '\n'), "template", "t", data)
    return func()
end

-- Ejemplo de uso
local tmpl = Template.new([[
Hello ${name}!
You have ${count} messages.
]])

print(tmpl:render({
    name = "Alice",
    count = 5
}))

-- Con expresiones
local tmpl2 = Template.new([[
Result: #{x + y}
Square: #{x * x}
]])

print(tmpl2:render_expressions({
    x = 10,
    y = 20
}))

-- Con control de flujo
local tmpl3 = Template.new([[
@for i = 1, #items do
  - #{items[i].name}: $#{items[i].price}
@end
]])

print(tmpl3:render_advanced({
    items = {
        { name = "Apple", price = 1.50 },
        { name = "Banana", price = 0.80 },
        { name = "Orange", price = 1.20 },
    }
}))

Code Builder

-- builder.lua - Constructor de código

local CodeBuilder = {}
CodeBuilder.__index = CodeBuilder

function CodeBuilder.new()
    local self = setmetatable({}, CodeBuilder)
    self.lines = {}
    self.indent_level = 0
    return self
end

function CodeBuilder:indent()
    self.indent_level = self.indent_level + 1
    return self
end

function CodeBuilder:dedent()
    self.indent_level = math.max(0, self.indent_level - 1)
    return self
end

function CodeBuilder:line(code)
    local indent = string.rep("  ", self.indent_level)
    table.insert(self.lines, indent .. code)
    return self
end

function CodeBuilder:build()
    return table.concat(self.lines, "\n")
end

function CodeBuilder:compile(env)
    local code = self:build()
    return load(code, "generated", "t", env or _ENV)
end

-- Generador de funciones
local function generate_getter_setter(class_name, fields)
    local builder = CodeBuilder.new()

    builder:line("local " .. class_name .. " = {}")
    builder:line(class_name .. ".__index = " .. class_name)
    builder:line("")

    -- Constructor
    builder:line("function " .. class_name .. ".new()")
    builder:indent()
    builder:line("local self = setmetatable({}, " .. class_name .. ")")

    for _, field in ipairs(fields) do
        builder:line("self._" .. field .. " = nil")
    end

    builder:line("return self")
    builder:dedent()
    builder:line("end")
    builder:line("")

    -- Getters y setters
    for _, field in ipairs(fields) do
        -- Getter
        builder:line("function " .. class_name .. ":get_" .. field .. "()")
        builder:indent()
        builder:line("return self._" .. field)
        builder:dedent()
        builder:line("end")
        builder:line("")

        -- Setter
        builder:line("function " .. class_name .. ":set_" .. field .. "(value)")
        builder:indent()
        builder:line("self._" .. field .. " = value")
        builder:line("return self")
        builder:dedent()
        builder:line("end")
        builder:line("")
    end

    builder:line("return " .. class_name)

    return builder:compile()
end

-- Generar clase
local User = generate_getter_setter("User", {"name", "email", "age"})()

local user = User.new()
user:set_name("Alice")
    :set_email("[email protected]")
    :set_age(30)

print(user:get_name())   -- Alice
print(user:get_email())  -- [email protected]
print(user:get_age())    -- 30

3. DSLs (Domain Specific Languages)

Config DSL

-- config_dsl.lua - DSL de configuración

local function create_config_dsl()
    local config = {}
    local current_section = nil

    local env = {
        -- Definir sección
        section = function(name)
            current_section = name
            config[name] = config[name] or {}

            -- Retornar proxy para método chaining
            return setmetatable({}, {
                __index = function(_, key)
                    return function(value)
                        config[current_section][key] = value
                        return getmetatable(_).__index
                    end
                end
            })
        end,

        -- Set global
        set = function(key, value)
            config[key] = value
        end,

        -- Include otro archivo
        include = function(file)
            local chunk = loadfile(file)
            if chunk then
                setfenv(chunk, env)
                chunk()
            end
        end,
    }

    return env, config
end

local function load_config(filename)
    local env, config = create_config_dsl()

    local chunk, err = loadfile(filename)
    if not chunk then
        return nil, err
    end

    setfenv(chunk, env)
    chunk()

    return config
end

-- Archivo config.lua:
--[[
set("app_name", "MyApp")
set("version", "1.0.0")

section("database")
    .host("localhost")
    .port(5432)
    .name("mydb")

section("server")
    .port(8080)
    .workers(4)
]]

-- Cargar y usar
local config = load_config("config.lua")
print(config.app_name)           -- MyApp
print(config.database.host)      -- localhost
print(config.server.port)        -- 8080

Query DSL

-- query_dsl.lua - SQL-like DSL

local Query = {}
Query.__index = Query

function Query.from(table_name)
    local self = setmetatable({}, Query)
    self.table = table_name
    self.conditions = {}
    self.order_by_field = nil
    self.limit_value = nil
    return self
end

function Query:where(field, op, value)
    table.insert(self.conditions, {
        field = field,
        operator = op,
        value = value
    })
    return self
end

function Query:orderBy(field, direction)
    self.order_by_field = field
    self.order_direction = direction or "ASC"
    return self
end

function Query:limit(n)
    self.limit_value = n
    return self
end

function Query:build()
    local parts = {}

    -- SELECT
    table.insert(parts, "SELECT * FROM " .. self.table)

    -- WHERE
    if #self.conditions > 0 then
        local where_parts = {}
        for _, cond in ipairs(self.conditions) do
            local value_str = type(cond.value) == "string"
                and "'" .. cond.value .. "'"
                or tostring(cond.value)
            table.insert(where_parts,
                cond.field .. " " .. cond.operator .. " " .. value_str)
        end
        table.insert(parts, "WHERE " .. table.concat(where_parts, " AND "))
    end

    -- ORDER BY
    if self.order_by_field then
        table.insert(parts, "ORDER BY " .. self.order_by_field .. " " .. self.order_direction)
    end

    -- LIMIT
    if self.limit_value then
        table.insert(parts, "LIMIT " .. self.limit_value)
    end

    return table.concat(parts, " ")
end

-- Uso
local query = Query.from("users")
    :where("age", ">=", 18)
    :where("status", "=", "active")
    :orderBy("created_at", "DESC")
    :limit(10)

print(query:build())
-- SELECT * FROM users WHERE age >= 18 AND status = 'active' ORDER BY created_at DESC LIMIT 10

HTML DSL

-- html_dsl.lua - Constructor de HTML

local function html(name)
    return function(attributes_or_content, content)
        local attrs = {}
        local inner = content

        if type(attributes_or_content) == "table" and not attributes_or_content[1] then
            -- Es tabla de atributos
            for k, v in pairs(attributes_or_content) do
                table.insert(attrs, k .. '="' .. tostring(v) .. '"')
            end
            inner = content
        else
            -- No hay atributos
            inner = attributes_or_content
        end

        local attr_str = #attrs > 0 and " " .. table.concat(attrs, " ") or ""

        if inner == nil then
            return "<" .. name .. attr_str .. " />"
        elseif type(inner) == "table" then
            local children = {}
            for _, child in ipairs(inner) do
                table.insert(children, tostring(child))
            end
            return "<" .. name .. attr_str .. ">" ..
                   table.concat(children) ..
                   "</" .. name .. ">"
        else
            return "<" .. name .. attr_str .. ">" .. tostring(inner) .. "</" .. name .. ">"
        end
    end
end

-- Sugar syntax
local div = html("div")
local p = html("p")
local a = html("a")
local ul = html("ul")
local li = html("li")
local h1 = html("h1")

-- Uso
local page = div({ class = "container" }, {
    h1("Welcome"),
    p("This is a paragraph."),
    ul({
        li("Item 1"),
        li("Item 2"),
        li("Item 3"),
    }),
    a({ href = "https://lua.org" }, "Learn Lua")
})

print(page)
--[[
<div class="container">
  <h1>Welcome</h1>
  <p>This is a paragraph.</p>
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
  </ul>
  <a href="https://lua.org">Learn Lua</a>
</div>
]]

4. Macro System

Simple Macro Expansion

-- macros.lua - Sistema de macros

local Macro = {}
Macro.__index = Macro

function Macro.new()
    local self = setmetatable({}, Macro)
    self.macros = {}
    return self
end

function Macro:define(name, expander)
    self.macros[name] = expander
end

function Macro:expand(code)
    -- Encontrar y expandir macros: @macro_name(args)
    local expanded = code:gsub('@(%w+)%(([^)]*)%)', function(name, args)
        local macro = self.macros[name]
        if not macro then
            error("Unknown macro: " .. name)
        end

        -- Parsear argumentos
        local arg_list = {}
        for arg in args:gmatch('[^,]+') do
            table.insert(arg_list, arg:match('^%s*(.-)%s*$'))
        end

        return macro(table.unpack(arg_list))
    end)

    return expanded
end

function Macro:compile(code, env)
    local expanded = self:expand(code)
    return load(expanded, "macro", "t", env or _ENV)
end

-- Definir macros
local m = Macro.new()

-- Macro LOG
m:define("LOG", function(msg)
    return 'print("[LOG]", ' .. msg .. ')'
end)

-- Macro ASSERT
m:define("ASSERT", function(cond, msg)
    return 'if not (' .. cond .. ') then error(' .. msg .. ') end'
end)

-- Macro BENCHMARK
m:define("BENCHMARK", function(name, code)
    return [[
        local _start = os.clock()
        ]] .. code .. [[
        local _elapsed = os.clock() - _start
        print("[BENCH] ]] .. name .. [[:", _elapsed, "seconds")
    ]]
end)

-- Uso
local code = [[
    @LOG("Starting application")

    local x = 10
    local y = 20

    @ASSERT(x < y, "x must be less than y")

    @BENCHMARK("calculation", [[
        local sum = 0
        for i = 1, 1000000 do
            sum = sum + i
        end
    ]])

    @LOG("Application finished")
]]

local func = m:compile(code)
func()

Compile-Time Computation

-- compile_time.lua - Cómputo en tiempo de compilación

local function const_fold(expr)
    -- Evaluar expresiones constantes en compile-time
    local literals = expr:gsub('(%d+)%s*([+*/-])%s*(%d+)', function(a, op, b)
        local ops = {
            ['+'] = function(x, y) return x + y end,
            ['-'] = function(x, y) return x - y end,
            ['*'] = function(x, y) return x * y end,
            ['/'] = function(x, y) return x / y end,
        }
        local result = ops[op](tonumber(a), tonumber(b))
        return tostring(result)
    end)

    return literals
end

local function optimize_code(code)
    -- Optimizar constantes
    code = const_fold(code)

    -- Eliminar código muerto
    code = code:gsub('if false then.-end', '')

    -- Simplificar
    code = code:gsub('if true then(.-)end', '%1')

    return code
end

-- Ejemplo
local original = [[
    local x = 10 + 20
    local y = 5 * 4

    if true then
        print("This runs")
    end

    if false then
        print("This never runs")
    end
]]

local optimized = optimize_code(original)
print("=== OPTIMIZED ===")
print(optimized)
--[[
    local x = 30
    local y = 20

    print("This runs")
]]

5. Serialización Avanzada

Code Serialization

-- serialize.lua - Serialización a código Lua

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

    local t = type(value)

    if t == "nil" then
        return "nil"
    elseif t == "boolean" then
        return tostring(value)
    elseif t == "number" then
        return tostring(value)
    elseif t == "string" then
        return string.format("%q", value)
    elseif t == "table" then
        local parts = {}
        local is_array = true
        local max_index = 0

        -- Detectar si es array
        for k in pairs(value) do
            if type(k) ~= "number" or k < 1 or k ~= math.floor(k) then
                is_array = false
                break
            end
            max_index = math.max(max_index, k)
        end

        if is_array then
            -- Serializar como array
            for i = 1, max_index do
                table.insert(parts, indent_str .. "  " ..
                    serialize(value[i], indent + 1))
            end
            return "{\n" .. table.concat(parts, ",\n") .. "\n" .. indent_str .. "}"
        else
            -- Serializar como tabla
            for k, v in pairs(value) do
                local key_str
                if type(k) == "string" and k:match("^[%a_][%w_]*$") then
                    key_str = k
                else
                    key_str = "[" .. serialize(k) .. "]"
                end
                table.insert(parts, indent_str .. "  " .. key_str .. " = " ..
                    serialize(v, indent + 1))
            end
            return "{\n" .. table.concat(parts, ",\n") .. "\n" .. indent_str .. "}"
        end
    elseif t == "function" then
        return "nil -- function cannot be serialized"
    else
        return "nil -- " .. t .. " cannot be serialized"
    end
end

-- Guardar a archivo
local function save_to_file(filename, data, varname)
    local file = io.open(filename, "w")
    if not file then
        return false, "Cannot open file"
    end

    file:write(varname .. " = " .. serialize(data) .. "\n")
    file:close()
    return true
end

-- Uso
local config = {
    app = "MyApp",
    version = "1.0.0",
    database = {
        host = "localhost",
        port = 5432,
        credentials = {
            user = "admin",
            pass = "secret"
        }
    },
    servers = { "web1", "web2", "web3" }
}

save_to_file("config_gen.lua", config, "config")

-- Archivo generado:
--[[
config = {
  app = "MyApp",
  version = "1.0.0",
  database = {
    host = "localhost",
    port = 5432,
    credentials = {
      user = "admin",
      pass = "secret"
    }
  },
  servers = {
    "web1",
    "web2",
    "web3"
  }
}
]]

6. JIT Compilation Pattern

Runtime Specialization

-- jit_pattern.lua - Especialización en runtime

local function create_specialized_adder(constant)
    -- Generar función especializada para sumar constante
    local code = string.format([[
        return function(x)
            return x + %d
        end
    ]], constant)

    return load(code)()
end

local add10 = create_specialized_adder(10)
local add50 = create_specialized_adder(50)

print(add10(5))   -- 15
print(add50(25))  -- 75

-- Compilador de expresiones
local ExpressionCompiler = {}

function ExpressionCompiler.compile(expr)
    -- Convertir expresión a función
    local code = "return function(x) return " .. expr .. " end"
    return load(code)()
end

-- Cache de funciones compiladas
local compiled_cache = {}

function ExpressionCompiler.compile_cached(expr)
    if not compiled_cache[expr] then
        compiled_cache[expr] = ExpressionCompiler.compile(expr)
    end
    return compiled_cache[expr]
end

-- Uso
local f1 = ExpressionCompiler.compile_cached("x * x + 2 * x + 1")
local f2 = ExpressionCompiler.compile_cached("math.sin(x) + math.cos(x)")

print(f1(3))  -- 16 (3² + 2*3 + 1)
print(f2(0))  -- 1 (sin(0) + cos(0))

-- Especialización de bucles
local function create_loop(n, body_expr)
    local code = string.format([[
        return function()
            local result = 0
            for i = 1, %d do
                result = result + (%s)
            end
            return result
        end
    ]], n, body_expr)

    return load(code)()
end

-- Generar bucle especializado
local sum_squares = create_loop(100, "i * i")
print(sum_squares())  -- 338350

7. Debugging Generated Code

Debug Utilities

-- debug_gen.lua - Herramientas de debug

local function dump_generated_code(code, filename)
    print("=== Generated Code: " .. filename .. " ===")

    local line_num = 1
    for line in code:gmatch('[^\n]+') do
        print(string.format("%3d: %s", line_num, line))
        line_num = line_num + 1
    end

    print("=== End ===\n")
end

local function load_with_debug(code, name)
    -- Mostrar código generado
    dump_generated_code(code, name or "anonymous")

    -- Compilar con manejo de errores
    local func, err = load(code, "@" .. (name or "generated"))

    if not func then
        print("Compilation error:", err)
        return nil
    end

    return func
end

-- Ejemplo
local template = [[
local x = ${value}
return x * 2
]]

local code = template:gsub('%$%{(.-)%}', function(var)
    return var == "value" and "42" or "nil"
end)

local func = load_with_debug(code, "template_test")
if func then
    print("Result:", func())
end

Mejores Prácticas

1. Validación de Código Generado

-- Siempre validar antes de ejecutar
local function safe_load(code, env)
    local func, err = load(code, "generated", "t", env)

    if not func then
        return nil, "Compilation error: " .. err
    end

    -- Validar que no use funciones peligrosas
    local dangerous = {"os.execute", "io.popen", "loadfile"}
    for _, pattern in ipairs(dangerous) do
        if code:find(pattern) then
            return nil, "Dangerous function detected: " .. pattern
        end
    end

    return func
end

2. Cache de Código Compilado

-- Cache para evitar recompilación
local code_cache = setmetatable({}, {__mode = "v"})

local function get_or_compile(code, env)
    local key = code .. tostring(env)

    if not code_cache[key] then
        local func, err = load(code, "cached", "t", env)
        if not func then
            return nil, err
        end
        code_cache[key] = func
    end

    return code_cache[key]
end

3. Perfilado de Código Generado

-- Medir rendimiento de código generado
local function profile_generated(code, iterations)
    local func = load(code)

    local start = os.clock()
    for i = 1, iterations do
        func()
    end
    local elapsed = os.clock() - start

    print(string.format("Executed %d iterations in %.4f seconds",
        iterations, elapsed))
    print(string.format("Average: %.6f seconds per iteration",
        elapsed / iterations))
end

Conclusión

La generación de código en Lua abre posibilidades infinitas:

La clave está en balancear poder con seguridad, usando sandboxing apropiado y validación rigurosa del código generado.

Con estas técnicas, Lua se convierte en una plataforma de metaprogramación extremadamente flexible y potente.