← Volver al listado de tecnologías
Capítulo 21: Code Generation
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:
- Templates: Renderización dinámica de contenido
- DSLs: Lenguajes específicos de dominio embebidos
- Optimización: Especialización de código en runtime
- Metaprogramación: Código que genera código
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.