09:19 1
2 Temas Funciones del Analizador Léxico Manejo de buffers de entrada Especificación y reconocimiento de tokens Generación automática de Analizadores Léxicos Objetivos Que el estudiante logre conocer, comprender y manejar conceptos y técnicas vinculados con el Analizador Léxico, para lo cual debe: Conocer las funciones del analizador léxico. Aprender a especificar formalmente analizadores léxicos. Conocer las distintas estrategias de implementación de analizadores léxicos.
3 Programa fuente Administrador de la Tabla de Símbolos Analizador Léxico Flujo de tokens o componentes léxicos Analizador Sintáctico Árbol sintáctico Analizador Semántico Árbol sintáctico Generador de Código Intermedio Representación intermedia Optimizador Representación intermedia Generador de Código Programa Objeto Manejador de errores
Función Principal Leer carácter por carácter de la entrada y elaborar como salida una secuencia de componentes léxicos (tokens) que utiliza el analizador sintáctico para hacer el análisis. Funciones secundarias Manejar el archivo fuente, es decir, abrirlo, leer sus caracteres y cerrarlo. Eliminar comentarios y espacios en blanco (espacios, tabuladores y fin de línea). Relacionar los mensajes de error con las líneas del programa fuente. Introducir los identificadores en la tabla de símbolos. 4
5 Razones para dividir en dos fases Diseño más sencillo: los símbolos que trata el analizador léxico se describen con una gramática más simple (gramática regular) que la del analizador sintáctico (gramática libre de contexto). Mejora la eficiencia: gran parte del tiempo de compilación se consume en la lectura y exploración de caracteres. Con técnicas especializadas de manejo de buffers se puede mejorar significativamente el rendimiento del compilador. Mejora la portabilidad: se pueden tener varias versiones del analizador léxico una para distintos códigos (EBCDID, ASCII,...), con el mismo analizador sintáctico.
6 Token (componente léxico): es un par que consiste en un nombre de token y un valor de atributo opcional. Componentes léxicos comunes en los lenguajes de programación Identificadores, Palabras claves, Operadores, Constantes, Cadena de literales, Signos de puntuación (paréntesis, coma, punto y coma, etc.) Patrón: el conjunto de cadenas de la entrada se describe mediante una regla llamada patrón asociada al componente léxico. Un patrón es una regla que describe el conjunto de lexemas. Para describir los patrones se utiliza la notación de expresiones regulares. Lexema: secuencia de caracteres en el programa fuente con lo que concuerda el patrón para un token. Los lexemas para el token que concuerdan con el patrón representan cadenas de caracteres en el programa fuente que se pueden tratar como unidad léxica.
7 Ejemplo: en la proposición de PASCAL Const pi = 3.1416; Componente Léxico Lexema Patrón Const Const Const id pi letra (letra/dígito)* Op-asig = = const 3.1416 dig + (.dig + )? (E(+/-)?dig + )? sig-punt ; ;
Cuando una secuencia de caracteres (por ejemplo, pi) aparece en el programa fuente se devuelve al analizador sintáctico un token que representa un identificador. Pero es necesario que el analizador léxico proporcione información adicional sobre el lexema concreto. Esta información adicional se denomina Atributos. Por ejemplo, el patrón num concuerda con las cadenas 3.1416 y 0, pero es indispensable que el generador de código conozca que cadena fue realmente la que se emparejo. El analizador léxico recoge información sobre los tokens en sus atributos asociados. El nombre de un token influye en las decisiones del analizador sintáctico, y el valor del atributo, en la traducción de los tokens después del analizador sintáctico. En la práctica los tokens suelen tener un solo atributo, un puntero a la tabla de símbolos, donde se guarda información del token. El puntero se convierte en el atributo. No siempre se necesita un valor de atributo, el nombre del token es suficiente para identificar al lexema. 8
El Analizador Léxico consumen mucho tiempo para realizar su tarea, por lo que se introducen buffers para mejorar su rendimiento. Funcionamiento El buffer se divide en dos mitades de tamaño N (N= un bloque del disco: 1024 o 4096). E = M * C * * 2 eof comienzo_lexema delentero Se leen N caracteres de entrada en cada mitad del buffer con una orden de lectura del sistema. El carácter eof marca el final del archivo fuente. Dos punteros: comienzo_lexema y delantero. Al principio los dos punteros apuntan al primer carácter del próximo lexema que hay que encontrar. El puntero delantero va avanzando hasta encontrar concordancia con patrón (un componente léxico). Una vez determinado el siguiente lexema, el puntero Delantero se coloca en su carácter derecho. Comienzo_lexema y delantero delimitan el lexema. Después de procesar el lexema (devolver componente léxico) se ubican comienzo_lexema en delantero (primer carácter del siguiente) Cuando termina una mitad, recarga la otra y continua hasta encontrar el siguiente componente léxico. 9
10 Algoritmo para avanzar puntero delantero: if delantero está al final de la primera mitad then begin recargar la segunda mitad; delantero: = delantero + 1 end else if delantero está al final de la segunda mitad then begin recargar la primera mitad; pasar delantero al principio de la primera mitad end else delantero := delantero + 1;
11 Si se utiliza el esquema anterior cada vez que se mueva el puntero delantero se debe comprobar si se ha salido de una mitad del buffer, si así ocurriera se deberá recargar la otra mitad. Excepto en los extremos de las mitades del buffer, el código anterior necesita dos pruebas para cada avance del puntero delantero. Se puede reducir estas dos pruebas a una si se amplía cada mitad del buffer para admitir un carácter centinela al final. Un centinela es un carácter especial (eof) que no existe en el programa fuente y se ubica al final de cada mitad. E = M * eof C * * 2 eof comienzo_lexema delentero
12 Algoritmo de recarga de buffers con centinelas: delantero: = delantero + 1: if delantero = eof then begin if delantero está al final de la primera mitad then begin recargar la segunda mitad; delantero: = delantero + 1 end else if delantero está al final de la segunda mitad then begin recargar la primera mitad; pasar delantero al principio de la primera mitad end else /* eof dentro de un buffer significa el final de la entrada */ terminar el análisis léxico end
13 Dos cuestiones: Cómo especificar los tokens? Cómo reconocer los tokens? Especificación Todos los elementos básicos en un lenguaje deben ser tokens, por lo tanto deben reconocerse. Los tokens se especifican con Expresiones Regulares.
Ejemplo: considere el siguiente fragmento gramatical prop if expr then prop / if expr then prop else prop / expr expr oprel termino / termino termino id / num Expresión Regular Componente léxico Atributo if if ----- then then ----- else else ----- letra (letra/dig)* id Puntero a la T.S. dig + (.dig + )? (E(+/-)?dig + )? num Puntero a la T.S. < oprel MEN <= oprel MEI > oprel MAY >= oprel MAI = oprel IGU <> oprel DIF delim + ---- ---- delim = blanco/tab/nueva línea 14
15 Como paso intermedio en la construcción de un Analizador Léxico se produce un diagrama de transición. Estos diagramas representan acciones que el analizador léxico debe llevar a cabo para obtener el siguiente token. Acciones: Getchar: leer el siguiente carácter de entrada. Devuelve (nombre-token, atributo): luego de reconocer un token, el A.L. devuelve al Analizador Sintáctico el token y el atributo. *: con asterisco se indica que se debe retroceder el puntero de entrada un símbolo a la izquierda.
16 ER= < / <= / > / >= / = / <> inicio 0 < 1 = 2 Devuelve (oprel,mei) > 3 Devuelve (oprel,dif) > otro 4 * Devuelve (oprel,men) 5 = 6 Devuelve (oprel,mai) = 8 otro Devuelve (oprel,igu) 7 * Devuelve (oprel,may)
17 ER= letra (letra/dig)* letra/dig inicio 0 letra 9 otro 10 Devuelve (obten-token, instala-id) Como las palabras claves son secuencias de letras, en vez de realizar diagramas de transición para cada palabra clave, se las considera como identificadores. Para ello, se almacenan, al comienzo, en la Tabla de Símbolos todas las palabras claves. Cuando se las reconoce se convoca a dos rutinas especiales: 1- obten-token: busca el lexema en la Tabla de Símbolos. Si el lexema es una palabra clave devuelve el correspondiente nombre de token, sino, devuelve id. 2- instala-id: examina la Tabla de Símbolos y si encuentra el lexema marcado como una palabra clave devuelve 0. Si se encuentra el lexema y es una variable del programa se devuelve un puntero a la Tabla de Símbolos. Si el lexema no se encuentra en la Tabla de Símbolos se instala como una variable y se devuelve un puntero a la entrada recientemente creada.
ER=dig + (.dig + )? (E(+/-)?dig + )? dig inicio 0 dig 11 otro. 12 * Devuelve (num, instala-num) dig dig otro 13 14 15 * Devuelve (num, instala-num) E E dig 16 +/- 17 dig dig otro 18 19 * Devuelve (num, instalanum) instala-num: cuando llega a cualquier estado de aceptación devuelve el nombre del token nun y llama a la rutina instala-id. Esta rutina introduce el lexema en una tabla de números y devuelve un puntero a la entrada creada. 18
19 delim= blanco/tab/nueva línea eb = delim + delim inicio 0 delim 20 otro 21 * Cuando el Analizador Léxico llega al estado de aceptación 21 no devuelve un nombre de token ni un atributo al Analizador Sintáctico.
20 1-Tabla de Transiciones: el Analizador Léxico recorre la tabla con un bucle ejecutando la sentencia: Estado := TablaTransiciones [ Estado, Entrada ]; s TablaTransición
21 De acuerdo a esta estrategia de implementación es necesario disponer de una variable de estado para codificar el estado actual y una batería de casos que describen la lógica de transición del autómata finito determínista. letra/dig 0 letra 1 otro 2
22 Hay pocos errores que el analizador léxico puede detectar ya que tiene una visión muy restringida de un programa fuente. Los errores que se detectan en esta fase ocurren cuando el Analizador Léxico no puede proceder, ya que ningún patrón para los tokens coincide con algún prefijo del resto de la entrada. Estrategias de recuperación Modo de pánico. Eliminar un carácter del resto de la entrada. Sustituir un carácter por otro. Transponer dos caracteres adyacentes.
23 Existen muchas herramientas para la generación automática de AL. Las más conocidas: Lex Flex Lex: Generador de analizadores léxicos a partir de expresiones regulares que definen el comportamiento del analizador. Genera código en distintos lenguajes de programación, como C. Esquema de funcionamiento de LEX: Especificaciones léxicas LEX yylex
24 Esquema de un archivo LEX: (zona de declaraciones) [código C o macros] %% (zona de reglas y acciones) %% (zona de rutinas de usuario) Zona de declaraciones: declaraciones necesarias como: declaración de variables, funciones, definiciones regulares. Ejemplo: Zona de declaraciones %{ #include <stdio.h> int y; %} digito (0 1 2 3 4 5 6 7 8 9) letraab (a b)
Zona de reglas y acciones P 1 {acción 1 } P 2 {acción 2 }... P n {acción n } Donde p i es una expresión regular y cada acción es un fragmento del programa que describe cual será la acción del Analizador Léxico cuando el patrón p i concuerde con un lexema. Ejemplo %{ #include <stdio.h> int cuenta=0; %} digito (0 1 2 3 4 5 6 7 8 9) %% {digito} {++cuenta} %% Zona de rutinas de usuario: contiene las rutinas auxiliares que pueden necesitar las acciones. 25
26 Gramáticas Regulares G = (V N, V T, P, S) while (a > b) do a:= a + 1; Lenguajes Regulares Autómatas Finitos Expresiones Regulares Obtén siguiente token Analizador Léxico token Leer carácter por carácter de la entrada y elaborar como salida una secuencia de tokens que utiliza el analizador sintáctico para hacer el análisis. A =(Q,,, q 0, F) Se puede definir un lenguaje comenzando con un número finito de palabras y aplicando operaciones regulares sobre ellas. Este método se conoce como expresiones regulares. Herramientas para la construcción automática de analizadores léxicos como [Lex] [Flex] [JFlex]. Se basan en la definición de una colección de reglas patrón-acción.
27 Alfred Aho, Mónica Lam, Ravi Sethi, Jeffrey Ullman. Compiladores. Principios, técnicas y herramientas. Pearson. Addison Wesley. 2 da. Edición (2008). Kenneth C. Louden. Construcción de compiladores: principios y práctica. International Thomson Editores (2004).