wandres.dev
ÁMBITO Y GLOBALES · _ENV lo explica todo

Globales declarados: la gran novedad de Lua 5.5

Lua 5.5 introduce la declaración explícita de variables globales con la palabra clave global. Qué bug de veinte años resuelve, la sintaxis exacta con atributos y la forma colectiva, el error de compilación que produce un nombre no declarado, y por qué esto cambia el estilo de escribir Lua sin romper nada del código anterior.

⏱ 18 min

Durante más de treinta años, Lua tuvo un agujero que todo el mundo conocía y nadie podía cerrar sin romper el lenguaje: escribir mal el nombre de una variable no era un error, era una creación. Un contdaor en lugar de contador compilaba, se ejecutaba y devolvía nil en silencio, quizá dentro de una rama que solo se toma un martes al mes. En diciembre de 2025, Lua 5.5 cerró el agujero con la novedad más comentada de la versión: la palabra clave global, que permite declarar variables globales igual que se declaran las locales, y hacer que usar una no declarada sea un error de compilación. Esta lección la desmonta entera, porque es el cierre natural de todo el nivel.

🎯 Al terminar esta lección sabrás
  • Enunciar con precisión el fallo de diseño que la declaración de globales resuelve.
  • Dominar la sintaxis de global, sus atributos y la forma colectiva con asterisco.
  • Entender el preámbulo implícito y cómo se activa el modo estricto en un chunk.
  • Distinguir qué es puramente sintáctico y qué ocurre realmente en tiempo de ejecución.

El bug de treinta años

El problema no era que Lua tuviera globales, sino que no tenía forma de distinguir una global intencionada de un error tipográfico. Las dos se escriben igual:

local function acumular (t)
  local total = 0
  for _, v in ipairs(t) do
    totla = totla + v          -- error: lee nil, suma, revienta o no
  end
  return total
end

Ese código compilaba sin una queja en toda la historia anterior del lenguaje. Y la razón es exactamente lo que aprendimos en 9.3: totla no es una variable no declarada, es _ENV.totla, y leer una clave ausente de una tabla es una operación perfectamente legal que devuelve nil. El compilador no podía protestar sin dejar de ser correcto, porque el programa es correcto según la semántica del lenguaje.

La comunidad convivió con paliativos. El strict.lua de la lección 9.4 pone una metatabla sobre _G y falla en ejecución, solo si esa línea se ejecuta, y a costa de una llamada Lua por acceso. Los analizadores externos como luacheck avisan bien, pero son una herramienta aparte que hay que instalar, configurar y que no todo el mundo ejecuta. Cachear la biblioteca en locales, como en 9.2, reduce la superficie pero no la elimina. Ninguno era el lenguaje diciendo la verdad sobre tu programa.

La sintaxis exacta

Lua 5.5 añade una declaración simétrica a la de locales. La gramática es prácticamente la misma:

stat ::= local  attnamelist [ = explist ]
stat ::= global attnamelist [ = explist ]
stat ::= global [attrib] *
stat ::= global function Name funcbody

En la práctica:

global contador                 -- declara; no toca su valor actual
global maximo = 100             -- declara e INICIALIZA
global <const> VERSION = "1.0"  -- global de solo lectura
global function iniciar () end  -- azucar de: global iniciar; global iniciar = function () end

Cuatro precisiones importan de verdad. Primera: sin inicialización, el valor de la global se deja intacto, a diferencia de una local sin inicializar, que vale nil. Segunda: la inicialización lanza un error de ejecución si la variable ya estaba definida con un valor no nulo; es una protección deliberada contra redefinir por accidente algo que ya existe, y es lo que hace valiosa la forma global function. Tercera: el atributo const está disponible para globales, mientras que close sigue siendo exclusivo de las locales. Cuarta: el ámbito de una declaración global sigue las mismas reglas léxicas de 9.1 —empieza en la sentencia siguiente y termina con el bloque—, de modo que puede declararse dentro de un do ... end y valer solo ahí.

El preámbulo implícito y el modo estricto

Aquí está el diseño fino, y es lo que permite que Lua 5.5 no rompa el mundo. Todo chunk arranca con una declaración implícita global *, que declara como globales todos los nombres libres. Esa declaración preambular queda anulada dentro del ámbito de cualquier otra declaración global. El ejemplo del manual lo dice todo:

X = 1       -- Ok, global por defecto
do
  global Y  -- anula la declaracion implicita inicial
  Y = 1     -- Ok, Y esta declarada
  X = 1     -- ERROR, X no esta declarada
end
X = 2       -- Ok, global por defecto otra vez

La regla resultante es de una economía notable: fuera de toda declaración global, Lua funciona como siempre; dentro de una, no hay valor por defecto y todo debe declararse. El modo estricto no es una bandera del intérprete ni una opción de compilación: se activa escribiendo una sola declaración.

La forma colectiva con asterisco da el control fino. global * restaura el comportamiento clásico de lectura y escritura libres; global <const> * declara como globales de solo lectura todos los nombres no declarados antes. Esa segunda forma es la cabecera práctica que conviene adoptar:

-- Cabecera recomendada de un fichero en Lua 5.5
global estado, contador          -- las globales que este fichero ESCRIBE
global <const> *                 -- todo lo demas: lectura libre, escritura prohibida

contador = 0                     -- Ok, declarada como escribible
print(math.pi)                   -- Ok, 'print' y 'math' son de solo lectura
contdaor = 1                     -- ERROR de compilacion: es de solo lectura

Consigues lo mejor de los dos mundos: no tienes que enumerar las cuarenta funciones de la biblioteca estándar que usas, y cualquier escritura accidental a una global —que es la vía por la que entran casi todos los bugs de este tipo— muere al compilar. Si quieres la máxima severidad, declara solo lo que uses y omite el asterisco: entonces también las lecturas de nombres no declarados son un error.

Los mensajes son claros y llegan del compilador, no del programa en marcha. Un nombre no declarado dentro del ámbito estricto da variable 'contdaor' not declared; asignar a una global de solo lectura da attempt to assign to const variable 'contdaor'.

Conviene ver los tres regímenes uno al lado del otro, porque la elección es tuya y se hace fichero a fichero:

🕰️

Sin declaraciones

Rige el preámbulo implícito global *. Todo nombre libre es global de lectura y escritura. Es el Lua de siempre, y es lo que permite que treinta años de código compilen sin tocar una línea.

🛡️

Colectiva de solo lectura

Declaras primero las globales que escribes y cierras con global <const> *. Lecturas libres, escrituras accidentales rechazadas al compilar. El equilibrio recomendado para código nuevo.

🔒

Estricto total

Solo declaraciones explícitas, sin forma colectiva. Todo nombre libre no declarado —se lea o se escriba— es un error de compilación. Ideal para módulos pequeños y críticos.

🎚️

Por bloques

Como el ámbito de global es léxico, puedes abrir un do ... end estricto dentro de un fichero laxo. Migrar un proyecto grande no exige convertirlo entero de golpe.

⚠️
Dos incompatibilidades reales

global es ahora una palabra reservada: cualquier código anterior que usara global como nombre de variable, de campo con la sintaxis de punto o de parámetro deja de compilar. Y recuerda que LuaJIT sigue anclado en la semántica de 5.1: nada de esta lección existe allí. Si tu código debe correr en ambos, la declaración de globales no es una opción.

Qué cambia y qué no cambia

Conviene ser muy preciso, porque es fácil sobreinterpretar la novedad. El efecto de una declaración global es puramente sintáctico, salvo por la asignación opcional. No crea una tabla nueva, no cambia dónde vive el valor, no altera la traducción de 9.3 y no cuesta nada en ejecución: contador sigue siendo _ENV.contador y sigue compilando a un GETTABUP. Lo que ha cambiado es que ahora el compilador sabe qué nombres tenías intención de usar así, y puede rechazar los demás.

De ello se deducen las limitaciones, todas ellas visibles en el ejemplo del propio manual:

global X <const>, _G
X = 1           -- ERROR, es de solo lectura
_ENV.X = 1      -- Ok: la ruta explicita no pasa por la declaracion
_G.print(X)     -- Ok
foo()           -- 'foo' puede cambiar cualquier global libremente

<const> protege el nombre, no el valor: la ruta explícita por _ENV o por _G sigue abierta, y cualquier función a la que llames puede modificar cualquier entrada del entorno. No es un sistema de tipos, no es inmutabilidad y no es aislamiento —para eso está _ENV, la lección 9.4—. Es exactamente lo que promete: la declaración de intención que faltaba, verificada al compilar y sin coste alguno en ejecución.

flowchart TD
A[El compilador ve un nombre libre] --> B[Estoy dentro del ambito de alguna declaracion global]
B -- no --> C[Preambulo implicito global asterisco]
C --> D[Se acepta como global de lectura y escritura]
B -- si --> E[Ese nombre fue declarado]
E -- si --> F[Acceso normal via _ENV]
E -- no --> G[Hay una forma colectiva previa]
G -- global const asterisco --> H[Lectura permitida y escritura rechazada al compilar]
G -- no la hay --> I[Error: variable no declarada]
Treinta años para añadir una palabra

Merece la pena medir la magnitud de lo ocurrido. El fallo era conocido desde el primer día, lo sufría absolutamente todo el mundo, la solución cabe en una palabra clave y aun así hicieron falta más de tres décadas. No fue lentitud: fue que en Lua las globales no eran una característica que arreglar, sino una consecuencia inevitable de dos decisiones más profundas y ambas correctas —que el entorno es una tabla corriente, y que indexar una tabla con una clave ausente devuelve nil en vez de fallar—. Prohibir el nombre no declarado exigía romper una de las dos, y ninguna se podía tocar sin dejar de ser Lua. La salida llegó cuando los autores dejaron de tratarlo como un problema de semántica y lo trataron como un problema de declaración de intención: el lenguaje no necesitaba cambiar lo que ocurre en ejecución, necesitaba que el programador pudiera escribir qué nombres pensaba usar como globales, para que el compilador pudiese rechazar el resto. Por eso la implementación es enteramente sintáctica, por eso no cuesta un solo ciclo, por eso global es simétrica de local en gramática y en reglas de ámbito, y por eso el preámbulo implícito global * deja compilar sin tocar una coma todo el Lua escrito desde 1993. Es una lección de diseño que trasciende al lenguaje: cuando una característica tuya lleva años pareciendo un fallo irreparable, quizá no estés ante un error de semántica, sino ante información que el programador nunca tuvo forma de expresar. Y observa el arco completo de este nivel, porque es el mismo movimiento repetido tres veces. local fue el lenguaje diciendo “este nombre tiene dueño y el dueño está escrito”. _ENV fue el lenguaje diciendo “los nombres sin dueño tampoco son magia: son campos de una tabla que puedes ver, cambiar y aislar”. global es el lenguaje diciendo, por fin, “y ahora también los nombres sin dueño tienen que estar escritos”. Treinta años para que un lenguaje de ocho tipos admitiera que todo nombre en un programa debería tener una declaración que lo respalde —y para hacerlo sin añadir un solo caso especial, sin cambiar la máquina virtual y sin invalidar una línea del código existente.

⚔️ Pasa un fichero real a modo estricto
  1. Reproduce el error tipográfico del acumulador en Lua 5.4 y comprueba que compila y se ejecuta sin protestar.
  2. Añade global <const> * al principio en Lua 5.5 y transcribe el mensaje exacto de compilación que ahora aparece.
  3. Prueba el ejemplo del manual con el bloque do ... end y explica por qué X = 2 vuelve a ser legal al salir del bloque.
  4. Declara global VERSION <const> = "1.0" e intenta asignarle; después consigue el mismo cambio vía _ENV y explica por qué no lo impide.
  5. Ejecuta dos veces global maximo = 100 en el mismo entorno y observa el error de inicialización sobre una global ya definida.
  6. Coge un fichero tuyo de más de cien líneas, ponle la cabecera recomendada y cuenta cuántas globales accidentales tenías.