wandres.dev
EL LENGUAJE · minimalismo como doctrina

El toolchain: intérprete, luac y LuaRocks

Instalar Lua sin equivocarse de versión, dominar el intérprete y el REPL, entender qué hace y qué no garantiza luac, usar LuaRocks con árboles por versión, y compilar el intérprete desde la fuente en un minuto.

⏱ 15 min

El toolchain de Lua es tan pequeño que casi nadie lo estudia, y por eso casi todo el mundo tropieza con las mismas tres piedras: instala una versión que no es la de su anfitrión, descubre que los módulos se instalaron en el árbol equivocado, y confía en un archivo de bytecode que resulta no ser portable. Las tres se evitan con veinte minutos de atención. La cuarta razón para leer esta lección es más ambiciosa: compilar Lua desde la fuente es tan rápido y tan limpio que se convierte en la forma natural de trabajar.

🎯 Al terminar esta lección sabrás
  • Instalar la versión correcta de Lua y verificar cuál está usando de verdad cada herramienta.
  • Manejar el intérprete y el REPL con sus opciones y sus variables de entorno.
  • Entender qué produce luac, para qué sirve y qué garantías no ofrece nunca.
  • Usar LuaRocks con árboles separados por versión y compilar el intérprete desde la fuente.

Instalar la versión correcta

La primera decisión del toolchain no es cómo instalar sino qué versión, y ya sabes por qué: las versiones menores de Lua son lenguajes distintos que conviven. Si vas a escribir para un anfitrión, tu instalación local debe coincidir con la suya o estarás probando en un lenguaje que no es el de producción.

# macOS
brew install lua          # rama actual
brew install lua@5.1      # si tu anfitrion vive en 5.1
brew install luajit

# Debian y derivados: los paquetes ya vienen versionados
sudo apt install lua5.4 liblua5.4-dev luarocks

# Arch
sudo pacman -S lua lua51 luajit luarocks

Fíjate en que los paquetes incluyen la versión en el nombre precisamente porque están pensados para coexistir. Los binarios suelen quedar como lua5.4, lua5.1 y luajit, y el lua a secas puede ser un enlace a cualquiera de ellos según la distribución. Esa ambigüedad es la fuente número uno de confusión, así que la primera orden que debes ejecutar siempre es la de comprobación:

lua -v            # que interprete es
which -a lua      # cuantos hay en el PATH y en que orden
luajit -v
lua -e 'print(_VERSION, jit and jit.version or "sin jit")'

Esa última línea es la comprobación honesta: _VERSION te dice la versión del lenguaje y la presencia de la tabla jit te dice si estás sobre LuaJIT. Un script que asuma lo contrario fallará de formas sutiles mucho después.

El intérprete y el REPL

El ejecutable de Lua hace tres cosas: ejecuta un archivo, evalúa fragmentos sueltos y abre una sesión interactiva. Sus opciones son pocas y merece la pena conocerlas todas.

lua script.lua uno dos       # los argumentos llegan en la tabla arg
lua -e 'print(1 + 1)'        # evaluar y salir
lua -l inspect script.lua    # cargar un modulo antes de ejecutar
lua -i script.lua            # ejecutar y quedarse en modo interactivo
lua -                        # leer el programa desde la entrada estandar

La combinación de ejecutar y quedarse en interactivo es la más infravalorada del conjunto: te deja con todo el estado del programa vivo y con el REPL delante, que es la forma más rápida de inspeccionar por qué una tabla no contiene lo que esperabas.

Dentro del REPL, las versiones modernas aceptan escribir una expresión directamente y muestran su valor, sin necesidad del prefijo con el signo igual que exigían las antiguas y que aún se admite por compatibilidad. Dos variables de entorno cambian la experiencia por completo:

# codigo que se ejecuta al arrancar cualquier interprete
export LUA_INIT='local i = require("inspect"); p = function(x) print(i(x)) end'
# o cargar un archivo entero
export LUA_INIT='@/home/tu/.luarc.lua'

Ese arroba delante de la ruta es la convención de Lua para decir que el contenido es un nombre de archivo y no código. Con eso tienes el equivalente a un archivo de configuración del REPL, donde conviene meter un impresor de tablas legible, porque el print de fábrica muestra una dirección de memoria y poco más.

Conviene además saber que el REPL de fábrica no trae edición de línea: no hay historial ni flechas si el intérprete no se compiló contra readline. Es una de las razones principales para compilarlo tú.

luac, el compilador que no promete portabilidad

luac traduce fuente a bytecode de la máquina virtual. Sus dos usos legítimos son la comprobación sintáctica sin ejecutar y el volcado de bytecode para estudiar qué hace el compilador.

luac -p script.lua              # solo analizar: comprobacion sintactica
luac -o salida.luac script.lua  # compilar
luac -s -o salida.luac s.lua    # compilar y quitar informacion de depuracion
luac -l -l script.lua           # listado completo de instrucciones

El listado es la puerta de entrada a entender la máquina virtual de registros, y verlo pronto cambia la intuición sobre el coste de las cosas. Un acceso a una variable local es una instrucción sobre un registro; un acceso a un global es una búsqueda en una tabla a través de una variable superior. Esa diferencia, que en el fuente son dos identificadores idénticos, en el listado son dos instrucciones de coste distinto.

Y ahora las tres advertencias que hay que interiorizar antes de usar luac en producción. El bytecode no es portable entre versiones: un archivo compilado con 5.4 no lo carga 5.5 ni al revés. Tampoco es portable entre plataformas con distinto tamaño de palabra o distinta ordenación de bytes. Y la más grave: el bytecode no es código de confianza. La máquina virtual no valida exhaustivamente lo que recibe, así que cargar bytecode de origen desconocido es equivalente a ejecutar código nativo arbitrario. Cuando cargues datos ajenos, restringe el modo a texto de forma explícita:

local f, err = load(entrada_no_confiable, "=usuario", "t", entorno_restringido)

El tercer argumento con el valor de texto es lo que impide que llegue bytecode. Omitirlo es un fallo de seguridad, no un descuido de estilo. Y sobre la motivación habitual para distribuir bytecode, que es ocultar el fuente: no funciona. Existen descompiladores razonables, y con luac -l cualquiera reconstruye la lógica.

flowchart LR
A[Archivo .lua] --> B[luac o load: analisis y compilacion]
B --> C[Bytecode de la maquina virtual de registros]
C --> D[Ejecucion en la VM]
A --> E[luac -p: solo comprobar sintaxis]
C --> F[luac -l: listado de instrucciones]
C -.no portable entre versiones.-> G[Otro interprete]
C -.no validado: nunca de origen ajeno.-> H[Riesgo de seguridad]
style C fill:#89b4fa,color:#11111b
style H fill:#f38ba8,color:#11111b

LuaRocks y compilar desde la fuente

LuaRocks es el gestor de paquetes, y su particularidad se deduce de todo lo anterior: como los módulos con parte en C se enlazan contra una versión concreta del intérprete, los árboles de módulos son por versión. Ignorar eso produce el error clásico de instalar algo y que require no lo encuentre.

luarocks --lua-version=5.4 install penlight
luarocks --lua-version=5.1 install penlight    # otro arbol, otra copia

luarocks --local install luafilesystem          # arbol del usuario
luarocks path --lua-version=5.4                 # imprime LUA_PATH y LUA_CPATH
eval "$(luarocks path --lua-version=5.4)"

luarocks list
luarocks write_rockspec                         # esqueleto para publicar
luarocks make                                   # instalar desde el rockspec local

Las dos variables que imprime la orden de rutas son las que consulta require: una para módulos en Lua y otra para bibliotecas compartidas en C. Ambas admiten el idioma de los dos puntos y coma seguidos, que significa insertar ahí los valores por defecto en lugar de reemplazarlos.

export LUA_PATH="./?.lua;./?/init.lua;;"
export LUA_CPATH="./?.so;;"

Para aislar de verdad un proyecto, la herramienta madura es hererocks, que construye una instalación completa de Lua o LuaJIT junto con LuaRocks dentro de un directorio, al modo de los entornos virtuales de otros ecosistemas.

Y llegamos a lo que hace especial a este toolchain. Compilar Lua desde la fuente no es un procedimiento de última instancia: es cuestión de un minuto y sin dependencias.

curl -R -O https://www.lua.org/ftp/lua-5.5.0.tar.gz
tar zxf lua-5.5.0.tar.gz
cd lua-5.5.0
make all test          # detecta la plataforma y ejecuta una comprobacion minima
sudo make install      # o: make local, para dejarlo dentro del arbol de fuentes

La compilación completa tarda segundos porque son treinta mil líneas de C sin dependencias externas. Para tener historial y edición de línea en el REPL, el objetivo con readline; para experimentar con la configuración, luaconf.h es el único archivo que hay que tocar.

make linux-readline                              # REPL con historial y flechas
make all MYCFLAGS=-DLUA_USE_APICHECK              # validaciones de la API en C
make all MYCFLAGS=-DLUA_32BITS                    # numeros de 32 bits

Esa opción de validación de la API merece un comentario, porque es la herramienta más útil de la lista y casi nadie la conoce: activa comprobaciones internas que detectan los errores de disciplina de pila al escribir extensiones en C, que de otro modo se manifiestan como corrupción silenciosa mucho más tarde. En una compilación de desarrollo debería estar siempre puesta.

Compila el intérprete el primer día, no el último

Existe una asimetría rara en el mundo de Lua que conviene explotar desde el principio. En cualquier otro lenguaje, construir el intérprete desde la fuente es una operación de especialista: dependencias, horas de compilación, opciones que nadie entiende. En Lua son treinta segundos, cero dependencias y un archivo de configuración legible. Y esa facilidad tiene una consecuencia que va mucho más allá de la comodidad de tener una versión concreta: convierte el intérprete en algo que puedes modificar. Puedes añadir una instrucción a la máquina virtual y ver el efecto, poner un contador en el recolector, o instrumentar la asignación de tablas para saber dónde asigna tu programa. Ninguna de esas cosas es sensata en un runtime de millones de líneas, y en Lua son ejercicios de una tarde. Quien empieza compilando desde la fuente adquiere desde el primer día una relación distinta con la herramienta: deja de ser una caja negra cuyo comportamiento se deduce por experimentación y pasa a ser un programa en C que se puede leer, medir y parchear. Esa diferencia de postura es la que separa a quien sabe usar Lua de quien lo entiende, y es también la razón por la que el nivel de esta guía sube tan rápido después: todo lo que viene, desde las metatablas hasta la máquina virtual y la API en C, se apoya en la premisa de que la implementación está a tu alcance.

⚔️ El toolchain completo, sin atajos
  1. Instala dos versiones distintas de Lua en la misma máquina y demuestra con _VERSION que puedes ejecutar el mismo archivo en ambas obteniendo resultados distintos.
  2. Configura LUA_INIT con un impresor de tablas legible y comprueba que se aplica tanto al REPL como a la ejecución de un archivo.
  3. Compila un archivo con luac -l -l y localiza en el listado la diferencia exacta entre leer una variable local y leer un global. Cuenta las instrucciones de cada caso.
  4. Instala el mismo módulo en dos árboles de versiones distintas, provoca a propósito el error de que require no lo encuentre y arréglalo usando la orden de rutas de LuaRocks.
  5. Compila Lua desde la fuente con las validaciones de la API activadas, escribe una función en C que deje un valor de más en la pila y observa el diagnóstico que aparece.