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

Locales y bloques: el ámbito léxico

El bloque como unidad de ámbito, la regla exacta de cuándo empieza a existir una local, el idioma de inicializar una local con la variable que oculta, el límite de 200 locales por función y por qué local es la primera palabra clave que se aprende del lenguaje.

⏱ 15 min

local es la primera palabra clave que aprende quien escribe Lua, y casi siempre se enseña como una superstición: ponlo siempre. Detrás hay una regla precisa y una decisión de implementación. Lua es un lenguaje de ámbito léxico estricto: el bloque delimita la vida de un nombre, el compilador resuelve cada nombre en tiempo de compilación y una variable local no es una entrada en ninguna tabla, sino un registro de la máquina virtual. Este nivel entero va a desmontar el concepto de “variable global” hasta dejar solo azúcar sintáctica; conviene empezar entendiendo a fondo la única clase de variable que Lua tiene de verdad.

🎯 Al terminar esta lección sabrás
  • Delimitar con precisión qué es un bloque y hasta dónde vive una variable declarada en él.
  • Aplicar la regla de que el ámbito comienza en la sentencia siguiente a la declaración.
  • Distinguir local function f de local f = function y resolver la recursión mutua.
  • Conocer el límite de 200 locales por función y los atributos <const> y <close>.

El bloque, unidad de ámbito

Lua no tiene ámbito de función como el var de JavaScript clásico ni ámbito dinámico como el Lisp antiguo: tiene ámbito léxico de bloque. Un bloque es el cuerpo de un chunk, el de una función, un do ... end explícito, y el cuerpo de cualquier estructura de control. Una local declarada dentro de un bloque deja de existir —deja de ser visible— al cerrarse ese bloque.

local x = 10          -- vive hasta el final del chunk
do
  local x = x + 1     -- otra variable distinta, oculta a la anterior
  print(x)            --> 11
end
print(x)              --> 10

for i = 1, 3 do
  local y = i * i     -- 'i' e 'y' solo existen dentro del cuerpo
end
-- aqui ni 'i' ni 'y' existen

El sombreado (shadowing) es legal, silencioso y frecuente: declarar una local con el nombre de otra que ya está en contexto anula la externa dentro de la región sombreada. Ningún aviso, ninguna advertencia. Es una herramienta, no un accidente, y sostiene el idioma más común del lenguaje, que veremos en la sección siguiente.

Conviene fijar el vocabulario, porque Lua solo distingue cuatro situaciones para un nombre y todo el nivel se apoya en ellas:

📦

Local

Declarada con local en el bloque actual o en uno externo de la misma función. El compilador le asigna un registro del marco y cada acceso es directo, sin búsqueda de ningún tipo.

🎯

Parámetro

Idéntico a una local en todo: ámbito, registro y coste. El manual lo dice sin rodeos —un parámetro formal equivale a una variable local—, así que no hay una quinta categoría escondida.

🔗

Upvalue

Una local de una función envolvente usada desde dentro de otra. La closure la captura por referencia, de modo que varias closures pueden compartir y modificar la misma variable.

🌐

Nombre libre

Ninguna de las anteriores. Aquí es donde entra el resto del nivel: el compilador lo reescribe como un campo de _ENV y deja de haber garantías locales sobre su valor.

El orden importa: el compilador prueba local, luego upvalue subiendo por las funciones envolventes, y solo si fracasa declara el nombre libre. No hay ámbito dinámico, no hay resolución en tiempo de ejecución y no hay forma de que quien te llama cambie el significado de un nombre local tuyo.

Hay un detalle con consecuencias profundas: cada ejecución de una sentencia local define una variable nueva. En un bucle que crea closures, eso significa una variable por iteración, no una compartida entre todas:

local acciones = {}
local base = 100                    -- una sola 'base' para todas
for i = 1, 3 do
  local n = i                       -- una 'n' NUEVA por iteracion
  acciones[i] = function () return base + n end
end
print(acciones[1](), acciones[3]())  --> 101   103

Las tres closures comparten base como un mismo upvalue y capturan tres n distintas. Este es exactamente el problema que otros lenguajes tardaron años en corregir con let frente a var; Lua lo tiene bien resuelto desde el principio porque el bloque, y no la función, es la unidad de ámbito.

Cuándo empieza a existir una local

La regla del manual es quirúrgica: el ámbito de una declaración comienza en la primera sentencia después de la declaración. La declaración misma no está todavía en su propio ámbito. De ahí sale el idioma más reconocible del lenguaje:

local print = print          -- el 'print' de la derecha es el global
local sqrt = math.sqrt       -- fija la funcion actual en una local

En local print = print, la variable que se declara aún no es visible cuando se evalúa la expresión de la derecha, así que ese print se resuelve al externo. No hay ambigüedad ni recursión accidental: es la regla léxica trabajando a tu favor. El nivel 9.2 convierte este idioma en una técnica de rendimiento consciente.

La excepción cuidadosamente diseñada es local function, que mete el nombre en ámbito antes de compilar el cuerpo:

local function fact (n)              -- azucar de: local fact; fact = function (n)
  if n <= 1 then return 1 end
  return n * fact(n - 1)             -- 'fact' es visible aqui dentro
end

local fib = function (n)             -- NO es lo mismo
  if n < 2 then return n end
  return fib(n - 1) + fib(n - 2)     -- ERROR: 'fib' es un nombre libre aqui
end

En el segundo caso el compilador aún no conoce fib cuando compila el cuerpo, así que lo trata como un nombre libre —es decir, como una global— y en ejecución intentará llamar a nil. La recursión mutua obliga a declarar por adelantado, porque local function solo adelanta su propio nombre:

local es_par, es_impar               -- declaracion adelantada

function es_par (n)  return n == 0 or es_impar(n - 1) end
function es_impar (n) return n ~= 0 and es_par(n - 1) end
⚠️
Cuidado con la declaración adelantada

Escribir function es_par (n) ... end después de haber declarado local es_par asigna a la local, no crea una global: function nombre ... end es azúcar de nombre = function ... end, y ese nombre se resuelve por las reglas léxicas normales. Si olvidas la línea de declaración adelantada, las dos funciones se vuelven globales sin previo aviso. Ese silencio es justo el bug que Lua 5.5 ataca en la lección 9.5.

Registros, límites y atributos

Una local no vive en ninguna tabla: vive en un registro de la máquina virtual de Lua, un hueco del marco de pila que el compilador asigna estáticamente. Leerla es indexar ese marco por un índice conocido en tiempo de compilación, sin búsqueda ni hash. Esa es la razón física de que las locales sean rápidas, y el fundamento de la lección siguiente.

Y de ahí salen también los límites duros. lparser.c define MAXVARS en 200: una función no puede tener más de doscientas locales activas a la vez. Superarlo no es un error de ejecución, es un error de compilación:

too many local variables (limit is 200) in main chunk near 'local'

Rara vez lo verás escribiendo a mano; aparece en código generado —tablas de datos enormes desplegadas como declaraciones, transpiladores, grandes ficheros de configuración—. El límite hermano es MAXUPVAL, 255 upvalues por función, y el techo de 255 registros por marco. La solución nunca es pelear con el límite: es agrupar los datos en una tabla, que ocupa un registro.

Desde 5.4 una declaración local admite atributos entre ángulos. <const> marca la variable como no asignable, y el compilador lo comprueba: asignarle algo da attempt to assign to const variable. Si además su valor es un literal, el compilador puede plegarlo y ni siquiera crear el registro. <close> declara una variable to-be-closed, cuyo metamétodo __close se invoca al salir del bloque, sea por final normal, break, return o error: es el RAII de Lua.

local MAXIMO <const> = 4096          -- constante real, verificada al compilar
local f <close> = io.open("datos.txt")  -- se cierra al salir del bloque, pase lo que pase

En Lua 5.5 se suma una pieza coherente con todo esto: la variable de control de un for es ahora inmutable, un <const> implícito. Asignarle dentro del cuerpo, que antes era legal y confuso, hoy es un error de compilación.

flowchart TD
N[El compilador ve el nombre x] --> B1[Hay una local x activa en este bloque o en uno externo de la misma funcion]
B1 -- si --> R[Registro del marco: acceso directo]
B1 -- no --> B2[Hay una local x en una funcion envolvente]
B2 -- si --> U[Upvalue: captura lexica]
B2 -- no --> G[Nombre libre: se traduce a un campo de _ENV]
local no es una optimización: es una declaración de límites

El consejo de “usa siempre local” se transmite como higiene de rendimiento, y ese es su efecto menos importante. Lo que realmente hace local es declarar la extensión del universo en el que un nombre significa algo. Una local tiene un principio visible en el texto, un final visible en el texto, y un compilador que puede verificar ambos sin ejecutar nada. Esa es la propiedad que hace analizable un programa: puedes leer un bloque de veinte líneas y saber, con certeza y sin salir de esas veinte líneas, todo lo que puede modificar ese nombre. Un nombre libre no te da nada de eso; te da un contrato con el programa entero, con cualquier módulo cargado después y con cualquier corrutina que se despierte más tarde. Por eso el ámbito léxico es una tecnología de razonamiento, no de velocidad: convierte preguntas globales, que ningún humano puede responder de memoria en un proyecto real, en preguntas locales que se contestan mirando. Y por eso el arco de este nivel tiene sentido: primero se aprende que la local es la unidad de significado acotado; después, que la global no es un tipo distinto de variable, sino un campo de una tabla llamada _ENV —es decir, la ausencia deliberada de ese acotamiento—; y por fin, en 5.5, que el lenguaje ya te deja pedirle al compilador que también las globales tengan que ser declaradas. Todo el nivel es la misma idea aplicada con rigor creciente: un nombre debe tener dueño, y el dueño debe estar escrito.

⚔️ Mide el borde exacto de un ámbito
  1. Escribe local x = 1; do local x = x + 1; print(x) end; print(x) y explica cada valor a partir de la regla de “la sentencia siguiente”.
  2. Define una función recursiva con local f = function ... y observa el fallo; corrígela con local function y explica la diferencia en el azúcar sintáctico.
  3. Escribe dos funciones mutuamente recursivas correctamente locales y comprueba con type(_G.es_par) que no has creado ninguna global.
  4. Genera con un script un chunk con 250 sentencias local, cárgalo y transcribe el mensaje de error exacto que da el compilador.
  5. Declara una local MAX <const> e intenta asignarle: anota el mensaje. Repite con la variable de control de un for en Lua 5.5.