44. Gramáticas y expresiones regulares

Las gramáticas describen cómo se construyen cadenas mediante reglas de producción; las expresiones regulares describen patrones regulares compactos. Juntas permiten especificar y procesar lenguajes, formatos y sintaxis.

44.1 Introducción

En el tema anterior usamos autómatas para reconocer cadenas. Otra forma de describir un lenguaje es indicar cómo generar sus cadenas a partir de símbolos y reglas: eso es una gramática.

Las expresiones regulares son una notación compacta para muchos patrones simples. Las gramáticas libres de contexto abarcan estructuras más ricas, como expresiones anidadas y bloques de un lenguaje de programación.

44.2 Gramática formal

Una gramática formal se expresa como G = (V, T, P, S):

V: símbolos no terminales.
T: símbolos terminales.
P: producciones o reglas.
S: símbolo inicial.

Los terminales aparecen en las cadenas finales;
los no terminales son categorías que se reemplazan durante una derivación.

El lenguaje generado por una gramática es el conjunto de todas las cadenas formadas solo por terminales que pueden derivarse desde S aplicando las producciones.

44.3 Terminales y no terminales

Los terminales son los símbolos que permanecen al final: letras, dígitos, operadores o palabras reservadas según el lenguaje. Los no terminales representan categorías como expresión, número o sentencia.

Ejemplo:
T = {a, b}.
V = {S}.
Producción: S → aSb | ε.

S es no terminal; a y b son terminales.

La flecha → se lee «puede reemplazarse por». La producción anterior permite envolver una derivación de S con una a a la izquierda y una b a la derecha, o terminar con la cadena vacía.

44.4 Derivaciones

Una derivación aplica producciones para transformar el símbolo inicial en una cadena de terminales. Con S → aSb | ε podemos derivar aabb:

S ⇒ aSb
⇒ aaSbb
⇒ aabb.

Por lo tanto, aabb pertenece al lenguaje generado.

El lenguaje de esta gramática es {anbn | n ≥ 0}: la misma cantidad de a seguidas por la misma cantidad de b. Los autómatas finitos no pueden reconocer este lenguaje para n arbitrario.

44.5 Gramáticas libres de contexto

Una gramática libre de contexto, GLC o CFG, tiene producciones cuyo lado izquierdo es un único no terminal. La forma general es A → α, donde A pertenece a V y α es una secuencia de terminales y no terminales.

E → E + E | E * E | (E) | número.

El no terminal E representa una expresión.
Las producciones indican formas válidas de construirla.

Las GLC son importantes para describir sintaxis con anidamiento. Los analizadores sintácticos de compiladores usan gramáticas de este tipo o variantes adaptadas para herramientas concretas.

44.6 Paréntesis balanceados

Una gramática clásica para paréntesis correctamente balanceados es:

S → ε | (S)S.

Genera: ε, (), ()(), (()), (()()), ...
No genera: (, ), )(, (().

La primera S entre paréntesis permite anidamiento; la segunda permite concatenar grupos balanceados. Esta estructura requiere una memoria de profundidad no acotada, por lo que no puede describirse con un AFD general.

44.7 Árboles de derivación

Un árbol de derivación o árbol sintáctico representa la aplicación de producciones. La raíz es el símbolo inicial, los nodos internos son no terminales y las hojas, leídas de izquierda a derecha, forman la cadena resultante.

Para S → aSb | ε y la cadena ab:

S
├── a
├── S → ε
└── b

Las hojas producen ab.

El árbol conserva la estructura que una cadena lineal no muestra. Es la base de los árboles de sintaxis abstracta que usan compiladores e intérpretes.

44.8 Ambigüedad

Una gramática es ambigua si una misma cadena admite más de un árbol de derivación distinto. La gramática E → E + E | E * E | número es ambigua para una cadena como 2 + 3 * 4.

(2 + 3) * 4 y 2 + (3 * 4)
corresponden a árboles diferentes.

Un lenguaje puede tener una gramática ambigua aunque se pueda definir una gramática no ambigua equivalente en algunos casos.

Los lenguajes de programación resuelven esto declarando precedencia y asociatividad, o usando una gramática que ya refleje la estructura deseada.

44.9 BNF y EBNF

BNF, Backus-Naur Form, es una notación usual para escribir gramáticas. EBNF agrega abreviaturas para opciones y repeticiones.

BNF:
<lista> ::= <elemento> | <elemento> "," <lista>.

EBNF:
lista = elemento, { ",", elemento }.

{...}: repetición; [...]: opcional, según la convención.

La notación facilita leer especificaciones, pero los símbolos exactos pueden variar entre documentos. Una gramática debe definir o dejar clara la convención que utiliza.

44.10 Gramáticas regulares

Las gramáticas regulares son una familia más limitada que las GLC. Sus producciones suelen tener forma A → aB o A → a, con restricciones equivalentes a las de los autómatas finitos.

Gramática regular y autómata finito:
mismo poder expresivo.

Gramática libre de contexto:
puede expresar anidamiento y dependencias como anbn.

La relación entre modelos ayuda a elegir el más conveniente: una expresión regular o un AFD para patrones regulares; una GLC y parser para estructuras recursivas.

44.11 Expresiones regulares: operaciones básicas

En teoría de lenguajes, una expresión regular se construye con símbolos, concatenación, unión y clausura de Kleene.

Concatenación: ab representa «a seguido de b».
Unión: a|b representa a o b.
Clausura: a* representa cero o más a.

a+ suele abreviar una o más a.
a? suele abreviar cero o una a.

Estas operaciones generan exactamente los lenguajes regulares. Las bibliotecas de programación usan una sintaxis ampliada para escribirlas como texto.

44.12 Expresiones regulares en JavaScript

En JavaScript, una expresión regular puede escribirse como literal entre barras o mediante el constructor RegExp. El método test indica si existe coincidencia.

const patron = /hola/;

console.log(patron.test("hola mundo")); // true
console.log(patron.test("adiós"));      // false

const dinamico = new RegExp("^" + "abc" + "$");
console.log(dinamico.test("abc")); // true

Si una parte del patrón proviene de datos externos, debe escaparse antes de construir el RegExp. Tratar texto del usuario como sintaxis de patrón puede cambiar el significado esperado.

44.13 Anclas y clases de caracteres

Las anclas indican posiciones y las clases de caracteres representan grupos de símbolos. Son herramientas comunes para validar una cadena completa.

^ inicio de cadena.
$ final de cadena.
[A-Za-z] una letra ASCII.
[0-9] un dígito ASCII.
\d un dígito según la semántica del motor.

/^[A-Za-z_][A-Za-z0-9_]*$/ valida un identificador ASCII simple.

Sin las anclas, el patrón podría encontrar una coincidencia parcial dentro de una cadena más larga. La elección entre búsqueda parcial y validación completa debe ser intencional.

44.14 Cuantificadores y agrupación

Los cuantificadores repiten el elemento inmediatamente anterior. Los paréntesis agrupan una subexpresión para aplicarle alternativas, cuantificadores o capturas.

a{3}: exactamente tres a.
a{2,5}: entre dos y cinco a.
(ab)+: una o más repeticiones de ab.
(?:ab)+: grupo sin captura.

/^(ab)+$/ acepta ab, abab, ababab, ...

Los cuantificadores codiciosos intentan consumir la mayor cantidad posible compatible con el patrón. También existen versiones no codiciosas, útiles en búsquedas, que requieren pruebas cuidadosas sobre entradas reales.

44.15 Validar un identificador

function esIdentificadorSimple(texto) {
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(texto);
}

console.log(esIdentificadorSimple("total_2026")); // true
console.log(esIdentificadorSimple("2total"));     // false
console.log(esIdentificadorSimple("total-valor")); // false

El patrón define deliberadamente un alfabeto ASCII simple. Un lenguaje internacionalizado puede permitir otras letras Unicode, pero entonces la política debe definirse y probarse explícitamente.

44.16 Regex no equivale siempre a lenguaje regular

Los motores de expresiones regulares modernos suelen incorporar características como referencias hacia atrás, búsquedas anticipadas y condiciones. Algunas van más allá del poder de los lenguajes regulares teóricos.

Teoría: expresiones regulares ↔ autómatas finitos.

Implementaciones: pueden añadir extensiones de motor.
Por ejemplo, una referencia hacia atrás compara texto previamente capturado y no se modela con un AFD general.

Conviene distinguir la noción matemática de expresión regular de la sintaxis particular de una biblioteca. Para patrones simples, ambas coinciden; para extensiones avanzadas, no necesariamente.

44.17 Riesgos de rendimiento

Algunos patrones con alternativas y cuantificadores anidados pueden requerir mucho tiempo en ciertas entradas porque el motor explora muchas combinaciones. Esto puede afectar un servicio que aplica patrones a texto no confiable.

Evitar patrones ambiguos como repeticiones anidadas sin necesidad.
Limitar longitud de entrada.
Probar casos que casi coinciden pero fallan al final.

Preferir validaciones simples y específicas cuando sea posible.

La mitigación exacta depende del motor y del entorno. La regla general es no asumir que una expresión breve tendrá costo lineal para cualquier entrada.

44.18 Análisis léxico y sintáctico

Un analizador léxico transforma caracteres en tokens, como identificadores, números y operadores. Un analizador sintáctico recibe tokens y comprueba que estén organizados según una gramática.

Texto: total = 3 + 4.
Tokens: IDENT, IGUAL, NÚMERO, MÁS, NÚMERO.
Sintaxis: asignación válida según una gramática.

Regex/autómatas suelen ayudar en la fase léxica;
gramáticas y parsers, en la fase sintáctica.

Separar las etapas simplifica la implementación y mejora los mensajes de error. Una expresión regular no es la herramienta apropiada para validar toda una sintaxis con anidamiento arbitrario.

44.19 Aplicaciones

  • Especificaciones de lenguajes y formatos de datos.
  • Compiladores, intérpretes y editores con resaltado de sintaxis.
  • Validación de identificadores, códigos y formatos acotados.
  • Extracción y búsqueda de patrones en texto.
  • Protocolos y mensajes con campos estructurados.
  • Parsers de expresiones, configuraciones y lenguajes específicos de dominio.

La herramienta debe ajustarse a la estructura de la entrada. Una gramática clara suele ser más mantenible que una expresión regular gigantesca cuando el formato tiene anidamiento o reglas contextuales.

44.20 Estrategia de diseño

  1. Define el alfabeto y las cadenas válidas con ejemplos positivos y negativos.
  2. Decide si el patrón es regular o necesita anidamiento y memoria adicional.
  3. Usa regex o autómata para la parte léxica y repetitiva.
  4. Usa una gramática y parser para estructura recursiva.
  5. Ancla las validaciones completas y limita entradas no confiables.
  6. Prueba bordes, cadena vacía, símbolos inválidos y entradas largas.

Una especificación precisa evita que la implementación invente reglas implícitas. El patrón o gramática debe ser legible por quienes lo mantendrán.

44.21 Errores frecuentes

  • Confundir terminales con no terminales.
  • Usar una gramática ambigua sin definir precedencia o asociatividad.
  • Validar una subcadena cuando se necesita validar la cadena completa por no usar anclas.
  • Usar regex para estructuras con anidamiento ilimitado.
  • Construir patrones dinámicos con texto externo sin escaparlo.
  • Ignorar casos de rendimiento adversos en patrones complejos.

44.22 Qué debes recordar y conclusión

  • Una gramática genera cadenas mediante terminales, no terminales, producciones y un símbolo inicial.
  • Las GLC describen estructuras recursivas y anidadas que superan a los autómatas finitos.
  • Las expresiones regulares describen lenguajes regulares mediante unión, concatenación y repetición.
  • BNF y EBNF son notaciones para escribir reglas gramaticales.
  • Regex y autómatas son apropiados para tokens y patrones regulares; parsers para sintaxis compleja.
  • Las validaciones deben anclarse, probarse y protegerse frente a entradas no confiables.

Las gramáticas explican cómo se construye una estructura; las expresiones regulares permiten reconocer patrones regulares de forma compacta. En el próximo tema veremos cómo las ideas de matemática discreta se aplican en bases de datos.