Clase 4 — Análisis Léxico con JFlex: Especificación, Expresiones Regulares y Generación del Analizador
Resumen Ejecutivo
La sesión se centró en el uso práctico de JFlex como generador de analizadores léxicos para Java, repasando la estructura del archivo de especificación .flex (código de usuario, opciones/declaraciones y reglas), las expresiones regulares y metacaracteres necesarios para definir patrones léxicos, y el comportamiento del analizador (algoritmo ambicioso, orden de reglas). Se realizó una demostración en Eclipse creando un analizador léxico sencillo para expresiones aritméticas y se introdujo la actividad práctica: construir el analizador léxico del lenguaje Pocket.
Conceptos Clave
- JFlex: Generador de analizadores léxicos para Java. Toma un archivo
.flexcon especificaciones y produce un archivo.javaque implementa el autómata finito determinista. ⚠️ EXAMEN - Especificación
.flex: Archivo dividido en tres secciones separadas por%%: código de usuario, opciones/declaraciones, y reglas. ⚠️ EXAMEN - Macros (definiciones): Nombres simbólicos que sustituyen expresiones regulares complejas para mejorar la legibilidad. Se definen en la sección de opciones y se usan entre llaves
{nombre}en las reglas. ⚠️ EXAMEN - Metacaracteres: Caracteres con significado especial en expresiones regulares (
[],*,+,?,.,|,(),^). Para usarlos como literales se entrecomillan o se escapan con\. - Token: Unidad léxica reconocida por el analizador. Se retorna como resultado del reconocimiento de un patrón.
- Algoritmo ambicioso (greedy): El analizador siempre intenta reconocer el lexema más largo posible. ⚠️ EXAMEN
yylex(): Método generado que ejecuta el reconocimiento léxico, lee carácter a carácter y retorna tokens.yytext(): Devuelve la cadena de caracteres (lexema) que acaba de ser reconocida.yyline/yycolumn: Contadores de línea y columna, activados con directivas%liney%column.
Desarrollo del Temario
1. Estructura del archivo de especificación JFlex
El archivo .flex se divide en tres secciones separadas por %%:
| Sección | Contenido |
|---|---|
| Código de usuario | Sentencias package, import, código Java que se copia al inicio del .java generado |
| Opciones y declaraciones | Directivas (%class, %line, %column), código insertado entre %{ y %}, macros |
| Reglas léxicas | Expresiones regulares seguidas de acciones Java entre llaves |
La sección de reglas es imprescindible; las otras dos son opcionales según las necesidades del diseño. ⚠️ EXAMEN
Ejemplo: Si no necesitamos paquetes ni imports, la sección de código de usuario puede quedar vacía. Pero si queremos organizar en paquetes Java, escribimos
package micodigo;en esa primera sección.
2. Directivas principales de JFlex
%class NombreClase: Define el nombre de la clase Java generada (por defecto se usa el nombre del archivo).%public: Hace la clase pública.%line: Activa el contador de líneas (yyline), de tipo entero.%column: Activa el contador de columnas (yycolumn).%type: Define el tipo de retorno deyylex().%{ ... %}: Bloque para insertar código Java dentro de la clase (atributos, métodos,main).
Ejemplo de directivas en el
.flex:%class Lexer %public %line %column %{ public static void main(String[] args) throws Exception { Lexer lexer = new Lexer(new java.io.InputStreamReader(System.in)); while (true) { YyToken t = lexer.yylex(); System.out.println(t); } } %}
3. Expresiones regulares y metacaracteres
Corchetes [ ] — Definen clases de caracteres (opcionalidad):
- [xyz] → reconoce x, y o z (uno solo)
- [a-z] → cualquier minúscula
- [0-9a-zA-Z] → un dígito, una minúscula o una mayúscula
- [^A-Z] → cualquier carácter excepto mayúsculas (negación con ^) ⚠️ EXAMEN
Cuantificadores:
- * (asterisco) → cero o más repeticiones. ab* reconoce: a, ab, abb, abbb...
- + (más) → una o más repeticiones. [0-9]+ reconoce al menos un dígito. ⚠️ EXAMEN
- ? (interrogación) → cero o una ocurrencia (opcional). "-"?[0-9]+ reconoce enteros positivos y negativos.
- {n} → exactamente \(n\) repeticiones. a{4} → aaaa
- {n,} → \(n\) o más repeticiones.
- {m,n} → entre \(m\) y \(n\) repeticiones.
Otros metacaracteres:
- . (punto) → cualquier carácter excepto salto de línea. ⚠️ EXAMEN
- | (barra/tubería) → alternativa: a|c reconoce a o c.
- ( ) (paréntesis) → agrupación: (ab|cd)+r reconoce combinaciones de ab y cd seguidas de r.
- " " (comillas) → literalización: "[" reconoce el carácter corchete, no el metacarácter.
- \ (barra invertida) → escape: \* reconoce el carácter asterisco literal.
Ejemplo de expresión para identificadores:
[a-zA-Z][a-zA-Z0-9]*→ empieza por letra, seguida de cero o más letras o dígitos. Reconoce:indice,m0,contadorDelTorno.Ejemplo de enteros con signo:
"-"?[0-9]+→ el guión es opcional (entre comillas para literalizarlo), seguido de uno o más dígitos. Reconoce:42,-7,100.
La ñ NO está incluida en los rangos [a-z] ni [A-Z]; los alfabetos de lenguajes de programación no la contemplan.
4. Macros (definiciones)
Las macros se definen en la sección de opciones con el formato NOMBRE = expresión_regular y se referencian en las reglas con {NOMBRE}. ⚠️ EXAMEN
Ejemplo:
La expresiónDIGITO = [0-9] LETRA = [a-zA-Z] ID = {LETRA}({LETRA}|{DIGITO})* INUM = {DIGITO}+{LETRA}({DIGITO}|{LETRA})*es mucho más legible que[a-zA-Z][a-zA-Z0-9]*.
Se pueden definir macros usando otras macros (composición), lo que simplifica progresivamente las expresiones.
5. Sección de reglas y acciones semánticas
Cada regla tiene la forma: expresión_regular { código_Java }.
Orden de las reglas — criterios fundamentales: ⚠️ EXAMEN
- Primero: blancos y separadores (espacios, tabuladores, saltos de línea) → se descartan sin retornar token. Se ponen primero por eficiencia, ya que son los caracteres más frecuentes.
- Segundo: operadores simples (
+,-,=,;) → reconocimiento inmediato, sin retroceso. - Tercero: palabras reservadas (
while,if,class...) → siempre antes que la regla de identificadores. - Cuarto: números e identificadores.
- Último: el punto
.→ captura cualquier carácter no reconocido previamente y genera un token de error.
Ejemplo de reglas:
[ \t\r\n]+ { /* ignorar espacios */ } "+" { return new YyToken("MAS", yytext(), yyline, yycolumn); } "-" { return new YyToken("MENOS", yytext(), yyline, yycolumn); } "=" { return new YyToken("IGUAL", yytext(), yyline, yycolumn); } ";" { return new YyToken("PYC", yytext(), yyline, yycolumn); } {INUM} { return new YyToken("NUM", yytext(), yyline, yycolumn); } {ID} { return new YyToken("ID", yytext(), yyline, yycolumn); } . { return new YyToken("ERROR", yytext(), yyline, yycolumn); }
6. Algoritmo ambicioso (Greedy / Maximal Munch)
El analizador léxico siempre intenta reconocer el lexema más largo posible: ⚠️ EXAMEN
- Lee carácter a carácter hasta que no puede continuar por ningún autómata.
- Si un carácter no encaja, retrocede una posición y devuelve el lexema reconocido hasta ese punto.
- Los blancos y operadores actúan como separadores naturales.
Ejemplo: Si existe la variable
privatepublic(todo junto), el analizador no se detiene enprivateporque tras laehay unapy puede seguir leyendo → reconoceprivatepubliccomo identificador completo.Ejemplo:
>=→ si existen reglas para>y>=, el analizador lee>, ve que el siguiente carácter es=, comprueba que>=es un lexema válido más largo, y retorna>=.
Concordancia con múltiples reglas del mismo tamaño: si dos expresiones regulares reconocen la misma cadena con la misma longitud, se elige la que aparece primero en el archivo .flex. ⚠️ EXAMEN
Ejemplo:
whileencaja tanto con la regla"while"como con{ID}. Si"while"está antes → devuelve palabra reservada. Si{ID}está antes → devuelve identificador (incorrecto). JFlex avisa de reglas inalcanzables si detecta este problema.
7. Tipos de tokens a reconocer en un lenguaje
| Categoría | Ejemplos | Observaciones |
|---|---|---|
| Palabras reservadas | class, main, while, if |
Un token por cada una; se ponen primero en las reglas |
| Identificadores | nombres de variables, funciones | Regla genérica; se pone después de las palabras reservadas |
| Números | enteros, reales | Se puede usar un único token NUM para simplificar la gramática; la distinción entero/real se delega al análisis semántico ⚠️ EXAMEN |
| Operadores aritméticos | +, -, *, / |
Un token distinto para cada uno |
| Operador de asignación | =, := |
Token único |
| Separadores/delimitadores | ;, ,, (, ), [, ] |
Token por cada uno |
| Comentarios | // ... |
Se ignoran (acción vacía) |
| Espacios en blanco | espacio, tab, salto de línea | Se ignoran |
| Errores | cualquier carácter no válido | Regla con . al final |
Sobre los números y la gramática: sintácticamente da igual si un número es entero o real; la gramática solo necesita saber que hay un "número". La verificación de tipos corresponde al análisis semántico (analogía: "el casa" es sintácticamente correcto pero semánticamente incorrecto). ⚠️ EXAMEN
8. La clase YyToken
Clase Java que representa el componente léxico retornado por yylex():
public class YyToken {
String tipo; // nombre del token ("MAS", "ID", "NUM"...)
String lexema; // cadena leída (yytext())
int linea; // yyline
int columna; // yycolumn
public YyToken(String tipo, String lexema, int linea, int columna) { ... }
@Override
public String toString() { ... } // permite System.out.println(token)
}
Se puede usar un enumerado en lugar de String para el tipo de token, lo que proporciona una representación numérica interna normalizada y evita errores por typos.
9. Flujo de trabajo práctico con Eclipse y JFlex
- Crear proyecto Java en Eclipse (Java SE 17, desmarcar
Create module-info.java). - Añadir el JAR de JFlex (
jflex-full-1.9.1.jar) al Build Path (classpath). - Crear carpeta
spec/para archivos.flexy mantenersrc/para.java. - Escribir el archivo
lexer.flex. - Generar el
.javadesde terminal:java -jar lib/jflex-full-1.9.1.jar spec/lexer.flex - Mover el
.javagenerado asrc/. - Ejecutar como Java Application.
JFlex informa durante la generación: construye un AFN (26 estados en el ejemplo), lo convierte a AFD (13 estados) y lo minimiza (9 estados).
10. Actividad práctica: Analizador léxico de Pocket
Elementos a identificar a partir de la gramática de Pocket:
- Palabras reservadas: main, in, var, function, malloc, if, else, while, return, print, read...
- Operadores y símbolos: =, +, -, *, /, (, ), [, ], ,, ;, & (ampersand)
- Identificadores: longitud máxima de 100 caracteres (usar {1,100} o equivalente)
- Números: enteros
- Comentarios: de una línea, delimitados por // hasta el final de línea → se ignoran
- Errores léxicos: símbolos no permitidos (capturados por .) e identificadores que excedan la longitud máxima
Preguntas de Autoevaluación
-
¿Cuáles son las tres secciones de un archivo de especificación JFlex y qué contiene cada una?
-
Explica qué es el algoritmo ambicioso (greedy) del analizador léxico y por qué es fundamental que las palabras reservadas aparezcan antes que la regla de identificadores en la sección de reglas.
-
Dada la expresión regular
"-"?[0-9]{1,2}, ¿qué cadenas reconoce y cuáles no? Justifica tu respuesta. -
¿Por qué se recomienda poner la regla de espacios en blanco al principio de la sección de reglas y la regla del punto (
.) al final? -
¿Por qué es preferible usar un único token
NUMpara todos los tipos numéricos en lugar de tokens separados para enteros y reales? ¿Qué fase del compilador se encarga de verificar la compatibilidad de tipos? -
¿Qué diferencia hay entre
[abc]yabcen una expresión regular de JFlex? -
Define las macros JFlex necesarias para reconocer identificadores que comiencen por letra y contengan letras, dígitos y guiones bajos, con un máximo de 100 caracteres.
-
¿Qué significa que JFlex informe de "reglas inalcanzables" durante la generación del código Java? ¿Cómo se soluciona?
Guía generada automáticamente a partir de transcripción con faster-whisper + Claude Sonnet.