08/09/2023
En el vasto universo de la programación, el manejo y procesamiento de entradas estructuradas es una tarea recurrente y fundamental. Ya sea para interpretar un nuevo lenguaje de programación, validar formatos de archivo o construir herramientas de análisis, la capacidad de un programa para ‘entender’ su entrada es crucial. Es aquí donde entran en juego dos herramientas poderosas y complementarias: Flex y Bison.

Originalmente concebidas para el desarrollo de compiladores, la utilidad de Flex y Bison ha trascendido con creces ese ámbito, demostrando ser increíblemente versátiles en una multitud de aplicaciones. Son los modernos sucesores de las clásicas herramientas Lex y Yacc, respectivamente, y se han consolidado como estándares de facto en el desarrollo de programas que requieren un análisis léxico y sintáctico robusto. Ambas herramientas están ampliamente disponibles en sistemas operativos como Linux (a través de APT) y también cuentan con versiones para Windows, facilitando su acceso a una amplia comunidad de desarrolladores.
Flex: El Analizador Léxico
Flex, abreviatura de “Fast Lexical Analyzer Generator”, es una herramienta diseñada para generar escáneres o analizadores léxicos. Un escáner es un programa encargado de reconocer patrones léxicos (o tokens) dentro de un texto de entrada. Su función principal es leer el flujo de caracteres de entrada y agruparlos en unidades significativas, como palabras clave, identificadores, números, operadores, etc.
¿Cómo funciona Flex?
El proceso con Flex comienza con un archivo de entrada que contiene una descripción del escáner que se desea generar. Esta descripción se compone de pares de expresiones regulares y código C, conocidos como 'reglas'. Flex procesa este archivo y genera como salida un archivo C (por defecto, lex.yy.c), el cual define una rutina llamada yylex(). Esta función yylex() es la que realiza el escaneo real del texto de entrada, devolviendo los tokens reconocidos.
Estructura del Archivo de Entrada de Flex
Un archivo de entrada de Flex está típicamente dividido en tres secciones principales, separadas por líneas que contienen únicamente %%:
- Sección de Definiciones: Se utiliza para simplificar la especificación del escáner asignando nombres a expresiones regulares. Una definición tiene la forma
nombre definición. Por ejemplo:digit [0-9]. Opcionalmente, se puede incluir un bloque%topque se copia literalmente al inicio del archivo C generado, útil para incluir cabeceras o definir macros, como#include <stdio.h>. - Sección de Reglas: Contiene la serie de reglas que definen los patrones léxicos y las acciones asociadas. Una regla se compone de un patrón (una expresión regular o un nombre definido en la sección de definiciones) y un bloque de código C. Por ejemplo:
{digit}+ { printf("Número: %s\n", yytext); }. Es importante notar queyytextapunta al primer carácter de la coincidencia en el búfer de entrada. Si el valor deyytextnecesita ser preservado más allá de la llamada actual ayylex(), se deben usar funciones comostrdup(). - Sección de Código de Usuario: Este bloque de código se copia literalmente al archivo
lex.yy.c. Se utiliza para rutinas complementarias que llaman o son llamadas por el escáner (como la funciónmaino funciones de manejo de errores). Esta sección es opcional; si no se utiliza, el segundo%%puede omitirse.
Consideraciones Importantes en Flex
Los comentarios pueden aparecer en casi cualquier lugar del archivo de entrada de Flex, con algunas excepciones clave: no están permitidos donde Flex espera una expresión regular (por ejemplo, al principio de una línea o inmediatamente después de una lista de estados de escáner) ni en líneas %option.
Una característica importante es yywrap(). Cuando yylex() recibe una indicación de fin de archivo, invoca yywrap(). Si yywrap() devuelve falso (cero), yylex() asume que yyin (el archivo de entrada global) ha sido redirigido a otra fuente y la exploración continúa. Si devuelve verdadero (no cero), el proceso de exploración termina. La opción %option noyywrap hace que yywrap() siempre devuelva verdadero, simplificando el manejo del fin de archivo.
Estados en Flex
Flex proporciona un mecanismo para activar condicionalmente reglas a través de los estados. Una regla cuyo patrón está prefijado con <estado> solo estará activa cuando el escáner se encuentre en ese estado. Los estados se declaran en la sección de definiciones utilizando líneas sin sangría que comienzan con %s (para estados inclusivos) o %x (para estados exclusivos) seguidos de una lista de nombres. Si el estado es inclusivo, las reglas sin estados también estarán activas. Si es exclusivo, solo las reglas calificadas con la condición de inicio estarán activas.
Un estado se activa utilizando la acción BEGIN y especificando el siguiente estado. Por ejemplo, para reconocer y descartar comentarios de C:
%x comentario %% "/*" { BEGIN(comentario); } <comentario>. { ; } <comentario>"*/" { BEGIN(INITIAL); }Proceso de Ejecución de Flex
Para generar el archivo de implementación del escáner (lex.yy.c), se ejecuta el comando: flex scanner.l. Luego, este archivo C debe ser compilado: gcc -o main lex.yy.c. Finalmente, el escáner puede ejecutarse: ./main archivo_entrada.txt.
Bison: El Analizador Sintáctico
Bison es un generador de analizadores sintácticos o parsers. Su función es recibir una secuencia de tokens (generados, por ejemplo, por Flex) y verificar si esta secuencia cumple con una gramática especificada. Aunque Bison puede producir parsers en C++, Java y otros lenguajes, su uso más común es con C.

¿Cómo funciona Bison?
Para operar, Bison requiere dos funciones esenciales:
- La función del analizador léxico,
yylex(), que reconoce los tokens y los devuelve al parser. - La función de informe de errores,
yyerror(), invocada por el parser cada vez que encuentra un token que no puede satisfacer ninguna regla gramatical.
A partir del archivo de entrada de Bison, se crea un archivo de implementación del parser (por defecto, parser.tab.c) que contiene la función de análisis principal, yyparse().
Estructura del Archivo de Entrada de Bison
Un archivo de entrada de Bison consta de cuatro secciones principales, similar a Flex, también separadas por líneas con %%:
- Sección de Prólogo: Se encierra entre
%{y%}. Contiene definiciones de macros, declaraciones de funciones y variables que se utilizarán en las acciones de las reglas gramaticales. Bison también ofrece la directiva%codecon calificadores explícitos (requires,provides,top) para controlar la ubicación del código generado. - Sección de Declaraciones: Aquí se definen los símbolos terminales (tokens) y no terminales de la gramática. Los tokens se pueden definir por nombre (
%token nombre), por carácter (%token 'c') o por literal de cadena (%token "literal"). Los símbolos no terminales se declaran con%type <tipo> nombreo%nterm <tipo> nombre. Las declaraciones de no terminales son obligatorias solo si se necesita especificar sus tipos de datos semánticos. - Sección de Reglas Gramaticales: Contiene las reglas de la gramática en el formato
lado_izquierdo: componente1 componente2 .... Múltiples reglas con el mismo lado izquierdo pueden condensarse usando el carácter de barra vertical|. También se pueden insertar acciones (bloques de código C) en medio de una regla; estas se ejecutan antes de que el parser reconozca los componentes siguientes. Es común encontrar reglas vacías, que se utilizan para indicar que un componente es opcional. - Sección de Epílogo: Este código se copia literalmente al final del archivo de implementación del parser. Si está vacía, el último
%%puede omitirse.
La Sinergia de Flex y Bison
Para que Flex y Bison trabajen juntos, es fundamental que compartan la misma lista de tokens disponibles. Esto se logra fácilmente incluyendo el archivo de cabecera generado por Bison (parser.tab.h) en el archivo de entrada del escáner (Flex). Este archivo parser.tab.h, producido junto con parser.tab.c, contiene el prototipo de la función yyparse() de Bison, así como las declaraciones de constantes para cada token declarado en el archivo de entrada del parser.
Es una práctica común utilizar un archivo adicional, como main.c, para implementar la función de informe de errores yyerror() y la función principal main(). Aunque es arbitrario y podrían incluirse en las secciones de código de usuario o epílogo, mantenerlas separadas puede mejorar la modularidad.
Una implementación básica de yyerror() simplemente imprime el mensaje de error:
void yyerror(char const *message) { printf("Error: %s\n", message); }La función main(), por su parte, se encarga de configurar el archivo de entrada global yyin y de invocar a yyparse():
int main(int argc, char const *argv[]) { yyin = fopen(argv[1], "r"); int result_code = yyparse(); fclose(yyin); return result_code; }Para que el escáner (Flex) genere un archivo de cabecera propio (ej., lex.yy.h), necesario si se usan funciones o variables de Flex en otros archivos, se debe añadir la opción %option header-file="lex.yy.h" al archivo de Flex. En el archivo de Bison, las funciones yylex() y yyerror() deben declararse como extern, ya que yyparse() las necesita para funcionar.
Proceso de Ejecución Conjunta
La compilación de un proyecto que usa Flex y Bison sigue varios pasos:
- Generar los archivos de implementación:
flex scanner.lybison -d parser.y(la opción-des crucial para generarparser.tab.h). - Compilar los archivos C generados y el archivo
main.c:gcc -c -o scanner.o lex.yy.c,gcc -c -o parser.o parser.tab.c,gcc -c -o main.o main.c. - Enlazar los objetos compilados para crear el ejecutable final:
gcc -o main parser.o scanner.o main.o. - Ejecutar el parser:
./main archivo_entrada.txt.
Manejo Avanzado con Flex y Bison
Manejo y Recuperación de Errores
Es inaceptable que un programa termine abruptamente ante un error sintáctico. Por ejemplo, un compilador debería recuperarse lo suficiente para continuar analizando el resto del archivo. Bison permite definir cómo recuperarse de un error sintáctico mediante la regla especial error. Este es un símbolo terminal siempre definido y reservado para el manejo de errores. Cuando ocurre un error sintáctico, el parser de Bison genera un token error; si existe una regla para reconocerlo en el contexto actual, el análisis puede continuar. Por ejemplo: sentencia: error ';'; permitiría saltar hasta el siguiente punto y coma tras un error.
Bison también permite forzar explícitamente la recuperación de errores mediante la macro YYERROR. Además, la variable yynerrs se incrementa cada vez que ocurre un error, contando el número de errores encontrados durante el proceso de análisis. La función yyerror() también puede ser invocada programáticamente.
Ubicaciones (Locations)
Para mejorar la información de errores, Bison ofrece la funcionalidad de ubicaciones, que permite rastrear la línea y el rango de columnas de cada símbolo en el parser. Las ubicaciones se habilitan explícitamente con %locations. El analizador léxico (Flex) debe rastrear la línea y columna actuales y establecer el rango de ubicación para cada token en una variable llamada yylloc antes de devolver el token. yylloc es de tipo YYLTYPE, que por defecto es una estructura con campos para la primera/última línea y columna.
Para actualizar yylloc, se puede usar la macro YY_USER_ACTION de Flex, que se invoca automáticamente para cada token reconocido por yylex(). Una implementación típica actualiza las líneas y columnas basándose en el contenido de yytext.

Reglas de Precedencia
En ocasiones, la gramática de un lenguaje puede llevar a ambigüedades o conflictos, especialmente con operadores. Las directivas de precedencia de Bison permiten mantener la gramática simple y resolver estos conflictos. Bison ofrece las directivas %left y %right para especificar la asociatividad de los símbolos. Por ejemplo, para la expresión 1 - 2 - 5, %left '-' indica que la operación debe ser (1 - 2) - 5 (asociatividad izquierda). La precedencia relativa de diferentes operadores se controla por el orden en que se declaran: %left '-' seguido de %left '*' significa que * tiene mayor precedencia que -.
Tipos de Datos de Valores Semánticos
Las reglas gramaticales solo definen la sintaxis; la semántica se determina por los valores asociados a los tokens. Por defecto, YYSTYPE, el tipo de esos valores, es int. Sin embargo, se puede especificar un tipo diferente (ej., #define YYSTYPE double). Para usar múltiples tipos de datos para los valores semánticos en un mismo parser, Bison requiere dos cosas:
- Especificar la colección completa de tipos de datos posibles, típicamente usando la declaración
%unionde Bison. - Incluir en la declaración de terminales y no terminales el tipo vinculado al símbolo. Por ejemplo, dentro de un
%union, se puede declarar%type <real_value> escalar_expro%token <real_value> NUM, vinculando esos símbolos a un valor flotante.
Flex y Bison: Los Herederos de Lex y Yacc
Es fundamental entender que Flex y Bison no son herramientas nuevas en el sentido de ser una invención radical, sino más bien los sucesores modernos y mejorados de un par de herramientas clásicas: Lex y Yacc (Yet Another Compiler Compiler). Yacc, en particular, fue desarrollado en Bell Labs como parte de los primeros esfuerzos para construir el sistema operativo Unix y el compilador original de C (pcc) por Steven C. Johnson en la década de 1970.
Aunque Lex y Yacc fueron revolucionarios en su momento y ayudaron a impulsar la expansión temprana de Unix, con el tiempo fueron superados en uso práctico. La razón principal de su declive fue la disponibilidad de herramientas similares con licencias menos restrictivas y, lo que es más importante, con más características y mejoras. Aquí es donde Flex y Bison se destacaron:
| Característica | Lex / Yacc (Originales) | Flex / Bison (Modernos) |
|---|---|---|
| Origen | Bell Labs (Unix, años 70) | Desarrollo más reciente (reemplazos GNU) |
| Licencia | Más restrictiva (propietaria en origen) | Menos restrictiva (licencias libres, como GPL) |
| Características | Funcionalidad básica | Más características, mejor manejo de errores, soporte para ubicaciones, etc. |
| Mantenimiento | Descontinuado/Obsoleto | Activamente mantenido y desarrollado |
| Interfaz | Variables globales (legado de lenguaje B) | Interfaz más limpia y moderna |
Bison, en particular, se originó como un "workalike" (una herramienta que funciona de manera similar) de Yacc, pero rápidamente lo superó en capacidades y flexibilidad. Hoy en día, si se va a construir un analizador léxico o sintáctico, Flex y Bison son las herramientas de elección preferidas debido a su robustez, las licencias permisivas y el soporte continuo de la comunidad.
Preguntas Frecuentes
¿Qué son Flex y Bison?
Flex y Bison son herramientas de desarrollo de software utilizadas para construir programas que procesan entradas estructuradas. Flex es un generador de analizadores léxicos (escáneres), que identifica y agrupa caracteres en unidades significativas (tokens). Bison es un generador de analizadores sintácticos (parsers), que verifica si una secuencia de estos tokens se ajusta a una gramática definida y realiza acciones semánticas.
¿Para qué se utilizan Flex y Bison?
Aunque originalmente se crearon para construir compiladores y traductores de lenguajes de programación, sus aplicaciones son mucho más amplias. Se usan para el análisis de archivos de configuración, validación de formatos de datos, desarrollo de lenguajes de scripting personalizados, creación de intérpretes, herramientas de procesamiento de texto complejas y cualquier tarea que implique la interpretación de una entrada con una estructura definida.
¿Son Yacc y Bison lo mismo?
No, Yacc y Bison no son lo mismo, aunque están estrechamente relacionados. Bison es un sucesor y una implementación mejorada de Yacc (Yet Another Compiler Compiler). Yacc fue la herramienta original desarrollada en Bell Labs, mientras que Bison (parte del proyecto GNU) es una reimplementación moderna que ofrece licencias más permisivas, más características y un desarrollo continuo, lo que lo ha convertido en el estándar actual.
Conclusión
Flex y Bison representan un dúo indispensable para cualquier desarrollador que necesite procesar y entender entradas estructuradas de manera eficiente y robusta. Su capacidad para automatizar la creación de analizadores léxicos y sintácticos, la flexibilidad que ofrecen en la definición de gramáticas y el manejo de errores, y su herencia como sucesores mejorados de herramientas clásicas, los convierten en pilares fundamentales en el desarrollo de compiladores, intérpretes y una vasta gama de aplicaciones de análisis de datos. Dominar estas herramientas no solo te abrirá las puertas al fascinante mundo del procesamiento de lenguajes, sino que también te proporcionará una base sólida para abordar problemas complejos de análisis de entrada en cualquier ámbito de la programación.
Si quieres conocer otros artículos parecidos a Dominando Flex y Bison: Herramientas Clave para el Análisis de Lenguajes puedes visitar la categoría Cálculos.
