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 0x30print(0xF0 | 0x0F) --> 255 0xFFprint(0xF0 ~ 0xFF) --> 15 xor binarioprint(~0) --> -1 negacion unaria de todos los bitsprint(1 << 8) --> 256print(256 >> 4) --> 16Hay 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 -1print(-8 // 2) --> -4 esto si divide con signoSi 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) --> 0print(1 << 1000) --> 0print(1 << -1) --> 0 equivale a 1 >> 1print(256 >> -4) --> 4096 equivale a 256 << 4Con esas piezas, el idioma clásico de banderas se escribe solo:
local LEER, ESCRIBIR, EJECUTAR = 1 << 0, 1 << 1, 1 << 2local permisos = LEER | ESCRIBIRlocal function tiene(mascara, bandera) return mascara & bandera ~= 0 endprint(tiene(permisos, ESCRIBIR)) --> truepermisos = permisos & ~ESCRIBIR -- quitar una banderaprint(tiene(permisos, ESCRIBIR)) --> falseLa 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 stringLa 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 exactoprint(math.tointeger(7.9)) --> nil no hay representacion enteraprint(math.tointeger("7")) --> nil no acepta cadenas en 5.4print(math.tointeger(2^63)) --> nil fuera de rango enteroprint(math.floor(7.9)) --> 7 redondea hacia abajoprint(math.floor(-7.1)) --> -8 hacia menos infinitoprint(math.type(math.floor(2^70)))--> float no cabe, devuelve flotanteprint(7.9 // 1) --> 7.0 redondea y conserva flotanteLa 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.
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.33333333333333print(string.format("%.17g", 1/3)) --> 0.33333333333333331print(0.1 + 0.2) --> 0.3 enganosoprint(string.format("%.17g", 0.1 + 0.2)) --> 0.30000000000000004Esa 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 representationprint(string.format("%x", 255)) --> ffprint(string.format("%.2f", 1/3)) --> 0.33print(string.format("%a", 0.5)) --> 0x1p-1En 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 integerprint(tonumber("42.0"), math.type(tonumber("42.0"))) --> 42.0 floatprint(tonumber("0x2A")) --> 42print(tonumber("ff", 16)) --> 255print(tonumber("hola")) --> nilprint(tonumber(" 7 ")) --> 7 ignora espaciosFí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.
Un número tiene tres vidas: valor, patrón y texto
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.
Cierra el nivel manipulando bits
- 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.