Bits, conversión y formato: el número como patrón
Los seis operadores bit a bit sobre enteros de 64 bits, el desplazamiento lógico que no conserva el signo, la conversión que se niega cuando no es exacta, math.tointeger frente a math.floor, y el formateo que decide qué ves.
Los operadores bit a bit fueron la razón práctica que empujó el cisma de 5.3: sin un entero de verdad no hay máscaras, ni banderas, ni desplazamientos, ni interoperabilidad con las estructuras binarias del mundo de C. Al añadirlos, Lua tuvo que responder también a la pregunta inversa —qué hacer cuando alguien pide una operación de bits sobre un flotante— y la respuesta que eligió, negarse salvo cuando la conversión es exacta, define el estilo de todo el sistema numérico. Este nivel cierra con la otra cara del mismo problema: cómo se convierte un número en texto y qué se pierde por el camino.
- Usar los seis operadores bit a bit sabiendo que operan siempre sobre 64 bits con signo.
- Anticipar cuándo un flotante es aceptado en un contexto entero y cuándo produce error.
- Elegir con criterio entre
math.tointeger,math.floory la división entera. - Formatear y leer números con
string.formatytonumbersin pérdidas silenciosas.
Los seis operadores bit a bit
Lua 5.3 añadió & para la conjunción, | para la disyunción, ~ para la disyunción exclusiva en forma binaria y para la negación en forma unaria, y << y >> para los desplazamientos. Todos operan sobre los 64 bits del entero y todos devuelven un entero.
print(0xF0 & 0x3C) --> 48 0x30
print(0xF0 | 0x0F) --> 255 0xFF
print(0xF0 ~ 0xFF) --> 15 xor binario
print(~0) --> -1 negacion unaria de todos los bits
print(1 << 8) --> 256
print(256 >> 4) --> 16
Hay dos rarezas que conviene fijar de inmediato. La primera: los desplazamientos son lógicos, no aritméticos. Al desplazar a la derecha se rellena con ceros, no con el bit de signo, de modo que un número negativo se convierte en un positivo enorme.
print(-1 >> 1) --> 9223372036854775807 no es -1
print(-8 // 2) --> -4 esto si divide con signo
Si lo que quieres es dividir entre potencias de dos conservando el signo, el operador es //, no >>. El desplazamiento sirve para manipular patrones de bits, no para hacer aritmética con signo.
La segunda: un desplazamiento mayor o igual que 64 da cero, no queda indefinido como en C, y un desplazamiento negativo invierte la dirección. Otra vez, Lua prefiere definir el caso raro antes que dejarlo en manos de la plataforma.
print(1 << 64) --> 0
print(1 << 1000) --> 0
print(1 << -1) --> 0 equivale a 1 >> 1
print(256 >> -4) --> 4096 equivale a 256 << 4
Con esas piezas, el idioma clásico de banderas se escribe solo:
local LEER, ESCRIBIR, EJECUTAR = 1 << 0, 1 << 1, 1 << 2
local permisos = LEER | ESCRIBIR
local function tiene(mascara, bandera) return mascara & bandera ~= 0 end
print(tiene(permisos, ESCRIBIR)) --> true
permisos = permisos & ~ESCRIBIR -- quitar una bandera
print(tiene(permisos, ESCRIBIR)) --> false
La conversión implícita y sus negativas
Los operadores de bits exigen enteros, pero aceptan flotantes si y solo si su valor tiene representación entera exacta. No hay redondeo, no hay truncamiento silencioso: o cabe, o error.
print(3.0 & 1) --> 1
-- print(3.5 & 1) --> error: number has no integer representation
-- print("3" & 1) --> error: attempt to perform bitwise operation on a string
La tercera línea marca una diferencia importante con la aritmética: la coerción automática de cadenas a números, que sí funciona con + o *, no se aplica a los operadores de bits. Es una restricción deliberada que evita que un dato textual mal validado acabe convertido en una máscara plausible.
Conversión que exige exactitud
Los operadores de bits, string.format con %d y los índices de biblioteca aceptan un flotante solo si su valor es entero exacto. Cualquier parte fraccionaria produce un error inmediato y localizado, en vez de un resultado plausible y equivocado.
Redondeo que siempre responde
math.floor y math.ceil no fallan nunca con un número finito: devuelven un entero si cabe y un flotante si no. Son redondeos, no conversiones, y usarlos para validar entrada acepta 3.9 donde solo valía 3.
Cadenas fuera del juego de bits
La coerción de cadena a número funciona en la aritmética pero está prohibida en los operadores bit a bit. Un dato de texto sin validar no puede colarse como máscara por accidente.
El formato decide lo que ves
tostring muestra catorce dígitos significativos, no diecisiete. Dos flotantes distintos pueden imprimirse igual, así que depurar con print puede ocultarte exactamente la diferencia que estás buscando.
math.tointeger frente a math.floor
Estas dos funciones se confunden constantemente porque en el caso feliz devuelven lo mismo. Son operaciones distintas con contratos distintos.
print(math.tointeger(7.0)) --> 7 exacto
print(math.tointeger(7.9)) --> nil no hay representacion entera
print(math.tointeger("7")) --> nil no acepta cadenas en 5.4
print(math.tointeger(2^63)) --> nil fuera de rango entero
print(math.floor(7.9)) --> 7 redondea hacia abajo
print(math.floor(-7.1)) --> -8 hacia menos infinito
print(math.type(math.floor(2^70)))--> float no cabe, devuelve flotante
print(7.9 // 1) --> 7.0 redondea y conserva flotante
La regla de uso es directa. Si lo que quieres es validar que un valor recibido es un entero disfrazado, math.tointeger es la herramienta correcta, porque su fallo es la señal que necesitas. Si lo que quieres es calcular una parte entera, math.floor es la herramienta correcta, porque siempre responde. Usar math.floor para validar es el error clásico que deja pasar 3.9 como si fuera 3.
flowchart TD A[Tengo un flotante y necesito un entero] --> B[El valor deberia ser ya entero] B -->|si, es un dato de entrada| C[math.tointeger y trata el nil como error] B -->|no, quiero la parte entera| D[math.floor o math.ceil] D --> E[Comprueba el subtipo si el valor puede ser enorme] C --> F[Ahora tienes un entero garantizado] style C fill:#a6e3a1,color:#11111b style D fill:#89b4fa,color:#11111b
Existe además el idioma x | 0, que fuerza la conversión a entero usando un operador de bits y lanza error si no es exacta. Es más corto que math.tointeger pero también más críptico y produce una excepción en lugar de un valor de fallo: úsalo solo cuando el error sea realmente lo que quieres.
Formatear y leer números
La conversión a texto es el último sitio donde el subtipo importa, y donde más silenciosamente se pierde información. tostring, y por tanto print y la concatenación, usan por defecto catorce dígitos significativos para los flotantes.
print(1/3) --> 0.33333333333333
print(string.format("%.17g", 1/3)) --> 0.33333333333333331
print(0.1 + 0.2) --> 0.3 enganoso
print(string.format("%.17g", 0.1 + 0.2)) --> 0.30000000000000004
Esa tercera línea explica por qué tantos programadores creen que su comparación falla “sin motivo”: el valor impreso ya venía redondeado. Para depuración numérica seria, %.17g garantiza el viaje de ida y vuelta, y %a da la representación hexadecimal exacta del flotante, sin ninguna interpretación decimal.
Los especificadores enteros exigen representación entera, con la misma severidad que los operadores de bits:
print(string.format("%d", 7.0)) --> 7
-- string.format("%d", 7.5) --> error: number has no integer representation
print(string.format("%x", 255)) --> ff
print(string.format("%.2f", 1/3)) --> 0.33
print(string.format("%a", 0.5)) --> 0x1p-1
En el sentido contrario, tonumber es la puerta de entrada disciplinada, y acepta una base opcional que la convierte en un analizador completo.
print(tonumber("42"), math.type(tonumber("42"))) --> 42 integer
print(tonumber("42.0"), math.type(tonumber("42.0"))) --> 42.0 float
print(tonumber("0x2A")) --> 42
print(tonumber("ff", 16)) --> 255
print(tonumber("hola")) --> nil
print(tonumber(" 7 ")) --> 7 ignora espacios
Fíjate en que tonumber preserva el subtipo que el texto implica, igual que hace el analizador léxico con los literales. Y en que devuelve nil en vez de lanzar error, lo que la convierte en la forma correcta de validar entrada externa: comprueba el nil y decide tú qué hacer, en lugar de dejar que la coerción implícita adivine.
Al terminar este nivel deberías poder ver que en Lua un número es tres cosas a la vez, y que la mayor parte de los errores numéricos nacen de confundir una con otra. Es un valor matemático cuando lo sumas o lo comparas, y ahí el lenguaje se esfuerza en ser exacto incluso entre subtipos distintos. Es un patrón de bits cuando lo enmascaras o lo desplazas, y ahí el signo deja de significar magnitud para significar simplemente el bit más alto, razón por la cual -1 desplazado a la derecha se convierte en math.maxinteger sin que nada esté roto. Y es un texto cuando lo imprimes o lo lees, y ahí interviene un redondeo de presentación que puede mostrarte iguales dos valores distintos, o distintos dos valores que tu programa considera iguales. La razón profunda por la que Lua exige conversiones explícitas entre estos tres mundos —negándose a hacer bits sobre 3.5, negándose a formatear 7.5 con %d, devolviendo nil en math.tointeger en vez de redondear— no es purismo académico, es que cada conversión implícita que un lenguaje se permite es un sitio donde el programa puede seguir funcionando con un valor equivocado. Un error en la conversión es barato: para el programa en la línea exacta donde está el fallo. Un redondeo silencioso es caro: sobrevive a las pruebas, atraviesa capas, se guarda en disco y aparece semanas después convertido en un identificador que no coincide, un saldo que no cuadra o una máscara de permisos que concede lo que no debía.
- Implementa
contar_bits(n)que devuelva cuántos bits a uno tiene un entero, usando&y>>, y comprueba quecontar_bits(-1)devuelve 64. - Escribe
a_binario(n)que produzca la cadena de 64 caracteres del patrón de bits, y úsala para observarmath.minintegery-1. - Construye un conjunto de banderas con nombre y funciones
poner,quitarytiene, todas con operadores de bits. - Compara
math.tointeger,math.flooryx // 1sobre los valores 3.0, 3.5,-3.5,2^70ymath.huge, y tabula subtipo y resultado. - Escribe
leer_entero(texto)que usetonumbermásmath.tointegery devuelvanilcon un mensaje para cualquier entrada que no sea un entero exacto.