La diferencia: por qué Lua no incluye expresiones regulares
El motor de patrones de Lua no es una versión reducida de las expresiones regulares sino un lenguaje distinto, nacido de una restricción de tamaño y de portabilidad. Qué se ganó con esa decisión, qué se perdió, y por qué los dos formalismos son incomparables en lugar de estar uno contenido en el otro.
Casi todo el que llega a string.find espera escribir una expresión regular. Teclea \d+, no funciona; prueba (a|b), tampoco; busca la bandera de insensibilidad a mayúsculas y no existe. La reacción habitual es concluir que Lua trae una implementación pobre de las expresiones regulares. La conclusión es falsa y merece la pena desmontarla con cuidado, porque de ella depende que uses bien la herramienta durante los cuatro capítulos siguientes. Lua no trae una regex incompleta: trae otro lenguaje, diseñado desde cero bajo una restricción de tamaño brutal, con un vocabulario propio, unas capacidades que las expresiones regulares no tienen y unas carencias que las expresiones regulares no sufren. Este capítulo explica de dónde salió esa decisión y qué costó.
- Reconstruir el argumento de tamaño y de portabilidad que llevó a rechazar las expresiones regulares.
- Describir la anatomía de un patrón y en qué se diferencia estructuralmente de una regex.
- Situar ambos formalismos como incomparables, no como subconjunto y superconjunto.
- Decidir con criterio cuándo un patrón basta y cuándo el problema pide otra herramienta.
El argumento del tamaño
La cifra que se cita en el propio manual de Lua es contundente: una implementación completa de expresiones regulares POSIX ocupa más de cuatro mil líneas de C, más que todas las bibliotecas estándar de Lua juntas. El motor de patrones, en cambio, cabe en unos pocos cientos de líneas dentro del fichero que implementa la biblioteca de cadenas. La proporción no es una anécdota de ingeniería: es la tesis del lenguaje entera aplicada a un caso concreto. Lua se diseñó para ser incrustado en programas escritos en C, distribuido con ellos y compilado en plataformas donde el presupuesto de memoria se mide en kilobytes. Un intérprete cuya biblioteca de texto pesara más que el intérprete no habría sido Lua.
Conviene poner las cifras en contexto para que el argumento no suene a leyenda. El intérprete completo de Lua, con su compilador, su máquina virtual, su recolector de basura y todas sus bibliotecas, se distribuye como un puñado de ficheros de C que producen una biblioteca de unos pocos cientos de kilobytes. Un motor de expresiones regulares maduro, con su analizador de expresiones, su representación intermedia, su optimizador de prefijos, su tabla de propiedades de Unicode y sus modos de compatibilidad, es del mismo orden de magnitud él solo. Incorporarlo habría significado que la mitad del peso del lenguaje fuese la búsqueda de texto, en un lenguaje cuya razón de ser es viajar dentro de otro programa.
Al argumento del tamaño se suma otro que se olvida más a menudo y que quizá sea el decisivo: el C estándar no tiene expresiones regulares. La familia de funciones de POSIX no forma parte de la norma ISO de C, de modo que «incluir regex» no significaba enlazar con algo que ya estaba en el sistema, sino escribirlo y arrastrarlo para siempre en cada plataforma de destino. Depender de la biblioteca del sistema habría atado Lua a los sistemas de tipo Unix y roto la promesa de compilar con cualquier compilador de C conforme a la norma. La elección real no era entre regex y patrones, sino entre un lenguaje portable y pequeño con un buscador propio, o un lenguaje que dejara de ser portable o dejara de ser pequeño.
La factura de esa decisión la paga el programador en tareas concretas, y es justo enumerarlas desde el principio: no hay alternancia, así que las alternativas se prueban en un bucle; no hay grupos cuantificados, así que las estructuras repetitivas se recorren en lugar de describirse; no hay insensibilidad a la caja, así que se normaliza antes de buscar; no hay conciencia de codificación, así que el texto multibyte exige otra herramienta; y no hay expresiones con nombre ni comentarios dentro del patrón, así que la legibilidad depende de mantenerlo corto. Cinco renuncias a cambio de un motor que cabe en el margen de un folio.
El lenguaje de patrones aparece con la biblioteca de cadenas en las primeras versiones de Lua y ha cambiado muy poco desde entonces: se añadió la clase de imprimibles, se retiró la clase del byte nulo cuando las cadenas pasaron a admitir ceros incrustados y se documentó oficialmente la frontera. Treinta años sin reescrituras ni modos de compatibilidad es, en el mundo del análisis de texto, una rareza que dice bastante sobre el acierto del recorte original.
Sin compilación previa
Un patrón es una cadena corriente que el motor interpreta byte a byte durante la búsqueda. No hay objeto compilado, ni caché, ni fase de preparación como en las bibliotecas de regex.
El escape es el porcentaje
Lua usa % en lugar de la barra invertida. Como la barra invertida ya es el escape de los literales de cadena, elegirla habría obligado a duplicarla en todo patrón escrito.
Ni subconjunto ni superconjunto
Los patrones expresan cosas que una regex clásica no puede, y no expresan cosas triviales para una regex. Son formalismos cruzados.
Bytes, no caracteres
El motor trabaja sobre bytes. Las clases predefinidas dependen de la configuración regional del sistema y no entienden nada de codificaciones multibyte.
Sin retroceso catastrófico
Al no poder anidar cuantificadores sobre grupos, el patrón no puede caer en la explosión exponencial que hace vulnerables a los motores de expresiones regulares.
Un lenguaje de una página
La gramática completa se enuncia en unos párrafos del manual. No hay dialectos, ni banderas, ni diferencias entre implementaciones que memorizar.
Anatomía de un patrón
Antes de mirar la sintaxis conviene ver cómo funciona el motor, porque explica todas sus virtudes y todos sus defectos. No construye ningún autómata ni ningún árbol: recorre el patrón y el sujeto a la vez, byte a byte, y cuando llega a un cuantificador prueba una longitud, sigue con el resto del patrón y, si falla, vuelve atrás y prueba otra. Es un buscador con retroceso, escrito como una función recursiva de un centenar de líneas. Si en la posición uno del sujeto no encaja nada, reinicia el intento en la posición dos, y así hasta el final. Ese algoritmo elemental es lo que cabe en el presupuesto, y es también la razón de que no exista una fase de preparación ni un objeto de patrón que reutilizar.
Un patrón de Lua es una secuencia de elementos, y cada elemento es una clase de caracteres seguida opcionalmente de un cuantificador. Alrededor de esa estructura mínima hay cuatro añadidos: los dos anclajes, los paréntesis de captura, la coincidencia equilibrada y la frontera. Eso es todo el lenguaje. No hay grupos que agrupen para cuantificar, no hay alternancia, no hay banderas, no hay modificadores de modo, no hay aserciones de anticipación. La gramática entera se explica en una página, y esa concisión es exactamente el producto que se compró con el dinero del tamaño.
flowchart TB A[Patron como cadena corriente] --> B[Secuencia de elementos] B --> C[Clase de caracteres] B --> D[Cuantificador opcional] C --> E[Clase predefinida con porcentaje] C --> F[Conjunto entre corchetes] C --> G[Caracter literal o punto] A --> H[Anadidos estructurales] H --> I[Anclajes de inicio y de fin] H --> J[Capturas y referencias hacia atras] H --> K[Coincidencia equilibrada y frontera]
La primera consecuencia visible es la notación, y es la que causa el desconcierto inicial. Conviene fijarla de una vez para no volver a tropezar con ella:
-- lo que se escribe en otros lenguajes -> lo que se escribe en Lua
-- \d+ %d+
-- \s* %s*
-- \w+ %w+ (ojo: sin guion bajo)
-- [^abc] [^abc] (identico)
-- \. %.
-- bandera de caja insensible no existe
print(("v1.20.3"):match("%d+%.%d+%.%d+")) --> 1.20.3
La diferencia estructural con una expresión regular no está en la notación sino en el nivel de composición. Una regex es un álgebra: la concatenación, la alternancia y la estrella se aplican a expresiones enteras, de modo que cualquier subexpresión puede repetirse, alternarse o anidarse dentro de otra. En Lua el cuantificador se aplica únicamente a una clase de un solo carácter, y por tanto no hay composición recursiva. Un patrón no es un árbol de subexpresiones: es una lista plana. Esa decisión es la que hace que el motor quepa en tan poco código, porque un buscador de listas planas se escribe con un bucle y una recursión de un solo nivel, mientras que un motor de expresiones regulares necesita construir un autómata o un árbol sintáctico.
Incomparables
La creencia de que los patrones son «regex recortadas» sugiere una relación de contenido que no existe: si fuera cierta, todo patrón tendría una expresión regular equivalente y bastaría con traducir. La realidad es que la traducción falla en ambas direcciones, y falla en casos que aparecen a diario. La comparación honesta es esta:
| Rasgo | Expresiones regulares | Patrones de Lua |
|---|---|---|
| Alternancia con la barra vertical | Sí | No existe |
| Cuantificar un grupo entero | Sí | No, solo clases de un carácter |
| Perezoso sobre cualquier cuantificador | Sí | Solo -, que equivale a la estrella perezosa |
| Anidar y componer subexpresiones | Sí | No |
| Emparejar delimitadores equilibrados | No en el formalismo clásico | Sí, con %b |
| Referencias hacia atrás | Extensión no regular | Sí, con %1 a %9 |
| Tamaño del motor | Miles de líneas | Unos cientos |
-- lo que Lua hace y una regex clasica no: recortar delimitadores anidados
print(("f(a, g(b, c), d)"):match("%b()")) --> (a, g(b, c), d)
-- lo que una regex hace y Lua no: elegir entre dos alternativas
print(("perro"):match("gato|perro")) --> nil, la barra no es alternancia
Las dos últimas filas de capacidad son las interesantes. %b() reconoce el lenguaje de los paréntesis equilibrados, que es el ejemplo canónico de lenguaje no regular: ningún autómata finito puede aceptarlo, porque requiere memoria no acotada para contar la profundidad. Que Lua lo ofrezca de fábrica y las expresiones regulares clásicas no significa que, en ese eje concreto, el formalismo pequeño sea estrictamente más potente. Y sin embargo el mismo motor es incapaz de expresar «esto o aquello», que cualquier autómata finito resuelve con una unión trivial.
Conviene decirlo con precisión, porque es la idea que ordena todo el nivel. Las expresiones regulares, en su definición formal, denotan exactamente los lenguajes regulares: los que reconoce un autómata finito. Los patrones de Lua no denotan los lenguajes regulares. Por un lado se quedan cortos, porque sin alternancia ni cuantificación sobre grupos hay lenguajes regulares elementales que no pueden expresar; el conjunto de las dos cadenas ab y cd es regular y no hay patrón de Lua que lo describa sin dos búsquedas separadas. Por otro lado se pasan de largo, porque la coincidencia equilibrada reconoce un lenguaje libre de contexto propiamente dicho, y las referencias hacia atrás dentro del mismo patrón permiten exigir que dos fragmentos distantes sean iguales, lo que ni siquiera es libre de contexto. La conclusión es que ninguno de los dos formalismos contiene al otro: se cruzan. Y de ahí se sigue una consecuencia práctica que vale más que la teoría: la intuición que traes de otro lenguaje no se traduce por sustitución de símbolos. No basta con cambiar la barra invertida por el porcentaje y seguir pensando en regex, porque el patrón que escribas así fallará en cuanto necesites alternar, y en cambio pasarás por alto la herramienta que tienes delante para recortar bloques anidados. Lo que hay que reaprender no es la sintaxis: es qué preguntas sabe responder este motor. Las que sabe, las responde con un código diez veces más pequeño que el de sus primos, sin fase de compilación, sin dependencias y sin sorpresas de rendimiento exponencial. Las que no sabe, no se fuerzan: se resuelven con dos pasadas, con un bucle, o con LPeg.
Qué se ganó a cambio
Enumerar solo las carencias sería deshonesto, porque el trato tuvo contrapartidas que se disfrutan a diario y que casi nunca se nombran. La primera es que no hay fase de compilación: un patrón es una cadena y no hay que construir, cachear ni liberar ningún objeto intermedio, lo que elimina de un plumazo toda una categoría de decisiones de diseño y de fugas. La segunda es que el rendimiento es predecible y modesto: sin alternancia ni grupos cuantificados no existe el retroceso catastrófico que convierte una expresión regular inocente en una denegación de servicio, un fallo cuya literatura es abundante y cuyas víctimas incluyen a servicios enteros. La tercera es que la superficie de aprendizaje cabe en una tarde, y con ella la superficie de los errores.
-- las tareas cotidianas siguen siendo de una linea
print((" hola "):gsub("^%s*(.-)%s*$", "%1")) --> hola
print(("a,b,,c"):gsub("[^,]*", "[%0]")) --> marca cada campo
print(("clave: valor"):match("^(%w+):%s*(.*)$")) --> clave valor
Hay además una ganancia cultural que se aprecia al leer código ajeno. Como el lenguaje no permite construcciones barrocas, los patrones que encontrarás en bibliotecas de Lua son casi siempre cortos y comprensibles de un vistazo; la escalada que en otros ecosistemas lleva a expresiones de doscientos caracteres que nadie se atreve a tocar simplemente no cabe aquí. La limitación funciona como un límite de complejidad impuesto por el diseño, y ese efecto disciplinante es real aunque nunca aparezca en las listas de características.
Ese reparto explica también qué clase de problemas se resuelven bien aquí. Los patrones son excelentes para texto de formato conocido y regular: ficheros de configuración, registros de servidor, listas separadas por un delimitador, plantillas, nombres de fichero, limpieza de espacios. Son malos para texto estructurado y anidado: lenguajes de marcado, expresiones aritméticas, formatos con escapes recursivos. La regla de oro con la que conviene salir de este primer capítulo es que si el texto que analizas tiene una gramática, necesitas un analizador; si solo tiene un formato, un patrón basta y sobra.
Un patrón que no cabe cómodamente en una línea, o que necesitas leer dos veces para entender, ya te está avisando. En este lenguaje la ausencia de composición hace que la complejidad crezca de golpe, no de forma gradual: pasas de tres elementos legibles a un jeroglífico sin escalones intermedios.
Lua rechazó las expresiones regulares por tamaño y por portabilidad: la implementación POSIX pesa más que todas sus bibliotecas estándar y no forma parte del C estándar. Lo que trae en su lugar es un lenguaje propio de listas planas de elementos, sin composición recursiva, que a cambio de perder la alternancia y la cuantificación de grupos gana la coincidencia equilibrada y cabe en unos cientos de líneas. Los dos formalismos son incomparables.
- Busca en el manual la lista completa de elementos del lenguaje de patrones y cuenta cuántos son. Compárala con el índice de opciones de cualquier motor de regex moderno.
- Escribe en papel, sin ejecutar nada, cinco tareas de texto que hagas a menudo y clasifica cada una en «esto lo hace un patrón» o «esto necesita alternancia o grupos».
- Explica con tus palabras por qué elegir la barra invertida como escape habría obligado a escribir cuatro barras seguidas para buscar una sola.
- Justifica por qué el lenguaje de los paréntesis equilibrados no es regular, y por qué eso convierte a
%ben algo más que una comodidad. - Argumenta a favor y en contra de la decisión de diseño desde el punto de vista de alguien que incrusta Lua en un dispositivo con memoria muy limitada.