Los límites: 64 bits que dan la vuelta sin avisar
El entero de Lua es de 64 bits en complemento a dos y desborda envolviendo, no fallando. math.maxinteger, math.mininteger, los enteros que un flotante no puede representar y las conversiones que se niegan a ocurrir.
Un entero de Lua tiene exactamente 64 bits y no crece. Cuando se pasa del máximo no lanza un error, no promociona a flotante y no aborta el programa: da la vuelta al contador y sigue como si nada, con un resultado matemáticamente absurdo pero perfectamente definido. Al otro lado del cisma, un flotante cubre un rango astronómicamente mayor pero solo representa con exactitud los enteros de hasta 53 bits. Los dos subtipos son finitos, se solapan solo en parte, y la zona donde no se solapan es donde viven los errores más difíciles de diagnosticar de todo el lenguaje.
- Manejar
math.maxintegerymath.minintegery la aritmética modular que los une. - Detectar y prevenir el desbordamiento entero antes de que ocurra, no después.
- Entender por qué 53 bits marcan el límite de exactitud de un flotante.
- Sobrevivir a los casos patológicos del extremo negativo y a
math.ult.
El rango entero y la vuelta al contador
Los dos extremos están en la biblioteca math como constantes, no como funciones, y valen lo que cabe esperar de un entero con signo de 64 bits en complemento a dos:
print(math.maxinteger) --> 9223372036854775807
print(math.mininteger) --> -9223372036854775808
print(math.type(math.maxinteger)) --> integer
print(math.maxinteger + 1 == math.mininteger) --> true
print(math.mininteger - 1 == math.maxinteger) --> true
Esas dos últimas igualdades no son un accidente de implementación ni un comportamiento indefinido que dependa del compilador: el manual de Lua define que la aritmética entera envuelve, es decir, que se calcula módulo dos elevado a 64. Es una garantía, y esa es una diferencia enorme con C, donde el desbordamiento de un entero con signo es comportamiento indefinido y el optimizador tiene permiso para asumir que jamás ocurre. En Lua puedes razonar sobre el resultado; en C no puedes ni razonar.
Que esté definido no lo hace inofensivo. El desbordamiento sigue siendo silencioso, y silencioso significa que un cálculo de tamaños, de sumas acumuladas o de factoriales pasa de golpe a devolver números negativos sin que nada se queje.
local function factorial(n)
local r = 1
for i = 2, n do r = r * i end
return r
end
print(factorial(20)) --> 2432902008176640000 correcto
print(factorial(21)) --> -4249290049419214848 envuelto y negativo
print(factorial(25)) --> 7034535277573963776 envuelto y positivo, peor aun
El caso de 21 al menos deja una pista visible, el signo negativo. El de 25 es el verdaderamente peligroso: un número grande, positivo y plausible que es basura. Ninguna comprobación superficial lo detecta.
Detectar el desbordamiento antes de sufrirlo
Como Lua no avisa, la comprobación la escribes tú, y la forma correcta es anticipar en lugar de comprobar después. Comprobar después es tentador y a menudo incorrecto, porque el propio resultado ya está corrompido.
-- suma segura: comprueba antes, no despues
local function suma_segura(a, b)
if b > 0 and a > math.maxinteger - b then return nil, "desbordamiento por arriba" end
if b < 0 and a < math.mininteger - b then return nil, "desbordamiento por abajo" end
return a + b
end
-- multiplicacion: la division entera es el arbitro fiable
local function mul_segura(a, b)
if a ~= 0 and (a * b) // a ~= b then return nil, "desbordamiento" end
return a * b
end
La técnica de la suma es exacta porque math.maxinteger - b nunca desborda cuando b es positivo. La de la multiplicación es una comprobación a posteriori, aceptable porque la envoltura es determinista y la división entera deshace la operación solo si no hubo pérdida.
La alternativa estructural es no usar enteros donde el rango no cabe. Si tu magnitud puede superar los 64 bits, la respuesta no es una comprobación más ingeniosa: es una biblioteca de precisión arbitraria, o representar el valor como cadena y no operar con él.
Hay un caso, sin embargo, en el que la envoltura no es un fallo sino la especificación: las funciones de dispersión. Un hash multiplicativo quiere que los bits altos se descarten, y el hecho de que Lua defina la envoltura en vez de dejarla indefinida convierte un idioma que en C sería peligroso en un idioma perfectamente portátil.
-- hash FNV de 64 bits: la envoltura forma parte del algoritmo
local function fnv1a(s)
local h = 0xcbf29ce484222325
for i = 1, #s do
h = h ~ s:byte(i)
h = h * 0x100000001b3 -- desborda a proposito, y esta definido
end
return h
end
La diferencia entre este código y el factorial de arriba no está en la aritmética, que es idéntica, sino en la intención. Documenta siempre cuál de los dos casos es el tuyo: una envoltura deliberada sin un comentario que lo diga es indistinguible de un error, y el siguiente que lea el código intentará “arreglarla”.
Los 53 bits del flotante
El flotante parece la salida fácil porque llega hasta diez elevado a 308, pero su rango grande está hecho de precisión pequeña. La mantisa aporta 53 bits significativos, así que a partir de dos elevado a 53 los enteros consecutivos dejan de ser representables uno a uno: primero se pierden los impares, luego tres de cada cuatro, y así sucesivamente.
local dos53 = 2^53
print(dos53 == dos53 + 1) --> true dos enteros distintos, un flotante
print(string.format("%.0f", 2^63)) --> 9223372036854775808
print(math.maxinteger + 0.0) --> 9.2233720368548e+18
La tercera línea encierra el detalle más traicionero del nivel. math.maxinteger vale dos elevado a 63 menos uno, un número impar que necesita 63 bits significativos. Al convertirlo a flotante se redondea al valor representable más cercano, que resulta ser dos elevado a 63 exacto: un número mayor que el propio máximo entero. Es decir, la conversión de entero a flotante puede producir un valor que ya no cabe de vuelta en un entero.
print(math.maxinteger + 0.0 == math.maxinteger) --> false
print(math.tointeger(math.maxinteger + 0.0)) --> nil o fail
flowchart LR A[Enteros de 64 bits] -->|conversion siempre posible| B[Flotantes de 64 bits] B -->|conversion solo si es exacta| A A --> C[Rango pequeno y precision total] B --> D[Rango enorme y precision de 53 bits] C --> E[Zona segura comun hasta dos elevado a 53] D --> E style E fill:#a6e3a1,color:#11111b style A fill:#89b4fa,color:#11111b style B fill:#f9e2af,color:#11111b
La zona verde del diagrama es la única región donde los dos subtipos coinciden valor a valor. Por debajo de dos elevado a 53 puedes moverte entre subtipos sin pensar; por encima, cada conversión es una decisión que hay que justificar.
El extremo negativo y la comparación sin signo
El complemento a dos es asimétrico: hay un valor negativo más que positivos, y eso produce una familia de casos patológicos concentrados en math.mininteger.
print(-math.mininteger == math.mininteger) --> true no tiene opuesto
print(math.mininteger // -1 == math.mininteger) --> true tampoco se puede negar dividiendo
print(math.abs(math.mininteger)) --> -9223372036854775808
math.abs devolviendo un negativo parece un error de biblioteca y no lo es: el valor absoluto de math.mininteger sencillamente no existe dentro del tipo, y Lua prefiere la envoltura definida a inventarse un error. Cualquier código que calcule distancias, magnitudes o normas con enteros debe tratar ese valor como un caso especial, igual que se hace en C.
Por último, hay una operación que existe precisamente para trabajar con patrones de bits que representan enteros sin signo: math.ult, que compara dos enteros interpretando sus 64 bits como magnitud sin signo.
print(math.mininteger < 0) --> true lectura con signo
print(math.ult(math.mininteger, 0)) --> false lectura sin signo: es enorme
print(math.ult(-1, 1)) --> false -1 sin signo es el maximo
Es la herramienta correcta cuando lo que tienes en la variable no es una cantidad sino un identificador, un hash o una máscara que vino de C.
Hay una tentación muy extendida al aprender un lenguaje de guion: creer que sus números son números, en el sentido matemático, y que los límites son detalles de implementación que solo importan en C. Lua desmiente eso de la forma más didáctica posible, porque te obliga a convivir con dos finitudes distintas y ortogonales. El entero es finito en rango pero infinitamente preciso dentro de él: todo valor entre math.mininteger y math.maxinteger está exactamente representado, sin excepción ni redondeo, y al salirse no se degrada, se envuelve. El flotante es casi ilimitado en rango pero solo tiene 53 bits de precisión: fuera de la zona común representa cantidades que no son las que escribiste, y al salirse no envuelve, se degrada a infinito. Elegir subtipo no es, por tanto, elegir entre exacto y aproximado, sino elegir en qué dimensión estás dispuesto a fallar —y esa elección la hace tu programa cada vez que escribe un punto decimal en un literal, aunque el programador no se dé cuenta. La consecuencia práctica es que ninguna de las dos representaciones es la opción por defecto sensata para todo: los identificadores, los contadores, los índices, los tamaños y los desplazamientos quieren enteros, porque su corrección es la exactitud bit a bit; las medidas, las proporciones, las probabilidades y las coordenadas quieren flotantes, porque su corrección es el orden de magnitud. Cuando un programa mezcla las dos sin criterio, no obtiene lo mejor de ambas: obtiene el rango del entero con la precisión del flotante, y ese es el peor cuadrante posible.
- Imprime
math.maxinteger,math.mininteger, su suma y su diferencia, y verifica a mano que todo cuadra módulo dos elevado a 64. - Escribe un bucle que calcule factoriales de uno a veinticinco y marque con una advertencia la primera iteración en la que
mul_seguradetecte el desbordamiento. - Busca el primer entero
npara el quen + 0.0 == n + 1.0sea verdadero y comprueba que coincide con dos elevado a 53. - Averigua cuántos enteros distintos hay entre
math.maxintegery el flotantemath.maxinteger + 0.0que no son representables en ninguno de los dos subtipos a la vez. - Implementa
abs_segura(x)que devuelvanilparamath.minintegery documenta en un comentario por qué ese caso no puede resolverse.