62
Índice 1. Introducción ................................................................................................................................ 7 1.1 Tipos de traductores ......................................................................................................... 7 1.2 Autómatas ........................................................................................................................... 10 1.2.1 Autómatas finitos (FA finite automata) .................................................................... 10 1.2.1.1 Autómatas finitos deterministas (DFA deterministic finite automata) .............. 11 1.2.1.2 Autómatas finitos no deterministas (NFA nondeterministic finite automata) ... 12 1.2.2 Autómata de Pila (PDA push-down automaton) ...................................................... 13 1.2.2.1 Autómatas de pila ................................................................................................. 13 1.3 Gramáticas formales ........................................................................................................... 14 1.3.1 Gramática Regular ....................................................................................................... 15 1.3.2 Gramática libre de contexto (CFG Context Free Grammar) .................................... 16 1.4 Fases de un compilador ....................................................................................................... 16 2. Análisis Léxico ......................................................................................................................... 21 2.1 Definición de un reconocedor de cadenas no trivial ........................................................... 22 2.1.1 Las operaciones regulares ............................................................................................ 23 2.1.2 Definición formal de una expresión regular ................................................................ 23 2.2 Programar sistemáticamente el reconocedor en lo referente a la obtención del autómata, almacenarlo eficientemente y manejar adecuadamente el archivo fuente ................................ 24 2.2.1 Conversión de una expresión regular a un autómata finito no determinista (NFA) .... 24 2.2.2 Conversión de un autómata finito no determinista (NFA) a su correspondiente autómata finito determinista (DFA) ...................................................................................... 34 2.2.3 Codificación de un DFA en pseudocódigo .................................................................. 41 3. Análisis sintáctico ..................................................................................................................... 45 3.1 Construcción de tablas parse LR(1) .................................................................................... 52 Algoritmo para construir el FA que servirá de base para la tabla parse LR(1) ........................ 52 3.2 Análisis sintáctico LALR(1) ............................................................................................... 56 3.2.1 Primer principio del análisis sintáctico LALR(1) ........................................................ 56 3.2.2 Segundo principio del análisis sintáctico LALR(1) ..................................................... 56 3.3 Análisis sintáctico LR(1) canónico ..................................................................................... 57 3.3.1 Autómatas finitos de elementos LR(1) ........................................................................ 57 3.3.3 Definición de transiciones LR(1) (parte 1) .................................................................. 58 3.4 Conjuntos primero .............................................................................................................. 59 4. Análisis Léxico ......................................................................................................................... 61 4.1 Planteamiento del problema................................................................................................ 61 4.2 Solución .............................................................................. ¡Error! Marcador no definido. 4.2.1 Análisis ........................................................................ ¡Error! Marcador no definido. 4.2.2 Diseño .......................................................................... ¡Error! Marcador no definido. 4.2.2.1 Expresiones regulares y NFA's ............................. ¡Error! Marcador no definido. 4.2.2.2 DFA....................................................................... ¡Error! Marcador no definido. 4.3 Implementación................................................................... ¡Error! Marcador no definido. 4.3.1 main.cpp (primera parte) .............................................. ¡Error! Marcador no definido.

Libro alumnos

Embed Size (px)

Citation preview

Page 1: Libro alumnos

Índice

1. Introducción ................................................................................................................................ 7

1.1 Tipos de traductores ......................................................................................................... 7 1.2 Autómatas ........................................................................................................................... 10

1.2.1 Autómatas finitos (FA – finite automata) .................................................................... 10 1.2.1.1 Autómatas finitos deterministas (DFA – deterministic finite automata) .............. 11

1.2.1.2 Autómatas finitos no deterministas (NFA – nondeterministic finite automata) ... 12

1.2.2 Autómata de Pila (PDA – push-down automaton) ...................................................... 13 1.2.2.1 Autómatas de pila ................................................................................................. 13

1.3 Gramáticas formales ........................................................................................................... 14

1.3.1 Gramática Regular ....................................................................................................... 15 1.3.2 Gramática libre de contexto (CFG – Context Free Grammar) .................................... 16

1.4 Fases de un compilador ....................................................................................................... 16

2. Análisis Léxico ......................................................................................................................... 21 2.1 Definición de un reconocedor de cadenas no trivial ........................................................... 22

2.1.1 Las operaciones regulares ............................................................................................ 23

2.1.2 Definición formal de una expresión regular ................................................................ 23 2.2 Programar sistemáticamente el reconocedor en lo referente a la obtención del autómata,

almacenarlo eficientemente y manejar adecuadamente el archivo fuente ................................ 24 2.2.1 Conversión de una expresión regular a un autómata finito no determinista (NFA) .... 24 2.2.2 Conversión de un autómata finito no determinista (NFA) a su correspondiente

autómata finito determinista (DFA) ...................................................................................... 34

2.2.3 Codificación de un DFA en pseudocódigo .................................................................. 41 3. Análisis sintáctico ..................................................................................................................... 45

3.1 Construcción de tablas parse LR(1) .................................................................................... 52

Algoritmo para construir el FA que servirá de base para la tabla parse LR(1) ........................ 52 3.2 Análisis sintáctico LALR(1) ............................................................................................... 56

3.2.1 Primer principio del análisis sintáctico LALR(1) ........................................................ 56 3.2.2 Segundo principio del análisis sintáctico LALR(1) ..................................................... 56

3.3 Análisis sintáctico LR(1) canónico ..................................................................................... 57 3.3.1 Autómatas finitos de elementos LR(1) ........................................................................ 57 3.3.3 Definición de transiciones LR(1) (parte 1) .................................................................. 58

3.4 Conjuntos primero .............................................................................................................. 59

4. Análisis Léxico ......................................................................................................................... 61

4.1 Planteamiento del problema ................................................................................................ 61 4.2 Solución .............................................................................. ¡Error! Marcador no definido.

4.2.1 Análisis ........................................................................ ¡Error! Marcador no definido. 4.2.2 Diseño .......................................................................... ¡Error! Marcador no definido.

4.2.2.1 Expresiones regulares y NFA's ............................. ¡Error! Marcador no definido.

4.2.2.2 DFA....................................................................... ¡Error! Marcador no definido.

4.3 Implementación................................................................... ¡Error! Marcador no definido. 4.3.1 main.cpp (primera parte) .............................................. ¡Error! Marcador no definido.

Page 2: Libro alumnos

4.3.2 Código referente al análisis léxico (compilador.cpp primera parte) . ¡Error! Marcador

no definido. 4.3.3 Implementación alternativa en Flex ............................. ¡Error! Marcador no definido.

5. Análisis sintáctico ..................................................................................................................... 62 5.1 Planteamiento del problema ................................................................................................ 62 5.2 Solución .............................................................................. ¡Error! Marcador no definido.

5.2.1 Análisis ........................................................................ ¡Error! Marcador no definido. 5.2.2 Diseño .......................................................................... ¡Error! Marcador no definido.

5.3 Implementación................................................................... ¡Error! Marcador no definido. 5.3.1 main.cpp ....................................................................... ¡Error! Marcador no definido. 5.3.2 compilador.ui ............................................................... ¡Error! Marcador no definido. 5.3.3 compilador.h ................................................................ ¡Error! Marcador no definido. 5.3.4 Código referente al análisis sintáctico (compilador.cpp parte 2) . ¡Error! Marcador no

definido. 5.3.5 Implementación alternativa Bison ............................... ¡Error! Marcador no definido.

Page 3: Libro alumnos

Índice de figuras

Fig. 1: Proceso de interpretación .................................................................................................... 9

Fig. 2: Un compilador ..................................................................................................................... 9 Fig. 3: Árbol sintáctico para............................................................................................................ 9

Fig. 4: Traductor híbrido para .................................. 10 Fig. 5: DFA que reconoce cadenas que contienen ........................................................................ 11

Fig. 6: NFA que reconoce a la cadena vacía o cadenas que tienen .............................................. 12

Fig. 7: PDA que reconoce lenguajes del tipo ..................................................................... 14 Fig. 8: Ejemplo de reglas gramaticales ......................................................................................... 15 Fig. 9: Ejemplo de reglas permitidas en una gramática regular (izquierda) y de reglas no

permitidas en una gramática regular (derecha) ............................................................................. 15 Fig. 10: Ejemplo de una CFG ....................................................................................................... 16

Fig. 11: Fases de un compilador ................................................................................................... 17

Fig. 12: Árbol sintáctico de la expresión

a [ index ] 4 2 ........................................................ 18

Fig. 13: Árbol semántico (corregir este árbol) de la expresión

a [ index ] 4 2 ...................... 19

Fig. 14: Optimizador de código fuente ......................................................................................... 19 Fig. 15: código objeto en ensamblador generado a partir de la representación intermedia de la

Fig. 14 ........................................................................................................................................... 20 Fig. 16: Código objeto optimizado ............................................................................................... 20 Fig. 17: Un pequeño ejemplo de un programa fuente ................................................................... 21

Fig. 18: un pequeño ejemplo de un programa fuente con un error léxico .................................... 21 Fig. 19: un pequeño ejemplo de un programa fuente sin error léxico .......................................... 22

Fig. 20: Un NFA que reconoce a la cadena vacía o cadenas que tienen cualquier número de a´s25

Fig. 21: Construcción de un NFA para reconocer 21 AA .......................................................... 27

Fig. 22: Construcción de M para reconocer 21 AA .................................................................... 28

Fig. 23: Construimos M para que reconozca *A ......................................................................... 29 Fig. 24: Autómata que reconoce a z . ........................................................................................... 30

Fig. 25: Autómata que reconoce a y ............................................................................................ 30

Fig. 26: Autómata que reconoce a x ............................................................................................ 30

Fig. 27: Autómata que reconoce yz ........................................................................................ 30

Fig. 28: Autómata que reconoce *)( yz .................................................................................... 31

Fig. 29: Autómata que reconoce .................................................................................. 32 Fig. 30: Ejemplo de un NFA ......................................................................................................... 35

Fig. 31: Estado inicial del DFA 1q ............................................................................................... 37

Fig. 32: Segundo estado del DFA ................................................................................................. 37 Fig. 33: Siguiente estado del DFA ................................................................................................ 37

Fig. 34: NFA correspondiente a xyz *)( .................................................................................. 38

Fig. 35: Estado inicial del DFA .................................................................................................... 38

Fig. 36: Nuevo estado del DFA generado por la transición x del NFA en 1q ............................ 38

Fig. 37: Estado 3 del DFA ............................................................................................................ 39 Fig. 38: Estado 4 del DFA ............................................................................................................ 39 Fig. 39: Agregación de estado de ERROR ................................................................................... 39 Fig. 40: Transición del estado 3 al estado 2 .................................................................................. 40

Page 4: Libro alumnos

Fig. 41: Transición del estado 3 a él mismo ................................................................................. 40

Fig. 42: Transición del estado 3 al estado 4 .................................................................................. 40 Fig. 43: Transición del estado 4 añ estado 2 ................................................................................. 41

Fig. 44: Algún título ...................................................................................................................... 41 Fig. 45: Transición del estado 4 a él mismo ................................................................................. 41 Fig. 46: DFA representando la sintaxis de un nombre de variable (identificador) ....................... 42

Fig. 47: PDA que reconoce lenguajes del tipo ................................................................... 45 Fig. 48: Estados del PDA .............................................................................................................. 46

Fig. 49: Introducción de la primera transición )#,;,,( 0 pq ................................................... 47

Fig. 50: Introducción de la segunda transición ),;,,( Sqp .................................................... 47

Fig. 51: Introducir transiciones por cada regla de producción ...................................................... 47

Fig. 52: Introducir una transición por cada símbolo terminal ....................................................... 47

Fig. 53: PDA para la gramática dada. ........................................................................................... 48

Fig. 54: Gramática ........................................................................................................................ 48 Fig. 55: PDA del Ejercicio 1 ......................................................................................................... 49 Fig. 56: PDA del ejercicio 2 ......................................................................................................... 49 Fig. 57: PDA del ejercicio 3 ......................................................................................................... 49

Fig. 58: Establecimiento de 4 estados ........................................................................................... 50 Fig. 59: Primeras dos transiciones ................................................................................................ 50

Fig. 60: Una transición por cada símbolo terminal ....................................................................... 51 Fig. 61: Una transición por cada regla gramatical ........................................................................ 51 Fig. 62: Última transición ............................................................................................................. 51

Fig. 63: Gramática ........................................................................................................................ 52

Fig. 64: Estado inicial del FA cerradura de SS ' ..................................................................... 53

Fig. 65: Segundo estado ................................................................................................................ 53

Fig. 66: Tercer estado ................................................................................................................... 54 Fig. 67: Estado 4 del AF ............................................................................................................... 54

Fig. 68: Estado 5 del AF ............................................................................................................... 54 Fig. 69: Transición x ..................................................................................................................... 55

Fig. 70: Transición del estado 3 al estado 4 con ........................................................................ 55 Fig. 71: Ultimo estado del AF....................................................................................................... 55

Fig. 72: LR(0) ............................................................................................................................... 56 Fig. 73: Otra figura ...................................................................................................................... 56

Fig. 74: DFA de aAA |)( ........................................................................................................ 57

Fig. 75: LR(1) ............................................................................................................................... 57

Fig. 76: Algún título ...................................................................................................................... 58

Fig. 77: Algún título ...................................................................................................................... 59

Fig. 78: NFA que reconoce una letra ............................................ ¡Error! Marcador no definido. Fig. 79: NFA que reconoce un dígito............................................ ¡Error! Marcador no definido. Fig. 80: NFA que reconoce un identificador ................................ ¡Error! Marcador no definido. Fig. 81: NFA que reconoce un dígito............................................ ¡Error! Marcador no definido. Fig. 82: NFA que reconoce un punto y coma ............................... ¡Error! Marcador no definido. Fig. 83: NFA que reconoce el operador de asignación (:=) .......... ¡Error! Marcador no definido. Fig. 84: NFA que reconoce un paréntesis abierto ......................... ¡Error! Marcador no definido. Fig. 85: NFA que reconoce un paréntesis cerrado ........................ ¡Error! Marcador no definido.

Fig. 86: Autómata finito que reconoce algún operador de suma .. ¡Error! Marcador no definido.

Page 5: Libro alumnos

Fig. 87: Autómata finito que reconoce algún operador de multiplicación ... ¡Error! Marcador no

definido. Fig. 88: Autómata Finito no Determinista .................................... ¡Error! Marcador no definido.

Fig. 89: Autómata Finito Determinista ......................................... ¡Error! Marcador no definido. Fig. 90: Algún título ...................................................................... ¡Error! Marcador no definido. Fig. 91: Interfaz de usuario ........................................................... ¡Error! Marcador no definido.

Page 6: Libro alumnos

Índice de tablas

Tabla 1: Salida del analizador léxico para la expresión ................................... 18 Tabla 2: Tokens del programa fuente de la Fig. 17 ...................................................................... 21 Tabla 3: Resultado de la función de transición para el NFA de la Fig. 20 ................................... 25 Tabla 4: Secuencia de instrucciones sugerida por el diagrama de transición de la Fig. 46. ......... 43 Tabla 5: Tabla de transición construida del diagrama de transición de la figura 9. ..................... 43

Tabla 6: Análisis léxico basado en la Tabla 4 de transiciones ...................................................... 44 Tabla 7: Tabla parse LL(1) para la gramática de la izquierda ...................................................... 48 Tabla 8: Rutina parse LL(1) genérica ........................................................................................... 48 Tabla 9: Tabla LALR(1) ............................................................................................................... 57

Tabla 10: Tabla LR(1) .................................................................................................................. 57 Tabla 11: Algoritmo ...................................................................................................................... 59 Tabla 12: Algún título ................................................................................................................... 60

Tabla 13: Tabla de cerraduras de los elementos del NFA de laFig. 88 ........ ¡Error! Marcador no

definido. Tabla 14: Código escrito en Flex .................................................. ¡Error! Marcador no definido. Tabla 15: Gramática ...................................................................................................................... 62 Tabla 16: Gramática re-escrita ...................................................... ¡Error! Marcador no definido.

Tabla 17: Tabla parse (parte 1) ..................................................... ¡Error! Marcador no definido. Tabla 18: Tabla parse (parte 2) ..................................................... ¡Error! Marcador no definido.

Tabla 19: Tabla parse (parte 3) ..................................................... ¡Error! Marcador no definido. Tabla 20: Tabla parse (parte 4) ..................................................... ¡Error! Marcador no definido.

Tabla 21: Código correspondiente al análisis sintáctico escrito en Bison .... ¡Error! Marcador no

definido.

Page 7: Libro alumnos

1. Introducción

Idealmente, un curso de compiladores debería llevarse en 2 semestres. Durante el primero

de éstos, se revisarían con detenimiento las técnicas asociadas a los diferentes tipos de análisis

que involucra la construcción de un compilador: autómatas de estados finitos y gramáticas

regulares para el análisis léxico, y autómatas de pila y gramáticas libres de contexto para el

análisis sintáctico y semántico. Durante el segundo semestre, se revisarían las técnicas asociadas

a la generación de código: grafos dirigidos acíclicos y código de tres direcciones para la

generación de código intermedio, asignación de registros y grafos de flujo para la generación de

código, y transformaciones para la optimización de código, entre otras. Además, hay que

mencionar que en ambos semestres se deben revisar las técnicas para la construcción de las

tablas de literales y de símbolos, así como para el módulo de manejo de errores pues todos ellos

guardan una estrecha relación con cada una de las fases de análisis y síntesis (esta última es la

encargada de la generación de código). En la realidad, en general, un curso de compiladores se

lleva en sólo un semestre. Esto hace que el material del curso se tenga que revisar rápidamente y

que con frecuencia dicho material no pueda cubrirse en su totalidad. Hay que mencionar también

que un curso de compiladores se enseña a los estudiantes que están cursando los últimos

semestres de su carrera pues se necesitan varios cursos pre-requisito para entenderlo:

matemáticas discretas, algoritmos y estructuras de datos, lenguajes de programación,

programación de sistemas, teoría de la computación, arquitectura de computadoras e ingeniería

de software, como mínimo. En la medida de lo posible, el material expuesto en el presente libro

será autocontenido; esto con la finalidad de revisar más rápidamente los temas aquí incluidos.

Sin embargo, es necesario hacer hincapié en que, dada la complejidad de un curso de esta

naturaleza, el estudiante lo aprovechará más si realiza por su cuenta los ejercicios de cada

capítulo así como si refuerza cada tema consultando fuentes complementarias. Por si esto fuera

poco, un curso de compiladores no sólo exige al estudiante desarrollar sus saberes teóricos sino

también los prácticos: para entender con mayor claridad el poder de un compilador, es necesario

no sólo comprender los conceptos teóricos a partir de los cuales se construye sino además

implementar dichos conceptos que lo harán darse cuenta que, al menos en este tópico en

particular, la teoría no está muy alejada de la práctica. Hay que decir, finalmente, que la

construcción de un compilador comercial involucra un equipo de al menos decenas de personas:

desarrolladores, diseñadores, ingenieros y arquitectos de software, „testers‟, etc. Es por esto que

un curso de compiladores a nivel licenciatura sólo puede aspirar a proveer al estudiante con las

técnicas básicas necesarias para la construcción de un compilador sencillo que pueda mostrar el

potencial de dichas técnicas en la construcción de un compilador comercial. Si el estudiante

entiende claramente todas estas técnicas, no le será muy difícil involucrarse en el proceso de

construcción de un compilador de este tipo, sea cual sea su participación.

En este capítulo, revisaremos brevemente los conceptos fundamentales sobre compiladores

y veremos cómo se aplican en cada una de las fases de un compilador. En cierta medida, es

como un resumen del resto del libro: presentaremos cómo un programa en código fuente es

traducido a su equivalente en código objeto, el cual puede ser entendido y ejecutado por la

computadora en cuestión. El resto de los capítulos exponen de manera más detallada cada una de

las técnicas para lograr este objetivo.

1.1 Tipos de traductores

Un lenguaje de programación sirve como canal de comunicación entre un usuario

humano y una computadora. Es decir, si un humano quiere implementar la solución de un

Page 8: Libro alumnos

problema específico en una computadora, éste debe usar un lenguaje de programación. Hoy en

día es tan común la noción de lenguaje de programación (generalmente de alto nivel) que nos

olvidamos de que la computadora no “entiende” directamente dicho lenguaje: el lenguaje que

ésta entiende está formado por largas cadenas de ceros y unos. Para que la computadora

“entienda” y ejecute las instrucciones contenidas en un programa escrito en algún lenguaje de

programación, dichas instrucciones deben ser traducidas al lenguaje que sí entiende la máquina:

el lenguaje binario. Podríamos programar una computadora usando directamente estas largas

secuencias de ceros y unos pero esto involucra una ardua y difícil tarea que hace muy

complicada la interacción con ella. La idea fundamental es entonces construir un traductor que

tome como entrada un programa escrito en un lenguaje de programación (frecuentemente de alto

nivel) y lo convierta en una versión equivalente en lenguaje de máquina. El lenguaje de máquina

es una representación abreviada de las secuencias de ceros y unos usando códigos numéricos, los

cuales representan operaciones en la máquina anfitrión. Un lenguaje de máquina representa el

más bajo nivel de un lenguaje de programación. Por ejemplo,

C7 06 0000 0002

representa la instrucción para mover el número 2 a la ubicación 0000 (en sistema hexadecimal)

en los procesadores Intel 8x86 que se utilizan en las PC de IBM

En general, al programa de entrada se le conoce como programa fuente y al programa de

salida como programa objeto o programa destino. Es importante señalar que el programa fuente

está escrito en un lenguaje fuente (comúnmente de alto nivel) y que el programa objeto

pertenece a un lenguaje objeto (que bien puede ser lenguaje máquina, lenguaje ensamblador o

incluso otro lenguaje de alto nivel). Un compilador que toma como entrada un programa fuente

escrito en un lenguaje de alto nivel y produce como salida un programa objeto escrito también en

un lenguaje de alto nivel se le conoce como “source-to-source”. En este libro construiremos un

compilador para un lenguaje de programación sencillo cuyos programas objeto estarán en

lenguaje ensamblador. Esta práctica es útil ya que no sólo es más fácil producir programas en

ensamblador (pues se evita generar código para la arquitectura de una computadora en

particular) sino que también es más fácil depurar los programas objeto escritos en este lenguaje.

Nos concentraremos entonces en la generación de código en lenguaje ensamblador que puede a

su vez ser leído por un programa ensamblador (de los cuales existen varias versiones que pueden

descargarse de la red e instalarse de forma gratuita) y así éste traducirlo a código máquina. De

hecho, algunos diseñadores de lenguajes de programación van más allá de esta práctica al

construir compiladores “source-to-source” para programas cuyo código fuente es traducido a

código que está en algún lenguaje de alto nivel (como C). Así, ellos aprovechan los

compiladores existentes que reciben como entrada el código escrito en este lenguaje objeto y

pueden revisar rápidamente el funcionamiento del lenguaje de su propio diseño sin tener que

preocuparse demasiado por los detalles de la generación de código en lenguaje máquina.

Aunque por el momento hemos hablado solamente de compiladores como traductores,

existen también otros tipos: ensambladores e intérpretes.

Un ensamblador (assembler) es un traductor cuya entrada es un programa escrito en

lenguaje ensamblador (assembly language) y cuya salida es un programa escrito en lenguaje de

máquina. Una posible secuencia de código en lenguaje ensamblador es la siguiente:

MOV R0, index ;; valor de index R0

MUL R0, 2 ;; duplica el valor en R0

MOV R1, &a ;; dirección de a R1

ADD R1, R0 ;; sumar R0 a R1

Page 9: Libro alumnos

MOV *R1, 6 ;; constante 6 dirección en R1

Un intérprete es también un traductor que no genera código objeto (como lo hace un

compilador) sino que ejecuta el programa fuente inmediatamente. En otras palabras, un

intérprete procesa y ejecuta al mismo tiempo el programa fuente y los datos de entrada para éste.

La Fig. 1 muestra a grandes rasgos como funciona un intérprete.

Fig. 1: Proceso de interpretación

Como puede apreciarse, el proceso de traducción usando un intérprete se realiza cada vez

que éste es ejecutado. Por ende, en general, los intérpretes tienden a ser mucho más lentos que

los compiladores (hasta por un factor de 10 o más) [ref. Louden, p. 5]. Sin embargo, por otro

lado, un intérprete puede por lo regular proveer un mejor diagnóstico de errores que un

compilador toda vez que aquél ejecuta el programa fuente instrucción por instrucción.

Un compilador es, como mencionamos, un traductor que toma como entrada un programa

fuente y lo convierte a un programa objeto o destino. Este programa objeto es una traducción fiel

del programa fuente escrita en lenguaje máquina, lenguaje ensamblador o incluso en algún otro

lenguaje de programación. Una vez generado el programa objeto, éste es ejecutado al recibir sus

respectivas entradas (ver Fig. 2 y Fig. 3).

Fig. 2: Un compilador

suma

deposito_inicial

interes60

:=

+

*

Fig. 3: Árbol sintáctico para

De haber errores en el programa fuente, el compilador deberá reportarlos y, de ser

posible, corregirlos. En comparación con un intérprete, un compilador traduce una sola vez el

programa fuente (el cual se convierte, después del proceso de traducción, en el programa objeto).

Así, cada vez que se ejecute el correspondiente programa objeto, ya no es necesario hacer de

nuevo otra traducción, lo cual ahorrará tiempo significativo de procesamiento. Es por esta razón

que un compilador es en general mucho más rápido que un intérprete. En la sección 1.4

mencionamos brevemente las fases de un compilador para que se pueda apreciar, entre otras

cosas, la complejidad en el proceso de traducción. El resto del libro (a partir del capítulo 2)

revisa con detalle cada una de estas fases.

Es importante mencionar que existen traductores híbridos, los cuales combinan el

proceso de interpretación con el de compilación. Los traductores para el lenguaje de

programación Java son un ejemplo de este tipo: un programa fuente escrito en Java puede

compilarse en una representación intermedia llamada “bytecodes” que después es interpretada

Page 10: Libro alumnos

por una máquina virtual. El beneficio de este tipo de traductores es que la representación

intermedia puede compilarse en una computadora e interpretarse en otra distinta (revisar el

concepto de portabilidad). La Fig. 4 muestra un traductor híbrido.

posicion

identificador

identificador

numero

expresion expresion+

Expresion aditiva

identificador expresion:=

Expresion de asignacion

inicial identificador

expresion *

velocidad

expresion

60

Fig. 4: Traductor híbrido para

Finalmente, para cerrar esta sección, hay que decir que hay otros programas relacionados

estrechamente con los compiladores: preprocesadores, ligadores, cargadores, editores y

depuradores, entre otros. Todos estos programas complementan la labor de un compilador y

cuyas tareas van desde facilitar al programador la escritura del programa fuente hasta crear el

programa objeto y determinar los errores de ejecución en dicho programa. Para mayores detalles

sobre dichos programas, se sugiere al lector consultar [ref. Louden y dragón].

1.2 Autómatas

Aunque en la sección 1.4 mencionaremos las fases de las que típicamente consta un

compilador, en esta sección aprovechamos para revisar brevemente los modelos de cómputo que

se usan en las fases correspondientes al análisis: autómatas de estados finitos para el análisis

léxico y autómatas de pila para el análisis sintáctico y semántico. Por el momento, no entramos

en detalles sobre estos modelos pero sí presentamos sus correspondientes definiciones formales

para que el lector aprecie que un compilador está basado en fundamentos matemáticos sólidos.

En el capítulo 2 presentamos minuciosamente a los autómatas finitos y sus correspondientes

lenguajes y gramáticas asociados: lenguajes y gramáticas regulares. En los capítulos 3 y 5

revisamos a los autómatas de pila y sus correspondientes lenguajes y gramáticas asociadas:

lenguajes y gramáticas libres de contexto.

1.2.1 Autómatas finitos (FA – finite automata)

Los autómatas de estados finitos, o simplemente autómatas finitos, son el modelo más

sencillo de cómputo. Esto no significa que tienen poco poder: de hecho, los autómatas finitos

son poderosos reconocedores de patrones en los datos. Esto es precisamente lo que queremos

hacer en primer lugar con el programa fuente: reconocer en él ciertos patrones que nos permitan

clasificarlos en tokens (los tokens son conjuntos de caracteres que forman una entidad).

Page 11: Libro alumnos

Ejemplos típicos de tokens son: nombres de variables (o identificadores), signos de agrupación

(como paréntesis, corchetes y llaves), símbolos de operaciones (suma, resta, multiplicación,

división), signos de puntuación (punto, coma, punto y coma) y números (enteros, reales), entre

otros. Para clarificar el concepto de token, en la sección 1.4 presentamos un ejemplo de cómo un

analizador léxico divide el programa fuente en dichos elementos. Además, en el capítulo 2,

revisaremos paso a paso cómo usar los autómatas finitos (y modelos equivalentes como las

expresiones y gramáticas regulares) para este fin. Por el momento, daremos las definiciones

formales de un FA para que el lector empiece a apreciar los fundamentos matemáticos que

soportan la construcción de un compilador.

La teoría sobre autómatas finitos suele revisarse en un curso de matemáticas discretas, de

teoría de la computación o de programación de sistemas. De cualquier manera, aquí repasaremos

estos conceptos pero nos concentraremos, en el capítulo 2, en cómo usarlos para construir un

analizador léxico.

Un FA puede ser de dos tipos: determinista (DFA) o no determinista (NFA). Aunque

estas definiciones difieren una de la otra principalmente en la función de transición, el poder de

cómputo de cada uno de estos tipos es equivalente: aquellas cadenas de símbolos que reconoce

uno las reconoce el otro y viceversa. De hecho, en el capítulo 2, revisamos un par de teoremas (y

sus respectivas demostraciones) que nos permiten construir, para cada NFA, su equivalente

DFA. En las secciones siguientes, damos la definición formal de DFA y NFA respectivamente.

1.2.1.1 Autómatas finitos deterministas (DFA – deterministic finite automata)

Un DFA es una 5-tupla ),,,,( 0 FqQ donde:

Q es un conjunto finito llamado estados

es un conjunto finito llamado alfabeto

QQ : es la función de transición

Qq 0 es el estado inicial

QF es el conjunto de estados de aceptación

Un ejemplo de un DFA aparece en la Fig. 5

Fig. 5: DFA que reconoce cadenas que contienen

al menos 2 a´s (sin importar el orden)

Como puede observarse, este DFA contiene 3 estados (q‟1, q‟2, q‟3), 2 elementos en el

alfabeto (a, b), un estado inicial (q‟1, el cual está marcado por la flecha viniendo de ningún

lugar), un estado final (q‟3, el cual se identifica con un doble círculo) y una función de transición

determinista: para cada entrada compuesta por cualquier combinación entre un estado y un

elemento del alfabeto, existe una única salida (un estado). Esta función de transición es la que

caracteriza a los DFA. En el capítulo 2 revisaremos con detalle cada una de las partes de dicha

Page 12: Libro alumnos

función. En la siguiente sección veremos que la función de transición que caracteriza a los NFA

contiene un ingrediente distinto al de los DFA: el no determinismo.

1.2.1.2 Autómatas finitos no deterministas (NFA – nondeterministic finite automata)

Un NFA es una 5-tupla ),,,,( 0 FqQ donde:

Q es un conjunto finito de estados

es un alfabeto finito

:Q P )(Q es la función de transición

Qq 0 es el estado inicial

F Q es el conjunto de estados de aceptación

Un ejemplo de un DFA aparece en la Fig. 6

Fig. 6: NFA que reconoce a la cadena vacía o cadenas que tienen

cualquier número de a´s

Como puede observarse, este NFA contiene 4 estados (q1, q2, q3, q4), 1 elemento en el

alfabeto (a), un estado inicial (q1), un estado final (q4) y una función de transición no

determinista: en contraste con un DFA, un NFA no tiene necesariamente que tener, para cada

entrada compuesta por cualquier combinación entre un estado y un elemento del alfabeto, una

única salida (un estado). De hecho, la definición de la función de transición para un NFA

cualquiera contempla como salida un conjunto de estados (incluido por supuesto el conjunto

vacío). Es por esto que esta función de transición incluye la definición del conjunto potencia

sobre el conjunto de estados así como la posibilidad de tener la cadena vacía como entrada en

uno de los argumentos de dicha función. Esto significa, para el primer caso (la definición del

conjunto potencia sobre el conjunto de estados), que dados como entrada un estado y un

elemento del alfabeto (incluida la cadena vacía), la salida es un conjunto de estados: esta

característica es la que define principalmente a la propiedad de no determinismo. Por ejemplo,

para nuestro NFA de la Fig. 6, si el autómata se encuentra en el estado q1, éste puede saltar tanto

al estado q2 como al estado q4 con la cadena vacía. Para el segundo caso (la posibilidad de tener

la cadena vacía como entrada), tener transiciones con la cadena vacía como entrada significa que

el autómata puede pasar de un estado a otro sin tener que leer absolutamente nada de la cadena

de entrada. Además, un NFA permite que no necesariamente para cada combinación de entrada

(estado x elemento del alfabeto) exista una salida determinada. Para esta misma figura podemos

apreciar que no existe transición (por mencionar una de ellas) cuando se está en el estado q1 y se

tiene una „a‟. Las implicaciones de estas características las revisaremos con detalle en el capítulo

2. En esta sección sólo queremos introducir algunos conceptos fundamentales que servirán de

base para construir un analizador léxico. Para finalizar dicha sección, debemos decir nuevamente

que usaremos la teoría de autómatas finitos para construir un analizador léxico pasando por los

siguientes pasos:

Expresión regular NFA DFA Programa

Page 13: Libro alumnos

A partir de una expresión regular (la cual revisaremos en el capítulo 2 y que sirve para

representar los tokens de un programa fuente), podemos construir un NFA que represente esa

expresión; después, a partir de ese NFA, se construye su DFA equivalente, el cual sirve para

codificar, en algún lenguaje de programación, el reconocedor léxico para ese token en

específico. Una vez más, en el capítulo 2 revisaremos a detalle cada uno de estos pasos.

1.2.2 Autómata de Pila (PDA – push-down automaton)

Los autómatas de pila tienen un componente extra respecto a los FA (sean deterministas

o no deterministas): una memoria tipo pila. Los FA en general sólo cuentan con sus estados

como memoria; es por ello que los FA son el modelo más sencillo de cómputo. Cada estado en

un FA sólo “recuerda” el último elemento del alfabeto con el cual se llegó a dicho estado. Si

necesitáramos que el autómata recuerde una secuencia de estos elementos, es necesario entonces

agregarle explícitamente una memoria. Para los PDA, la memoria es de tipo pila (LIFO – last

input first output). Con este componente extra, es posible reconocer lenguajes que no pueden ser

reconocidos por los FA. A los lenguajes aceptados/reconocidos por un PDA se les conoce como

lenguajes libres de contexto. Un ejemplo de un PDA con su correspondiente lenguaje libre de

contexto que reconoce se presenta en la Fig. 7. El lenguaje reconocido por este autómata es

(con ), es decir, dicho PDA reconoce cadenas conformadas por un número específico

de ceros (denotado por ) seguido del mismo número de unos. Es importante mencionar que no

existe un FA que reconozca dicho lenguaje: es aquí donde queda de manifiesto su limitación

para reconocer lenguajes que no son regulares. Por supuesto que se revisarán a detalle los

conceptos de lenguajes/gramáticas regulares y libres de contexto en los capítulos 2 y 3

respectivamente. Por el momento, el lector puede intentar construir un FA que reconozca este

lenguaje. Al intentarlo, podrá notar que lo mejor que podrá hacer es construir un FA con

instancias específicas de este lenguaje: , , etc., pero no logrará construir un solo NFA

que pueda contender con el caso general; i.e., con cualquier valor de n. Dicho sea de paso,

cuando , entonces la cadena resultante es la cadena vacía. Esta cadena cumple con la

condición que impone este lenguaje: un número específico de ceros (en este caso ninguno)

seguido del mismo número de unos. Entonces, para poder reconocer este lenguaje, se necesita un

elemento extra: la pila. Los PDA son la base para construir analizadores sintácticos. Para el caso

concreto de un compilador, un analizador sintáctico sirve para verificar que la estructura del

programa fuente sea la correcta; i.e., que el programa fuente esté correctamente escrito. Como

los lenguajes de programación están basados en gramáticas libres de contexto, y éstas son

definiciones equivalentes a los autómatas de pila, éstos entonces pueden ser usados para

reconocer que la estructura de un programa fuente (escrito en algún lenguaje de programación)

sea correcta. En el capítulo 3 revisamos cómo se logra esto. Por el momento, veamos la

definición formal de un PDA para que el lector empiece a familiarizarse con este tipo de

autómata.

1.2.2.1 Autómatas de pila

Un PDA es una 6-tupla

(Q, ,,, q0, F)donde Q , , y F son todos conjuntos

finitos y:

Q es el conjunto finito de estados.

es el alfabeto de entrada.

es el alfabeto de la pila.

Q: P

(Q ) es la función de transición.

Page 14: Libro alumnos

Qq 0 es el estado inicial.

QF es el conjunto de estados de aceptación.

q1

q4

ε, ε $

1, 0 ε

ε, $ ε

0, ε 0

1, 0 ε

q2

q3

Fig. 7: PDA que reconoce lenguajes del tipo

Como puede observarse en la definición, un PDA consta de 6 partes. La parte extra con

respecto a los FA es la pila, la cual acepta un alfabeto específico que bien puede ser diferente al

alfabeto de entrada. Por ejemplo, en el PDA de la Fig. 7, el alfabeto de entrada * +, mientras que el alfabeto de la pila es * +. Por otro lado, tenemos a la función de transición

que, debido a la pila, se vuelve más compleja: la entrada de dicha función está formada por un

elemento de los estados del autómata, un elemento del alfabeto de entrada (incluida la cadena

vacía) y uno de la pila respectivamente (incluida la cadena vacía), y la salida por un elemento en

el conjunto de estados y un elemento en la pila (incluida la cadena vacía). Aunque revisaremos a

detalle los PDA en el capítulo 3, podemos mencionar aquí brevemente el significado de la

función de transición. Tomando como referencia a la Fig. 7, podemos decir por ejemplo que para

que el autómata pase del estado al , tienen que cumplirse 2 condiciones: que no se lea nada

de la entrada (esto es, que se lea la cadena vacía - representada por ) y que no se lea nada de la

pila (representado también por ); el resultado será entonces pasar al estado q2 desde el estado q1

modificando el contenido de la pila al meter a ésta el símbolo especial $. Las operaciones de

lectura y escritura de la pila se conocen comúnmente como “pop” y “push” respectivamente. En

el capítulo 3 construiremos un analizador sintáctico a partir de la teoría de autómatas de pila y

gramáticas libres de contexto (éstas últimas son una definición equivalente a los PDA). En la

siguiente sección, presentamos brevemente los dos tipos de gramáticas que usaremos para el

análisis léxico y sintáctico respectivamente: gramáticas regulares y gramáticas libres de

contexto.

1.3 Gramáticas formales

Antes de hablar de gramáticas formales, debemos mencionar brevemente qué es un

lenguaje formal. A diferencia de un lenguaje natural (como el inglés, español, francés, etc.), un

lenguaje formal está definido por reglas preestablecidas; ejemplos de lenguajes formales son los

lenguajes de programación, el álgebra y la lógica proposicional. Para el caso de un lenguaje de

programación, esta característica de los lenguajes formales permite la construcción eficiente de

un traductor automático (por ejemplo, un compilador). Para el caso de un lenguaje natural, es la

falta de estas reglas preestablecidas la que hace una tarea compleja la construcción de un

traductor automático para dicho lenguaje. Son precisamente estas reglas las que conforman

principalmente una gramática. Una gramática permite entonces verificar si un enunciado está

correctamente escrito dado un lenguaje específico. En nuestro caso, un enunciado será un

Page 15: Libro alumnos

programa fuente escrito en algún lenguaje de programación. Utilizaremos un tipo de gramática

conocida como gramática regular para verificar si los tokens de un programa fuente pertenecen

al lenguaje de programación en cuestión; usaremos una gramática conocida como gramática

libre de contexto para verificar que la sintaxis de un programa fuente es correcta, de acuerdo a

dicho lenguaje de programación. En las siguientes secciones revisamos brevemente las

definiciones de una gramática regular y una gramática libre de contexto respectivamente.

1.3.1 Gramática Regular

En general, una gramática consiste en un conjunto de reglas de sustitución o de re-

escritura conocidas también como producciones. Cada regla aparece en una línea de la gramática

conteniendo un símbolo (variable) del lado izquierdo de una flecha y una cadena de símbolos

(que pueden ser variables y símbolos terminales) del lado derecho de dicha flecha (Fig. 8). Las

variables están comúnmente representadas por letras mayúsculas mientras que los símbolos

terminales por letras minúsculas, números o símbolos especiales (los símbolos terminales son

análogos al alfabeto de entrada). Además, una de las variables se designa como el símbolo

inicial de la gramática y frecuentemente se escribe del lado izquierdo de la primera regla de la

gramática. Para el caso de la Fig. 8, la única variable es la letra S, la cual, por ende, coincide con

ser el símbolo inicial de la gramática. Los símbolos terminales son las letras x, y, z. Para el caso

específico de una gramática regular, las reglas de re-escritura se conforman de acuerdo a las

siguientes restricciones: el lado izquierdo de cualquiera de estas reglas de re-escritura debe

consistir en un solo no-terminal y el lado derecho debe ser un terminal seguido por un no-

terminal, un solo terminal o la cadena vacía (representada por o ). Las reglas de la Fig. 8

conforman una gramática regular así como las de la Fig. 9 (izquierda). Las reglas de la derecha

de esta última figura no son permitidas en una gramática regular pues no cumplen con las

restricciones antes mencionadas.

S xS

S y

S z Fig. 8: Ejemplo de reglas gramaticales

Z yX

Z x

W

Reglas

permitidas en

una

gramática

regular

yW X

X Zy

WyZYX

Reglas no

permitidas

en una

gramática

regular

Fig. 9: Ejemplo de reglas permitidas en una gramática regular (izquierda) y de reglas no permitidas en una

gramática regular (derecha)

Formalmente, una gramática regular es una 4-tupla ),,,( SRV donde:

1. V es un conjunto finito, llamado variables (o no-terminales).

2. es un conjunto finito disjunto de V, llamado terminales.

3. R es un conjunto finito de reglas, con cada regla siendo una variable y una cadena de

variables y terminales conforme a las restricciones antes mencionadas.

4. S es la variable inicial.

Page 16: Libro alumnos

En el capítulo 2 revisaremos la manera detallada de construir la siguiente secuencia:

Expresión regular NFA DFA Programa

Por el momento, podemos decir que una expresión regular es equivalente a una gramática

regular (buscar teorema). Dichas expresiones regulares pueden usarse para definir los tokens de

nuestros programas fuente (basados en algún lenguaje de programación específico) y, a partir de

éstas, construir un autómata finito que reconozca dichos tokens. Una vez hecho esto, es posible

escribir un programa que identifique estos tokens y así verificar que cada uno de éstos sean

expresiones válidas dentro de nuestro lenguaje de programación de referencia.

1.3.2 Gramática libre de contexto (CFG – Context Free Grammar)

Una CFG es una 4-tupla ),,,( SRV donde:

5. V es un conjunto finito, llamado variables (o no-terminales).

6. es un conjunto finito disjunto de V, llamado terminales.

7. R es un conjunto finito de reglas, con cada regla siendo una variable a la izquierda de la

flecha y una cadena de variables y terminales a la derecha de la flecha.

8. S es la variable inicial.

Un ejemplo de una CFG aparece en la Fig. 10.

zMNzS aMaM

M z bNbN

N z Fig. 10: Ejemplo de una CFG

En el capítulo 3 revisaremos diferentes técnicas para construir un analizador sintáctico

basado en una CFG. Por el momento, podemos decir que la mayoría de los lenguajes de

programación están basados en una CFG, lo cual nos permite utilizar a los PDA para verificar si

un programa fuente, escrito en algún lenguaje de programación específico, está escrito

correctamente o, dicho de otra manera, si su estructura gramatical es la correcta.

1.4 Fases de un compilador

En esta sección revisaremos brevemente las fases de un compilador (ver ¡Error! No se

encuentra el origen de la referencia.).

Page 17: Libro alumnos

Analizador léxico

o rastreador

Analizador

sintáctico

Analizador

semántico

Optimizador de

código fuente

Generador de

código

Optimizador de

código objetivo

Código fuente

Tokens

Árbol sintáctico

Árbol con anotaciones

Código intermedio

Código objetivo

Código objetivo

Tabla de

literales

Tabla de

símbolos

Manejador

de errores

Fig. 11: Fases de un compilador

En primer lugar, el programa fuente (escrito en algún lenguaje de programación

determinado) sirve de entrada al analizador léxico o rastreador [ref.]. Como ejemplo, digamos

que nuestro programa fuente consta de la siguiente línea:

a[index] = 4+2

La salida del analizador léxico es un conjunto de tokens que forman parte del lenguaje de

programación en cuestión (ver Tabla 1):

Lexema1 Tipo de token

1 Un lexema es un conjunto de caracteres del programa fuente que representan una secuencia

significativa

Page 18: Libro alumnos

a identificador

[ corchete izquierdo

index identificador

] corchete derecho

4 número

+ operador de adición

2 Número

Tabla 1: Salida del analizador léxico para la expresión , -

Como se puede apreciar en la Tabla 1, el analizador léxico ignora los espacios en blanco.

Toca ahora el turno del analizador sintáctico, el cual toma como entrada los tokens

producidos en la fase anterior y genera con ellos un árbol sintáctico (ver Fig. 12).

Identificador

a

Identificador

index

expresion expresion

Expresion de subindice

[ ]

numero

4

numero

2

expresion expresion+

Expresion aditiva

expresion expresion=

Expresion de asignacion

expresion

Fig. 12: Árbol sintáctico de la expresión

a [ index ] 4 2

Como se puede apreciar en la Fig. 12, la línea de código del presente ejemplo se

representa en forma de un árbol, en el cual los nodos internos de dicho árbol representan una

operación y los hijos de cada uno de estos nodos representan los argumentos de sus respectivas

operaciones.

La tercera fase corresponde al analizador semántico, el cual toma como entrada el árbol

sintáctico y produce como salida un árbol con anotaciones (ver Fig. 13). Éstas incluyen las

declaraciones y la verificación de tipos.

Page 19: Libro alumnos

Fig. 13: Árbol semántico (corregir este árbol) de la expresión

a [ index ] 4 2

La cuarta fase corresponde al optimizador de código fuente (Fig. 14). Esta fase toma

como entrada el árbol con anotaciones y produce como salida una representación intermedia (o

código intermedio) entre el programa fuente y el programa objeto, el cual optimiza (siempre que

sea posible) las operaciones representadas en el árbol sintáctico. Por ejemplo, en el árbol de la

Fig. 14, la rama derecha de dicho árbol es el resultado de colapsar el subárbol derecho de la Fig.

12. Es importante mencionar que aunque muchas optimizaciones se pueden llevar a cabo

directamente sobre el árbol, en varios casos se utiliza una representación lineal de éste conocida

como código en tres direcciones (pues contiene hasta tres operandos por instrucción, tal y como

sucede en las instrucciones en lenguaje ensamblador). Este tipo de representación se revisará

más a detalle en el capítulo 5.

Fig. 14: Optimizador de código fuente

La quinta fase se refiere a la generación de código. Ésta toma como entrada la

representación intermedia generada en la fase anterior y produce su correspondiente código para

la máquina objeto. Como mencionamos ya en la sección 1.1, en este libro construiremos un

compilador para un lenguaje de programación sencillo cuyos programas objeto estarán en

lenguaje ensamblador. El código en un hipotético lenguaje ensamblador (considerar agregar

código en ensamblador real producido en el compilador de Louden) que se genera a partir de la

representación intermedia mostrada en la Fig. 14, se presenta en la Fig. 15.

MOV R0, index ;; Valor de index R0

MUL R0, 2 ;; Doble valor en R0

MOV R1, &a ;; Dirección de a R1

ADD R1, R0 ;; Sumar R0 a R1

Page 20: Libro alumnos

MOV *R1, 6 ;; Constante 6 dirección en R1

Fig. 15: código objeto en ensamblador generado a partir de la representación intermedia de la Fig. 14

Para este ejemplo específico, &a es la dirección de a y *R1 significa direccionamiento

indirecto de registro, por lo que la última instrucción guarda el valor 6 en la dirección apuntada

por R1. En el capítulo 6 revisaremos con detalle cómo generar código objeto a partir de una

representación intermedia.

La última fase propiamente dicha es la optimización de código objeto, la cual intenta

mejorar el código que ha sido generado en la fase anterior. La optimización incluye, en términos

generales, que se sustituyan instrucciones lentas por otras más rápidas así como que se eliminen

operaciones redundantes o innecesarias. La Fig. 16 muestra la optimización del código objeto de

la Fig. 15.

Optimizador de código objeto

MOV R0, index ;; Valor de index R0

SHL R0 ;; doble valor en R0

MOV &a[R0], 6 ;; constante 6 dirección a + R0

Fig. 16: Código objeto optimizado

Como se puede apreciar en la Fig. 16, el optimizador ha reducido el número de líneas con

respecto al código de la Fig. 15 manteniendo el mismo significado del programa pero reduciendo

el tiempo de ejecución. En el capítulo 7 revisaremos con detalle las técnicas para la optimización

de código objeto.

Para terminar esta sección, es importante mencionar que cada una de las fases de un

compilador interactúan con 3 componentes, tal y como lo muestra la ¡Error! No se encuentra el

origen de la referencia.: la tabla de literales, la tabla de símbolos y el manejador de errores.

Brevemente podemos mencionar que la tabla de literales se utiliza básicamente para almacenar

constantes y cadenas que se usan a lo largo de un programa, la tabla de símbolos guarda la

información asociada con los identificadores (tales como funciones, variables, constantes y tipos

de datos) mientras que el manejador de errores es el módulo que se encarga no sólo de reportar

claramente los problemas generados en cada fase del compilador sino también de corregirlos. En

los capítulos 3 y 4 veremos algunas técnicas para la recuperación de errores sintácticos y la

construcción de tablas de símbolos respectivamente. En el siguiente capítulo, revisaremos las

técnicas para construir la primera fase de un compilador: el analizador léxico.

Page 21: Libro alumnos

2. Análisis Léxico

En esta unidad revisaremos a detalle las técnicas asociadas a la fase de análisis léxico de

un compilador. Básicamente lo que queremos lograr es construir la siguiente secuencia:

Expresión regular NFA DFA Programa

Recordemos que el trabajo del analizador léxico es dividir en tokens (unidades

significativas del lenguaje en cuestión) el programa fuente y reconocer si dichos tokens forman

parte del lenguaje para el cual se está llevando a cabo el proceso de traducción. Por ejemplo,

dado el siguiente programa:

comienza

a:=b3;

termina; Fig. 17: Un pequeño ejemplo de un programa fuente

Nuestro analizador deberá reconocer los siguientes tokens:

Lexema Tipo

comienza palabra clave

a identificador

:= operador de asignación

b3 identificador

; Símbolo especial

termina palabra clave

Tabla 2: Tokens del programa fuente de la Fig. 17

Si algún token no estuviera previamente incluido en la definición de nuestro lenguaje de

programación como un token válido, entonces la labor de nuestro analizador léxico es detectar a

dicho token como inválido. Por ejemplo, podemos observar que el operador de asignación está

formado por el símbolo compuesto :=. Si el programa estuviera escrito de la siguiente forma

(Fig. 18):

comienza

a=b3;

termina; Fig. 18: un pequeño ejemplo de un programa fuente con un error léxico

Y suponiendo que el símbolo = (sin los dos puntos) no ha sido incluido como símbolo

válido en la definición de nuestro lenguaje de programación, entonces nuestro analizador léxico

deberá producir un mensaje de error cuando encuentra dicho símbolo en el programa fuente. Por

otro lado, suponiendo que el paréntesis izquierdo y el paréntesis derecho son símbolos válidos

dentro de nuestro lenguaje de programación, entonces en programas como el de la Fig. 19 no

existe error léxico:

comienza

a=b3));

termina

Page 22: Libro alumnos

Fig. 19: un pequeño ejemplo de un programa fuente sin error léxico

La razón es porque al dividir en tokens el programa de la Fig. 19, el analizador léxico

reconocerá los 2 paréntesis derechos que aparecen en la línea 2 como tokens válidos. La fase que

debería reconocer este error (asumiendo que tener 2 paréntesis que cierran sin sus

correspondientes paréntesis que abren es un error estructural del programa fuente) es la fase de

análisis sintáctico (los detalles de esta fase los veremos en el capítulo 3). Mientras tanto,

revisaremos paso a paso las técnicas necesarias para poder construir la secuencia de arriba y así

poder llegar a codificar, como paso final de dicha secuencia, nuestro analizador léxico.

2.1 Definición de un reconocedor de cadenas no trivial

Antes de definir un reconocedor de cadenas no trivial, necesitamos algunos conceptos

que servirán de fundamento para construir nuestra conocida secuencia:

Expresión regular NFA DFA Programa

En primer lugar, debemos mencionar que las cadenas de caracteres representan bloques

de construcción fundamentales dentro de la Ciencia de la Computación. El alfabeto sobre el cual

dichas cadenas se encuentran definidas puede variar de aplicación en aplicación. Para nuestro

primer propósito (la construcción de un analizador léxico), definimos un alfabeto como un

conjunto finito no vacío de símbolos. En general, usamos las letras griegas y para designar

alfabetos como se muestra a continuación:

* + * +

Una cadena definida sobre un alfabeto es una secuencia finita de símbolos tomados de

ese alfabeto, usualmente escritos uno junto al otro y no separados por comas. Por ejemplo, si

es el alfabeto mostrado arriba, entonces 011101 es una cadena sobre dicho alfabeto. Si es el

alfabeto mostrado arriba también, entonces abracadabra es una cadena sobe ese alfabeto. Si w

es una cadena sobre , la longitud de dicha cadena es el número de símbolos que contiene y se

representa como . Es importante mencionar que la cadena que no contiene símbolos (es decir,

de longitud cero) se le llama cadena vacía y se escribe comúnmente o . Así que un lenguaje

es un conjunto de cadenas definidas sobre un alfabeto que cumplen cierta condición. Por

ejemplo, el lenguaje * + definido sobre el alfabeto * + contiene todas las cadenas de y que cumplan con la condición de que dichas cadenas

contengan al menos 2. Así que las cadenas abaaabb y bbbbaaaa son elementos del

lenguaje mientras que las cadenas bbbbb y bbbbbabbbb no lo son. Hay que notar que el

conjunto de cadenas pertenecientes al lenguaje es infinito. Para nuestro caso específico

(análisis léxico), el tipo de lenguaje que nos atañe es el de los lenguajes regulares. Los lenguajes

regulares pueden ser descritos usando expresiones regulares, lo que hace que podamos construir

nuestro analizador léxico usando nuestra conocida secuencia:

Expresión regular NFA DFA Programa

El teorema 2.1 asegura que podamos representar un lenguaje regular mediante una

expresión regular:

Page 23: Libro alumnos

Teorema 2.1: Un lenguaje es regular si y sólo si alguna expresión regular lo describe.

Aunque no demostraremos aquí dicho teorema, podemos apreciar que éste nos permite

pasar de una representación a otra con la seguridad de que ambas son equivalentes. El lector

interesado en la demostración puede consultar [ref. libro Sipser]. Antes de dar la definición

formal de una expresión regular, necesitamos definir las operaciones regulares de las que dicha

definición hace uso.

2.1.1 Las operaciones regulares

La siguiente definición y su respectivo ejemplo los tomamos de [ref. Sipser]. Sean A y

B lenguajes. Definimos las operaciones regulares unión, concatenación y estrella (Kleene)

como sigue:

UNION: }|{ BxAxxBA

CONCATENACIÓN: }|{ ByAxyxBA

ESTRELLA: } caday 0|{ 321

* AxkxxxxA ik

Ejemplo: Sea el alfabeto estándar de 26 letras },,,,,,{ zyxcba . Si

A { good, bad } y

},{ girlboyB entonces:

},,,{ girlboybadgoodBA

},,,{ badgirlbadboygoodgirlgoodboyBA

},,,,,,,,,{* odgoodgoodgodgoodgoodbabadbadbadgoodgoodbadgoodgoodbadgoodA Una vez definidas las operaciones regulares, podemos definir una expresión regular.

Dicha definición también está tomada de [ref. Sipser].

2.1.2 Definición formal de una expresión regular

Decimos que R es una expresión regular si R es:

1. a para cualquier a

2.

3.

4. )( 21 RR , donde 1R y 2R son expresiones regulares

5.

(R1 R2)

6. )( 1*R donde 1R es una expresión regular

Para el punto 1, cualquier elemento que pertenezca al alfabeto es una expresión regular.

En este caso, la expresión regular a representa el lenguaje a. Para el punto 2, la expresión

regular formada por la cadena vacía (representada por ) representa el lenguaje . Para el

punto 3, la expresión regular representa el lenguaje vacío. Es importante aclarar que la

expresión regular representa el lenguaje que contiene una sola cadena: la cadena vacía;

mientras que la expresión regular representa el lenguaje que no contiene ninguna cadena

(incluida la cadena vacía). Se deja como ejercicio al lector diseñar un autómata finito que acepte

el lenguaje representado por y el lenguaje representado por respectivamente. Para los puntos

4, 5, y 6, las expresiones regulares representan los lenguajes obtenidos al aplicar las operaciones

regulares de unión, concatenación y estrella respectivamente.

Page 24: Libro alumnos

A primera vista, la definición anterior parece ser una definición circular ya que parece

que definimos las expresiones regulares en términos de sí mismas. Sin embargo, las expresiones

regulares R1 y R2 son siempre más pequeñas que R, lo que nos permite evitar la circularidad en

la definición. A una definición de este tipo se le llama definición inductiva. Los paréntesis en las

expresiones regulares pueden omitirse: la evaluación entonces se hace usando la precedencia de

los operadores: estrella, concatenación y unión.

Una vez que se tienen los conceptos y definiciones anteriores, es posible entonces definir

un reconocedor de cadenas no trivial usando una expresión regular. Esto lo haremos en la

siguiente sección.

2.2 Programar sistemáticamente el reconocedor en lo referente a la obtención

del autómata, almacenarlo eficientemente y manejar adecuadamente el

archivo fuente

Para construir nuestro analizador léxico, debemos cubrir los siguientes pasos: a)

conversión de una expresión regular a un autómata finito no determinista (NFA), b) conversión

de un NFA a un autómata finito determinista (DFA), y c) codificación del DFA resultante en un

programa (pseudocódigo). Una vez que se tiene el programa en pseudocódigo es posible, sin

mayores complicaciones, la codificación de éste en un lenguaje de programación propiamente

dicho. En las siguientes secciones, describiremos a detalle cada uno de estos pasos.

2.2.1 Conversión de una expresión regular a un autómata finito no determinista

(NFA)

Antes de hacer la conversión propiamente dicha de una expresión regular a un NFA,

recordemos la definición de este tipo de autómata vista en el capítulo 1.

Un NFA es una 5-tupla ),,,,( 0 FqQ donde:

es un conjunto finito de estados

es un alfabeto finito

:Q P )(Q es la función de transición

Qq 0 es el estado inicial

F Q es el conjunto de estados de aceptación

Un ejemplo de un NFA aparece en la Fig. 20.

Q

Page 25: Libro alumnos

q1

q2

q3

b

a

a, b

a

ε

Fig. 20: Un NFA que reconoce a la cadena vacía o cadenas que tienen cualquier número de a´s

Analicemos cada una de las partes de este NFA.

1. Q = q1, q2, q3

2. = a,b

3. Revisemos con detenimiento la función de transición. Una función es un objeto que

define una relación de entrada-salida; i.e., una función recibe una cierta entrada y

produce una salida específica. El lado izquierdo de la flecha en la función de transición

es la entrada para esa función y el lado derecho de la flecha denota la salida. Así que la

función de transición toma como entrada un par ordenado cuyo primer elemento es un

elemento de Q y cuyo segundo elemento es un elemento de (i.e., ). Este conjunto

de pares ordenados está definido por el producto cartesiano, representado por Q ,

entre el conjunto de estados y el alfabeto (incluida la cadena vacía). Así que el producto

cartesiano de dos conjuntos, digamos Q y , es el conjunto de todos los pares ordenados

cuyo primer elemento pertenece a Q y cuyo segundo elemento pertenece a . Note que

el orden de los elementos de un par ordenado, a diferencia del orden de los elementos de

un conjunto, sí importa, por lo que en general, dados 2 conjuntos A y B, A B B A.

Ahora bien, la salida de la función de transición es un elemento del conjunto potencia del

conjunto de estados. El conjunto potencia de un conjunto A es el conjunto de todos los

subconjuntos de A. Para este caso específico, el conjunto potencia de Q, denotado (Q) =

, q1, q2, q3, q1,q2, q1,q3, q2,q3, q1,q2,q3. Con estas definiciones en

mano, podemos ya saber cuál es la salida de la función de transición para cada par

ordenado. Dado el NFA de la Fig. 20, las entradas y salidas correspondientes a dicha

función las representamos en la Tabla 3: Resultado de la función de transición para el

NFA de la Fig. 20

a b

q1 q2 q3

q2 q2,q3 q3

q3 q1

Tabla 3: Resultado de la función de transición para el NFA de la Fig. 20

Como se puede observar, el resultado de cualquier combinación estado-elemento del

alfabeto es un conjunto de estados que pertenece al conjunto potencia. Además, note que el

conjunto potencia nos permite representar el no-determinismo: por ejemplo, dado el estado q2 y

Page 26: Libro alumnos

una entrada a, el NFA nos permite quedarnos en ese estado o ir al estado q3 lo cual lo

representamos como el estado combinado q2,q3; o bien, el conjunto potencia nos permite

representar que no hay transición definida para el estado q1 y una entrada a, representándola

como .

4. El estado inicial q0 = q1

5. El conjunto de estados de aceptación F = q1

Para realizar la conversión de una expresión regular a su correspondiente NFA, necesitamos

la ayuda de 3 teoremas, los cuales presentamos a continuación [ref. Sipser].

Teorema 1: La clase de lenguajes regulares es cerrada bajo la operación de unión.

Sean 1A y 2A dos lenguajes regulares, queremos probar que 21 AA es regular. La idea

es tomar dos NFA‟s, 1M y 2M para 1A y 2A , respectivamente, y combinarlos en un nuevo NFA

que llamaremos M .

La máquina M debe aceptar una estrada si 1M o 2M aceptan esa entrada. La nueva

máquina tiene una nuevo estado inicial, con una transición al estado inicial de 1M y otra

transición al estado inicial de 2M . De esta manera la nueva máquina adivina no

deterministicamente cuál de las dos máquinas acepta dicha entrada. Si una de ellas acepta una

entrada M la aceptará también.

Representamos esta construcción en la Fig. 21. En la parte superior podemos ver a las

dos máquinas 1M y 2M , en cada una se encuentran el estado inicial, el o los estados finales (en

doble circulo) y algunos estado intermedios. La parte inferior muestra a la máquina M , la cual

contiene tanto a 1M como a 2M , además de tener un estado “adicional” que contiene dos

transiciones , una a 1M y otra a 2M .

Page 27: Libro alumnos

Fig. 21: Construcción de un NFA para reconocer 21 AA

Demostración

Sean ),,,,( 11111 FqQM que reconoce a 1A y ),,,,( 22222 FqQM que reconoce a

2A . Construyamos ),,,,( 0 FqQM para que reconozca a 21 AA

1. 210}{ QQqQ

Los estados de M son todos los estados de 1M y 2M , con la adición de un nuevo estado

0q .

2. El estado 0q es el estado inicial de M .

3. El conjunto de estados de aceptación 21 FFF .

Los estados de aceptación M son todos los estados de aceptación de 1M y 2M . De esta

manera M acepta si lo hacen 1M o 2M

4. Definimos de manera tal que para cualquier Qq y cualquier a .

aqq

aqqqq

Qqaq

Qqaq

aq

y

y },{

),(

),(

),(

0

021

22

11

Teorema 2: La clase de lenguajes regulares es cerrada bajo la concatenación.

Tenemos dos lenguajes regulares 1A y 2A queremos probar que 21 AA es regular. La

idea es tomar dos s'NFA , 1M y 2M para 1A y 2A , respectivamente, y combinarlos en un nuevo

Page 28: Libro alumnos

NFA que llamaremos M , como lo hicimos en el caso de la unión, pero ésta vez de una manera

un poco diferente, como se muestra en la Fig. 22.

Fig. 22: Construcción de M para reconocer 21 AA

Asignaremos a M en estado inicial

de 1M . Los estados de aceptación de

1M tendrán transiciones que

permitan no determinísticamente

“anclar” a 2M con 1M , es decir, dichas

transiciones irán de los estados finales

de 1M al estado inicial de 2M ; de esta

manera, cada vez que nos encontremos

en un estado de aceptación de 1M

significa que éste ha encontrado una

pieza inicial de la entrada que

constituye un carácter en 1A . Los

estados de aceptación de M serán sólo

los estados de aceptación de 2M . Por lo

tanto, M

Acepta una cadena cuando la entrada puede ser dividida en dos partes, la primera

aceptada por 1M y la segunda por 2M .

Demostración

Sean ),,,,( 11111 FqQM que reconoce a 1A y ),,,,( 22222 FqQM que reconoce

a 2A . Construyamos ),,,,( 0 FqQM para que reconozca a 21 AA .

1. 21 QQQ

Los estados de M son todos los estados de 1M y 2M .

2. El estado 1q es el estado inicial de 1M .

3. El conjunto de estados de aceptación 2FF .

Los estados de aceptación M son todos los estados de aceptación de 2M

4. Definimos de manera tal que para cualquier Qq y cualquier a .

22

121

11

111

),(

y}{),(

y),(

y),(

),(

Qqaq

aFqqaq

aFqaq

FqQqaq

aq

Teorema 3: La clase de lenguajes regulares es cerrada bajo la estrella de Kleene.

Page 29: Libro alumnos

Fig. 23: Construimos M para que reconozca

*A

Tenemos un lenguaje

regular 1A y lo modificamos

para que reconozca *

1A , como se

muestra en la figura 3.

Demostración

Sea ),,,,( 11111 FqQM que reconoce a 1A . Construyamos ),,,,( 0 FqQM para

que reconozca a *

1A .

1. 10}{ QqQ

Los estados de M son todos los estados de 1M mas un nuevo estado inicial.

2. El estado 0q es el nuevo estado inicial.

3. El conjunto de estados de aceptación 10}{ FqF .

Los estados de aceptación M son todos los estados de aceptación de 1M , con los que ya

contaba, más el nuevo estado inicial.

4. Definimos de manera tal que para cualquier Qq y cualquier a .

aqq

aFqqaq

aFqaq

FqQqaq

aq

y

y}{),(

y),(

y),(

),(

0

111

11

111

Una vez teniendo estos 3 teoremas, contamos con las herramientas necesarias para

convertir una expresión regular en su correspondiente NFA. A continuación presentamos un

ejemplo, paso a paso, de cómo realizar dicha conversión.

Convertir la siguiente expresión regular en su correspondiente NFA:

xyz *)(

1.1. Construimos los autómatas que reconocen a (Fig. 24), (Fig. 25) y (Fig. 26)

Page 30: Libro alumnos

Fig. 24: Autómata que

reconoce a z .

Fig. 25: Autómata que

reconoce a y

Fig. 26: Autómata que

reconoce a x

1.2. Construimos el autómata que reconoce a yz .

Siguiendo el Teorema 1,

agregamos un nuevo estado inicial,

2q en nuestro caso, y llevamos una

transición vacía al autómata que

reconoce a z , y otra transición vacía

al autómata que reconoce a (Fig.

27)

Fig. 27: Autómata que reconoce yz

Page 31: Libro alumnos

1.3. Construimos el autómata que reconoce *)( yz

Siguiendo el Teorema 3,

agregaremos un nuevo estado inicial

1q que además será un estado final.

Agregamos transiciones vacías de

todos los estados finales de yz al

estado inicial de yz ( 2q ), además

de una transición vacía de 1q a

(Fig. 28)

Fig. 28: Autómata que reconoce

*)( yz

Page 32: Libro alumnos

Compiladores Nicandro Cruz Ramírez

32

1.4. Finalmente, concatenamos el autómata anterior con el autómata que reconoce al carácter

x . Como se vio en el Teorema 2, debemos llevar los estados de aceptación de *)( yz

con el estado inicial del autómata que reconoce a x a través de transiciones . Como se

menciona en el teorema, los únicos estados de aceptación que existen son los de x (8q ),

y el estado inicial del nuevo autómata será el estado inicial de *)( yz , a saber (Fig.

29).

Fig. 29: Autómata que reconoce ( )

En los ejemplos que presentamos a continuación, construimos directamente un NFA a

partir de la expresión regular correspondiente. Quedan como ejercicios para el lector, la

construcción paso a paso de dichos NFA.

2. **)( xyz

Page 33: Libro alumnos

Compiladores Nicandro Cruz Ramírez

33

3. ** )( zyx

4. **100

5. 1001 .

6. ** 101 .

7. ababa *)(

Page 34: Libro alumnos

Compiladores Nicandro Cruz Ramírez

34

8. *0)10(

9. *)|( digitoletraletra .

2.2.2 Conversión de un autómata finito no determinista (NFA) a su correspondiente autómata finito determinista (DFA)

Para completar el recorrido de nuestra conocida secuencia,

Expresión regular NFA DFA Programa

nos hace falta convertir un NFA en su correspondiente DFA y éste a su vez codificarlo en forma

de programa. En esta sección revisamos las herramientas necesarias para convertir un NFA en su

correspondiente DFA. Para lograr esto, afortunadamente contamos con el siguiente teorema:

Teorema 4: Cada NFA tiene un DFA equivalente

Existen al menos 2 demostraciones que nos permiten pasar de un NFA a su

correspondiente DFA [ref. Sipser y Louden]. Aquí mencionaremos sólo una de ellas que se

conoce como construcción de subconjuntos [ref. Louden]. Antes de ver formalmente dicha

demostración, es importante mencionar que para que un DFA acepte las mismas cadenas que un

NFA, necesitamos una manera de eliminar tanto las transiciones como las transiciones

múltiples que caracterizan a los NFA de tal forma que éstas puedan representarse

Page 35: Libro alumnos

Compiladores Nicandro Cruz Ramírez

35

determinísticamente. Para recordar los elementos y propiedades de un DFA vistos en el capítulo

anterior, escribimos nuevamente la definición de un DFA.

Un DFA es una 5-tupla ),,,,( 0 FqQ donde:

es un conjunto finito llamado estados

es un conjunto finito llamado alfabeto

QQ : es la función de transición

q0 Q es el estado inicial

F Q es el conjunto de estados de aceptación

Como podemos observar, la función de transición de un DFA, a diferencia de la de un

NFA, nos permite ir, dados un estado y un símbolo del alfabeto, a uno y sólo un estado. Así que

la pregunta es: ¿cómo podemos eliminar las transiciones y las transiciones múltiples que se

presentan en los NFA?

Primero que nada necesitamos definir la cerradura de un conjunto de estados. La

siguiente definición la tomamos de [ref. Louden]. La cerradura de un estado simple s es el

conjunto de estados alcanzables por una serie de cero o más transiciones . A este conjunto lo

denotamos como

s . Para ejemplificar más claramente este concepto de cerradura, usamos la

figura 9, la cual es un NFA que representa la expresión regular a*.

Fig. 30: Ejemplo de un NFA

La cerradura del conjunto de estados del NFA de la Fig. 30 se muestra a continuación:

q 1 {q1, q2, q4}

q 2 {q2}

q 3 {q2, q3, q4}

q 4 {q4}

Para este ejemplo específico, la cerradura de q1 es el conjunto de estados a los que se

puede llegar desde q1 con cero o más transiciones . Siguiendo estos mismos pasos, podemos

entonces encontrar la cerradura de q2, q3 y q4 como se muestra arriba.

Ahora definimos la cerradura ya no de un solo estado sino de un conjunto de estados

como sigue:

S { UsS

s }

Donde S es un conjunto de estados. Por ejemplo, para el NFA de la Fig. 30:

{ q 1q 3} q 1q 3 {q1, q2, q4}{q2, q3, q4}{q1, q2, q3, q4}

Una vez que se tienen estas definiciones, es posible describir el procedimiento para la

construcción de un DFA M a partir de un NFA N.

Q

Page 36: Libro alumnos

Compiladores Nicandro Cruz Ramírez

36

PASO 1: Calcular la cerradura del estado inicial de N (consideramos el NFA de la Fig. 30):

q 1 {q1, q2, q4}

PASO 2: Calcular para todo s S y para toda a

S'a {t | para cualquier s S existe una transición de s a t con a}

En otras palabras, el conjunto

S'a es un conjunto de estados que cumplen con la

condición de pertenecer a S y de tener una transición hacia cualquier otro estado t con cualquier

elemento del alfabeto a. Para nuestro ejemplo en particular, consideremos el estado inicial de

nuestro NFA:

S'a {q1,q2,q4}a =

{q3}

Es decir, consideramos todos los estados a los que se pueden llegar desde q1, q2 y q4 con

a. Como puede observarse, el único estado al que se puede llegar con a desde este conjunto de

estados es q3.

PASO 3: Calculamos

S 'a : la cerradura de

S'a . Esto define un nuevo estado para el DFA junto

con una nueva transición

S'a

S 'a con a . En nuestro ejemplo:

S 'a {q1,q2,q4}a =

{q3}=

{q2,q3,q4}

Este paso se aplica repetidamente a cada nuevo estado creado hasta que ya no se crean

nuevos estados o transiciones. Además, los estados de aceptación de este DFA resultante son

aquéllos que contengan en cualquiera de sus estados un estado de aceptación del NFA original.

Como se puede apreciar, el DFA resultante no contiene transiciones ya que todo estado

en este DFA se construye como una cerradura . Además, la función de transición es

determinista pues el procedimiento nos asegura que existe uno y sólo un estado al que ir desde

cualquier estado con un elemento específico del alfabeto.

Para ilustrar mejor lo anterior, tomemos nuevamente de ejemplo el NFA mostrado en la Fig.

30. Debemos transformarlo a su correspondiente DFA siguiendo los pasos anteriores y tomando

en cuenta que }{a .

1. Obtengamos la cerradura de cada uno de los estados:

q 1 {q1, q2, q4}

q 2 {q2}

q 3 {q2, q3, q4}

q 4 {q4}

2. El estado inicial de nuestro DFA será la cerradura del estado inicial del NFA

(cerradura de 1q ) , como se muestra en la Fig. 31. En este momento verificamos si

alguno de los estados de 1q es un estado de aceptación en el DFA, de ser así también lo

será 1q en el NFA. Para este ejemplo 4q es estado de aceptación del DFA y como

14 qq , entonces 1q será estado de aceptación.

Page 37: Libro alumnos

Compiladores Nicandro Cruz Ramírez

37

Fig. 31: Estado inicial del DFA 1q

3. Para definir el segundo estado en el DFA (Fig. 32), verificamos para cada estado de 1q si

existen transiciones “no vacías” en el NFA a otros estados con cada uno de los elementos

de ; es decir, },,{ 4211 qqqq , verificamos si en 1q existe alguna transición distinta de

hacia algún estado en el NFA, cómo esto no sucede seguimos con el siguiente estado

de 1q . Ahora verificaremos si 2q tiene alguna transición distinta de épsilon hacia algún

estado en el NFA, en este caso si existe una transición diferente de y es aquella que va

del estado 2q a 3q a través de una a . Por último, verificamos si 4q tiene alguna

transición diferente de hacia algún estado en el NFA, lo cual no sucede. Por lo tanto,

nuestro siguiente estado en el DFA será 3q a través de una transición con a . Si más

estados hubieran resultado de ir de un estado a otro con transiciones diferentes de con

a , entonces la unión de las cerraduras de todos esos estados hubiera sido el siguiente

estado en el DFA. Nuevamente, verificamos si alguno de los elementos de 3q es un

estado de aceptación en el NFA también lo será 3q en el DFA.

Fig. 32: Segundo estado del DFA

4. Verificamos las transiciones existentes en el NFA con los elementos de },,{ 4323 qqqq .

Realizamos los mismos pasos que en el inciso anterior y observamos que sólo existe una

transición en el NFA diferente de que es de 2q a 3q con a . Así el siguiente estado

será de 3q a 3q con (Fig. 33).

Fig. 33: Siguiente estado del DFA

5. Aquí termina la construcción del DFA, pues ya no existen nuevos estados que agregar o

nuevas transiciones.

Hagamos otro ejercicio para reforzar los conceptos involucrados en la transformación de un

NFA en un DFA.

Transforme el NFA de la Fig. 34 en su respectivo DFA, con },,{ zyx

Page 38: Libro alumnos

Compiladores Nicandro Cruz Ramírez

38

Fig. 34: NFA correspondiente a xyz *)(

1. Obtenemos las cerraduras para cada estado del NFA.

},,,,{ 753211 qqqqqq },,{ 5322 qqqq }{ 33 qq

},,,,{ 754324 qqqqqq }{ 55 qq },,,,{ 765326 qqqqqq

}{ 77 qq }{ 88 qq

Dibujamos el estado inicial del DFA que será 1q (Fig. 35).

Fig. 35: Estado inicial del DFA

2. Verificamos las transiciones de cada elemento de 1q con cada elemento del alfabeto .

2.1. Verificamos si los elementos de 1q tienen transiciones a otros estados en el NFA para

x , en este caso sólo 7q va a 8q con x , por lo tanto }{ 88 qq será el estado 2 del

DFA (Fig. 36). Por ser 8q estado de aceptación en el NFA entonces también lo será en

el DFA

Fig. 36: Nuevo estado del DFA generado

por la transición x del NFA en 1q

2.2. Verificamos si los elementos de 1q tienen transiciones a otros estados en el NFA para

y , en este caso sólo 5q va a 6q con y , por lo tanto },,,,{ 765326 qqqqqq será el

estado 3 del DFA (Fig. 37).

Page 39: Libro alumnos

Compiladores Nicandro Cruz Ramírez

39

Fig. 37: Estado 3 del DFA

2.3. Verificamos si los elementos del estado 1 del DFA tienen transiciones a otros estados en

el NFA para z , en este caso sólo 3q va a 4q con y , por lo tanto

},,,,{ 754324 qqqqqq será el estado 4 del DFA (Fig. 38).

Fig. 38: Estado 4 del DFA

3. Como ya no existen elementos en el alfabeto, realizamos el mismo proceso para el estado 2

del DFA. Como podemos observar sólo tiene un elemento, 8q , verificamos en el NFA si 8q

tiene alguna transición a otro estado para x , no la hay entonces creamos una transición a

un estado (estado 5) que nos indica error. Hacemos lo mismo para y , pero

nuevamente no hay más transiciones, igual sucede con z ; por lo tanto, se crean

transiciones hacia el estado para y (Fig. 39).

Fig. 39: Agregación de estado de ERROR

4. Como ya no existen elementos en el alfabeto, realizamos el mismo proceso para el estado 3

del DFA.

5.1. Verificamos los elementos del estado 3 que tienen transiciones a otros elementos en el

NFA para x , sólo 7q tiene una transición a 8q con x , por lo tanto, el DFA va a 8q ,

que es el estado 2 (Fig. 40).

Page 40: Libro alumnos

Compiladores Nicandro Cruz Ramírez

40

Fig. 40: Transición del estado 3 al estado 2

5.2. Hacemos el procedimiento anterior pero ahora para y ; sólo 5q tiene una transición

a 6q con x , por lo tanto, el DFA va a 6q , que es el estado 3 (Fig. 41)

Fig. 41: Transición del estado 3 a él mismo

5.3. Finalmente verificamos para z . Sólo 3q tiene una transición a 4q con x , por lo

tanto, el DFA va a 4q , que es el estado 4 (Fig. 42).

Fig. 42: Transición del estado 3 al estado 4

5. Como ya no existen elementos en el alfabeto, realizamos el mismo proceso para el estado 4

del DFA.

6.1. Verificamos los electos del estado 4 que tienen transiciones a otros elementos en el NFA

para x , sólo 7q tiene una transición a 8q con x , por lo tanto, el DFA va a 8q , que

es el estado 2 (Fig. 43).

Page 41: Libro alumnos

Compiladores Nicandro Cruz Ramírez

41

Fig. 43: Transición del estado 4 añ estado 2

6.2. Hacemos el procedimiento anterior pero ahora para y ; sólo 5q tiene una transición

a 6q con x , por lo tanto, el DFA va a 6q , que es el estado 3.

Fig. 44: Algún título

6.3. Finalmente verificamos para z . Sólo 3q tiene una transición a 4q con x , por lo

tanto, el DFA va a 4q , que es el estado 4 (Fig. 45).

Fig. 45: Transición del estado 4 a él mismo

Aquí ha terminado la construcción del DFA

2.2.3 Codificación de un DFA en pseudocódigo

Para terminar nuestro recorrido por la conocida secuencia,

Expresión regular NFA DFA Programa

Page 42: Libro alumnos

Compiladores Nicandro Cruz Ramírez

42

nos hace falta codificar el correspondiente DFA en forma de programa (pseudocódigo). Aquí

mostramos 2 diferentes maneras de hacerlo. Para la primera, podemos escribir pseudocódigo

directamente del DFA correspondiente. Consideremos el DFA de la ¡Error! No se encuentra el

origen de la referencia. que reconoce un nombre de variable o identificador válido así como su

pseudocódigo correspondiente (ver Tabla 4):

Fig. 46: DFA representando la sintaxis de un nombre de variable (identificador)

Como podemos apreciar, es posible escribir rutinas (programa) a partir de un DFA. Sin

embargo, el código que se genera a partir de este diagrama de transiciones no representa

necesariamente una solución óptima al problema de codificación. Esto es debido a que, para

cada estado, sus posibles opciones de transición se manejan con estructuras condicionales

anidadas lo que hace que el programa crezca significativamente en función del número de

estados y el número de elementos en el alfabeto. Es principalmente por esta razón que se

propone una mejor solución basada en el uso de tablas de transición: esta es la segunda manera

de escribir código a partir de un NFA. Un ejemplo de esto se muestra en la Tabla 5 que toma

como entrada la tabla de transición de la Tabla 4. Es importante mencionar que la Tabla 4 se

construyó a partir del DFA de la figura Fig. 46. EOS en la Tabla 5 significa fin de cadena (end-

of-string).

Page 43: Libro alumnos

Compiladores Nicandro Cruz Ramírez

43

Estado := 1;

LEER(siguiente Símbolo de entrada);

MIENTRAS ( ! FinDeCadena ) HACER

CASE Estado DE

1: SI Símbolo = letra ENTONCES

Estado := 3;

SI NO

SI Símbolo = dígito

ENTONCES

Estado := 2;

SI NO

Salir a RutinaError

FIN-SI

FIN-SI

2: Salir a RutinaError

3: SI Símbolo = letra ENTONCES

Estado := 3;

SI NO

SI Símbolo = dígito

ENTONCES

Estado := 3;

SI NO

Salir a RutinaError

FIN-SI

FIN-SI

FIN-case

LEER(siguiente Símbolo de entrada)

FIN-MIENTRAS

SI Estado ! = 3 ENTONCES

Salir a RutinaError;

Tabla 4: Secuencia de instrucciones sugerida por el diagrama de transición de la Fig. 46.

letra número EOS

1 3 2 Error

2 Error Error Error

3 3 3 ACCEPT

Tabla 5: Tabla de transición construida del diagrama de transición de la figura 9.

Page 44: Libro alumnos

Compiladores Nicandro Cruz Ramírez

44

Estado := 1;

REPETIR

LEER(siguiente Símbolo de entrada);

CASE Símbolo DE

letra : Entrada := “letra”;

dígito: Entrada := “dígito”;

MarcadorDeFinDeCadena: Entrada := “EOS”;

NingunoDeLosAnteriores: Salir A RutinaError;

FIN-CASE

Estado := Tabla [Estado, Entrada] ;

SI Estado = Error ENTONCES

SalirRutinaError;

FIN-SI

HASTA Estado = “ACCEPT”

Tabla 6: Análisis léxico basado en la Tabla 4 de transiciones

Page 45: Libro alumnos

Compiladores Nicandro Cruz Ramírez

45

zMNzS a M aM zM

b N bN zN

3. Análisis sintáctico

Verificar si la cadena z az abz bzes generada por la gramática mostrada en el

cuadro de la izquierda.

1. Comenzamos escribiendo la regla perteneciente a la

variable inicial: NzM zS

2. Aplicamos, para M la regla a M aM aNzMza

3. Aplicamos, para M la regla zM zNzaza

4. Aplicamos, para N la regla b N bN

bzNzazab

5. Aplicamos, para N la regla zN z a z a b z b z

Un autómata de pila (PDA – Push Down Automaton) es una 6-tupla ),,,,,( 0 FqQ

donde Q , , y F son todos conjuntos finitos y:

1. Q es el conjunto finito de estados.

2. es el alfabeto de entrada.

3. es el alfabeto de la pila.

4. Q: P )( Q es la función de transición.

5. Qq 0 es el estado inicial.

6. QF es el conjunto de estados de aceptación.

Recuerde que }{ y }{

q1

q4

ε, ε $

1, 0 ε

ε, $ ε

0, ε 0

1, 0 ε

q2

q3

Fig. 47: PDA que reconoce lenguajes del tipo

Una gramática libre de contexto (CFG, Context Free Grammar) es una 4-tupla ),,,( SRV

donde:

1. V es un conjunto finito llamado las variables.

2. es un conjunto finito llamado los terminales

Page 46: Libro alumnos

Compiladores Nicandro Cruz Ramírez

46

3. R es un conjunto finito de reglas, con cada regla siendo una variable y una cadena de

variables y terminales.

4. VS es la variable inicial.

Teorema: Para cada CFG existe un PDA M tal que )()( MLGC

Demostración

Dada una CFG construimos un PDA M como sigue:

1. Designe el alfabeto de M como los símbolos terminales de G y los símbolos de la pila

como los terminales y no terminales de G junto con el símbolo especial # (asumimos que

# no es ni terminal ni no-terminal en G).

2. Designe los estados de M como qpq ,,0 y f , siendo 0q el estado inicial y f el único

estado de aceptación.

3. Introduzca la transición )#,;,,( 0 pq .

4. Introduzca una transición ),;,,( Sqp , donde S es el símbolo inicial en G.

5. Introduzca una transición de la forma ),;,,( wqNq para cada regla de reescritura

wN en G (aquí estamos usando nuestra convención que permite a una transición

simple meter más de un símbolo a la pila. En particular, w puede ser una cadena de cero

o más símbolos incluyendo terminales y no terminales).

6. Introduzca una transición de la forma ),;,,( qxxq para cada Terminal x en G (es

decir, para cada símbolo en el alfabeto de M).

7. Introduzca la transición ),;#,,( fq .

Veamos el teorema anterior aplicado a la siguiente gramática:

zN

bNbN

zM

aMaM

zMNzS

1. Sea }#,,,,,,{ bazNMS el alfabeto.

2. Designamos los estados qpq ,,0 y f , siendo 0q el estado inicial y f el único estado de

aceptación (Fig. 48).

Fig. 48: Estados del PDA

3. Introducimos la transición )#,;,,( 0 pq , es decir, una transición de 0q a p que tenga

como entrada el par ),( y como “salida” el símbolo # que será introducido a la pila

(Fig. 49).

Page 47: Libro alumnos

Compiladores Nicandro Cruz Ramírez

47

Fig. 49: Introducción de la primera transición )#,;,,( 0 pq

4. Introducimos la transición ),;,,( Sqp , donde S es el símbolo inicial en G, es decir,

una transición de p a q , que tiene como entrada el par ),( y como salida el símbolo

S , que será metido a la pila (Fig. 50).

Fig. 50: Introducción de la segunda transición ),;,,( Sqp

5. Introducimos una transición de la forma ),;,,( wqNq para cada regla de reescritura

wN en G. Para nuestro ejemplo, introduciremos las transiciones ),;,,( zMNzqSq ,

),;,,( aMaqMq , ),;,,( zqMq , ),;,,( bNbqNq , ),;,,( zqNq (Fig. 51).

Fig. 51: Introducir transiciones por cada regla de producción

6. Introducimos una transición de la forma ),;,,( qxxq para cada Terminal x en G, en

nuestro caso, los terminales son a , b y z (Fig. 52).

Fig. 52: Introducir una transición por cada símbolo terminal

7. Introducir la transición ),;#,,( fq , es decir, la transición que une al estado q con el

estado f (Fig. 53).

Page 48: Libro alumnos

Compiladores Nicandro Cruz Ramírez

48

Fig. 53: PDA para la gramática dada.

De esta manera hemos comprobado que para cada CFG existe un PDA M tal que

)()( MLGC

Una tabla parse para un parser LL(1) es un arreglo bidimensional. Los renglones se

etiquetan con los no terminales de la gramática y las columnas con los terminales de la gramática

más una columna adicional llamada EOS (End Of String).

La ),( nm -ésima entrada de la Tabla 7 indica que acción debe llevarse a cabo cuando el

no-terminal m aparece hasta arriba de la pila y el símbolo hacia delante es n .

zN

bNbN

zM

aMaM

zMNzS

Fig. 54: Gramática

a b z EOS

S ERROR ERROR zMNz ERROR

M aMa ERROR Z ERROR

N ERROR bNb z ERROR

Tabla 7: Tabla parse LL(1) para la gramática de la izquierda

push (s)

read (symbol)

while (snack_not_empty) do

case top_of_stack of

terminal: if top_of_stack = symbol then

pop stack and read (symbol.)

else

exit_to_error_routine;

non-terminal: if table[top_of_stack, symbol] ≠

error then

replace top_of_stack with

table[top_of_stack, symbol]

else

exit_to_error_routine;

end-case

end-while

if symbol not end_of_string marker then

exit_to_error_routine

Tabla 8: Rutina parse LL(1) genérica

Page 49: Libro alumnos

Compiladores Nicandro Cruz Ramírez

49

1. Ejercicio 1: Dibujar el PDA correspondiente a la gramática:

S

ySxS (Fig. 55)

Fig. 55: PDA del Ejercicio 1

2. Ejercicio 2: Dibujar el PDA correspondiente a la gramática:

S

zSyS

zSxS

(Fig. 56)

Fig. 56: PDA del ejercicio 2

3. Ejercicio 3: Dibujar el PDA correspondiente a la gramática:yxS

ySxS

(Fig. 57)

Fig. 57: PDA del ejercicio 3

Teorema: Para cada CFG existe un PDA M tal que L(G) = L(M)

Demostración

1. Establecer cuatro estados, un estado inicial llamado 0q , un estado final llamado f y

otros dos estados p , q .

2. Introduzca la transiciones )#,;,,( 0 pq y ),;#,,( fq , donde asumimos que # es

un símbolo que no ocurre en la gramática.

Page 50: Libro alumnos

Compiladores Nicandro Cruz Ramírez

50

3. Para cada símbolo Terminal x de la gramática, introduzca la transición ),;,,( xpxp .

Estas transiciones permiten al autómata transferir los símbolos de entrada a la pila,

mientras que permanece en el estado p . La ejecución de esta operación de llama

operación de cambio (shift operation), ya que su efecto es cambiar un símbolo de la

entrada a la pila.

4. Para cada regla de reescritura wN (donde w representa una cadena de 1 o más

símbolos) de la gramática, introduzca la transición ),;,,( Npwp (aquí permitimos a

una transición remover más de un símbolo de la pila). Así que para ejecutar la transición

),;,,( zpyxp

un autómata debe tener una y hasta arriba de la pila con una x debajo

de ella. La presencia de éstas transiciones significa que si los símbolos de la parte de más

arriba de la pila concuerdan con el lado derecho de una regla de reescritura entonces

dichos símbolos pueden reemplazarse con el único no-terminal del lado izquierdo de esa

regla. La ejecución de tal transición se llama operación de reducción (reduce operation)

ya que su efecto es el de reducir el contenido de la pila a una forma más simple.

5. Introduzca la transición ),;,,( qSp donde S es el símbolo inicial de la gramática.

Veamos el teorema anterior aplicado a la siguiente gramática:

zN

bNbN

zM

aMaM

zMNzS

1. Establecer cuatro estados, un estado inicial llamado 0q , un estado final llamado f y

otros dos estados p , q (Fig. 58).

Fig. 58: Establecimiento de 4 estados

2. Introducir las transiciones )#,;,,( 0 pq y ),;#,,( fq , donde asumimos que # es

un símbolo que no ocurre en la gramática (Fig. 59).

Fig. 59: Primeras dos transiciones

3. Para cada símbolo Terminal x de la gramática, introduzca la transición ),;,,( xpxp .

La ejecución de esta operación de llama operación de cambio (shift operation), ya que

su efecto es cambiar un símbolo de la entrada a la pila (Fig. 60).

Page 51: Libro alumnos

Compiladores Nicandro Cruz Ramírez

51

Fig. 60: Una transición por cada símbolo terminal

4. Para cada regla de reescritura wN (donde w representa una cadena de 1 o más

símbolos) de la gramática, introduzca la transición ),;,,( Npwp (Fig. 61)

Fig. 61: Una transición por cada regla gramatical

5. Introducir la transición ),;,,( qSp donde S es el símbolo inicial de la gramática (Fig.

62).

Fig. 62: Última transición

Page 52: Libro alumnos

Compiladores Nicandro Cruz Ramírez

52

3.1 Construcción de tablas parse LR(1)

zN

bNbN

zM

aMaM

zMNzS

Fig. 63: Gramática

1. Introducir un nuevo símbolo inicial 'S y una nueva regla SS ' .

2. Introducimos el marcador ^ para indicar el estatus del proceso de

análisis sintáctico. Por ejemplo, usando el marcador podemos

escribir: MNzzS ^ para indicar que se ha leído la primera z y se

prepara para leer el resto de la regla: MNz

terminalForma^

^

^

^

inicial Forma^

zMNzS

zzMNS

NzzMS

MNzzS

zMNzS

3. Cerradura de un conjunto de reglas marcadas. Formamos dicha cerradura al encontrar

primero todos los no-terminales que aparezcan de inmediato a la derecha de un marcador en

alguna regla del conjunto.

Entonces añadimos al conjunto las formas iniciales de todas las reglas de la gramática

cutos lados izquierdos consistan en esos no-terminales. Si algunas de estas reglas añadidas

tienen no-terminales que aparezcan inmediatamente a la derecha de un marcador, agregamos

las formas iniciales de esas reglas para esos no-terminales también. Continuamos éste

proceso hasta que algún no-terminal nuevo aparezca inmediatamente a la derecha de un

marcador.

¿Cuál es la cerradura del conjunto que contiene a las 2 reglas MaaM

NzzMS

^

^

?

Respuesta: zMMaaMzNbNbNMaaMNzMS ,^,^,^,^,^

Algoritmo para construir el FA que servirá de base para la tabla parse LR(1)

1. Forme la cerradura del conjunto que contiene solamente la regla marcada '^SS .

Establezca este conjunto como el estado inicial del diagrama de transición.

2. Mientras sea posible, sin redundancia haga lo siguiente:

a. Seleccione un símbolo s (Terminal o no-terminal) que aparezca inmediatamente a la

derecha del marcador en una regla de algún estado A .

b. Sea X la colección de todas las reglas marcadas en A que tengan s inmediatamente

a la derecha de sus marcadores.

c. Sea Y el conjunto de reglas marcadas obtenidas al mover el marcador de cada regla

en X a la derecha del símbolo s .

d. Si la cerradura de Y no se ha establecido como estado, hágalo.

e. Dibuje un arco etiquetado como s desde A hacia la cerradura de Y .

3. Sea cada estado representado por reglas marcadas en su forma Terminal, un estado de

aceptación en el autómata.

Page 53: Libro alumnos

Compiladores Nicandro Cruz Ramírez

53

Ejercicio: Construir el AF para la gramática yS

zSxS

1. Introducimos un nuevo símbolo inicial 'S y una nueva regla SS ' , así como un

marcador que indique la forma inicial de cada regla gramatical.

yS

zSxS

SS

^

^

^'

2. Formamos la cerradura de la regla SS ^' , ésta servirá como primer estado (Fig. 64).

Fig. 64: Estado inicial del FA cerradura de SS '

3. Seleccionamos Ss , con lo cual }^'{ SSX y }^'{ SSY , observamos que

^' SS se encuentra en su forma Terminal, con lo cual YY . Agregamos ese nuevo

estado (estado 2) y una transición a él con el símbolo S (Fig. 65).

Fig. 65: Segundo estado

4. Escogemos para el estado 1 xs , con lo cual }^{ zSxSX y }^{ zSxSY .

Obtenemos la cerradura de Y .

},^,^{ ySzSxSzSxSY

Como no existe un estado con esas reglas gramaticales se agrega al AF (estado 3), junto

con una transición de 1 a 3 para el símbolo x (Fig. 66).

Page 54: Libro alumnos

Compiladores Nicandro Cruz Ramírez

54

Fig. 66: Tercer estado

5. Sea ys , el último símbolo para el estado 1. Por lo que }^{ ySX , }^{ ySY ,

con lo que YY . Como Y no se encuentra en el AF lo agregamos (estado 4). Como

además la regla se encuentra en su estado Terminal, el estado 4 será de aceptación (Fig.

67).

Fig. 67: Estado 4 del AF

6. Como ya no existen más símbolos en el estado 1 y el estado 2 ya es un estado de

aceptación, hacemos el mismo proceso para el estado 3.

Sea Ss , con lo cual }^{ zSxSX y }^{ zSxSY . Debido a que no

existen reglas de producción que comiencen con símbolos terminales YY . Como Y no

existe como estado en el AF se agrega (estado 5), junto con una transición del estado 3 al

estado 5 con el símbolo S (Fig. 68).

Fig. 68: Estado 5 del AF

7. Sea xs , con lo cual }^{ zSxSX y }^{ zSxSY . De lo cual

}^,^,^{ ySzSxSzSxSY que es el mismo estado 3. Con lo cual, sólo

agregamos una transición a él mismo con el símbolo x (Fig. 69).

Page 55: Libro alumnos

Compiladores Nicandro Cruz Ramírez

55

Fig. 69: Transición x

8. Sea ys , con lo cual }^{ ySX y YySY }^{ que es el estado 4, con lo

cual, agregamos una transición del Estado 3 al estado 4 con el símbolo y (Fig. 70).

Fig. 70: Transición del estado 3 al estado 4 con

9. Como ya no existen más símbolos en el estado 3 y el estado 4 ya es un estado de

aceptación, hacemos el mismo proceso para el estado 5.

Sea zs , con lo cual }^{ zSxSX y YzSxSY }^{ . Como no

existe Y en el AF se agrega como estado (estado 6), junto con una transición con el

símbolo z . Como además es una regla en su estado terminal, el estado 6 es un estado de

aceptación (Fig. 71).

Fig. 71: Ultimo estado del AF

Page 56: Libro alumnos

Compiladores Nicandro Cruz Ramírez

56

3.2 Análisis sintáctico LALR(1)

Este análisis está basado en la observación de que en muchos casos el tamaño del DFA

de conjuntos de elementos LR(1) se debe en parte a la existencia de muchos estados diferentes

que tienen el mismo conjunto de primeros componentes en sus elementos, mientras que difieren

sólo en sus segundos componentes (los símbolos de búsqueda hacia adelante).

El algoritmo de análisis sintáctico LALR(1) expresa el hecho de que tiene sentido

identificar todos esos estados y combinar sus búsquedas hacia delante. Al hacerlo así, siempre

debemos finalizar con un DFA que sea idéntico el DFA de los elementos LR(0) excepto si cada

estado se compone de elementos con conjuntos de búsqueda hacia delante.

Considere la siguiente gramática: aAA |)(

Fig. 72: LR(0)

Fig. 73: Otra figura

3.2.1 Primer principio del análisis sintáctico LALR(1)

El núcleo de un estado del DFA de elementos LR(1) es un estado del DFA de elementos

LR(0).

3.2.2 Segundo principio del análisis sintáctico LALR(1)

Dados dos estados 1s y 2s del DFA

de elementos LR(1) que tengan el mismo

núcleo, suponga que hay una transición con

el símbolo x desde 1s a un estado 1t .

Entonces existe también una transición con

x del estado 2s al estado 2t , y los estados 1t

y 2t tienen el mismo núcleo.

Considere la siguiente gramática

aAA |)( cuyo DFA es:

Page 57: Libro alumnos

Compiladores Nicandro Cruz Ramírez

57

Fig. 74: DFA de aAA |)(

A a ( ) $

0 1 3 2

1

2

3 4 2 3

4 5

5

Tabla 9: Tabla LALR(1)

Algún texto

Fig. 75: LR(1)

A a ( ) $

0 1 3 2

1 ACCEPT

2 4 6 5

3 Aa

4 Aa,)

5 8 6 5

6 Aa,)

7 A(A) A(A)

8 8

9 A(A)

Tabla 10: Tabla LR(1)

3.3 Análisis sintáctico LR(1) canónico

3.3.1 Autómatas finitos de elementos LR(1)

La dificultad con el métodos SLR(1) es que aplica las búsquedas hacia delante después

de construir el DFA de elementos LR(0), una construcción que ignora búsquedas hacia adelante.

La potencia del método LR(1) general (canónico) se debe a que utiliza un nuevo DFA que tiene

las búsquedas integradas en su construcción desde el inicio.

Este DFA utiliza elementos que son una extensión de los elementos LR(0) se conocen

como elementos LR(1), es un par compuesto de un elemento LR(0) un token de búsqueda hacia

adelante simple en cada elemento. Más exactamente, un elemento LR(1) es un par compuesto de

un elemento LR(0) y un token de búsqueda hacia adelante. Escribimos elementos LR(1)

utilizando corchetes como:

],[ aA

Donde A es un elemento LR(0) y a es un token (la búsqueda hacia adelante)

Page 58: Libro alumnos

Compiladores Nicandro Cruz Ramírez

58

Para completar la definici+on del autómata utilizado para el análisis sintáctico LR(1)

general, necesitamos definir las transiciones LR(0), excepto por que también se mantienen al

tanto de las búsquedas hacia delante. Como con los elementos LR(0) que incluyen transiciones

, es necesario construir un DFA cuyos estados sean conjuntos de elementos que sean

cerraduras . La diferencia principal entre los autómatas LR(0) y LR(1) viene en la definición

de las transiciones . Daremos primero la definición del caso más fácil (las transiciones no ),

que son esencialmente idénticas a las correspondientes al caso LR(0).

3.3.3 Definición de transiciones LR(1) (parte 1)

Dado un elemento LR(1) ],[ aXA donde X es cualquier símbolo (Terminal o

no-terminal), existe una transición con X al elemento ],[ aXA .

Observe que en este caso la misma a de búsqueda hacia adelante aparece en ambos

elementos. De este modo, estas transiciones no provocan la aparición de nuevas búsquedas hacia

adelante. Sólo las transiciones “crean” nuevas búsquedas hacia delante de la manera siguiente:

Dado un elemento LR(1) ],[ aBA , donde B es un no Terminal, existen

transiciones para elementos ],[ bB para cada producción B y para cada

token b en )(Pr aimero

Un elemento LR(0) de una CFG es una regla de producción con una posición distinguida

en su lado derecho. Indicaremos esta posición distinguida mediante un punto. De este

modo si A es una regla de producción, y si y son dos cadenas cualesquiera de

símbolos (incluyendo ), tales como , entonces A es un elemento LR(0).

Estos se conocen como elementos LR(0), por que no contienen referencia explícita a la

búsqueda hacia delante.

Considere la siguiente CFG

Fig. 76: Algún título

S

SSS

SS

)(

'

SSSS

SSSSSS

SSSSSS

SSSS

)(

)()(

)()(

''

Page 59: Libro alumnos

Compiladores Nicandro Cruz Ramírez

59

Fig. 77: Algún título

3.4 Conjuntos primero

Si x es un símbolo de la gramática (terminal o no-terminal) o , entonces el conjunto

)(xprimero compuesto de terminales y posiblemente de , se define de la siguiente manera:

1. Si x es un terminal o , entonces }{)( xxprimero

2. Si x es un no-terminal entonces, para cada selección de producción nxxxx ,,, 21 ,

)(xprimero contiene }{)( 1 xprimero . Si también para cualquier ni , todos los

conjuntos )(,),( 1 nxprimeroxprimero , )( 1xprimero contiene , entonces )(xprimero

contiene }{)( 1 ixprimero . Si todos los conjuntos )(,),( 1 nxprimeroxprimero

contienen , entonces )(xprimero también contiene

Ahora definamos )(primero para cualquier cadena nxxx 21 (una cadena de

terminales y no-terminales) de la siguiente manera.

)(primero contiene }{)( 1 xprimero para cada ni ,,2 . Si )( kxprimero contiene

para toda 1,,1 ik , entonces )(primero contiene }{)( ixprimero .

Finalmente, si para toda ni ,,1 , )( ixprimero contiene , )(primero contiene .

Algoritmo simplificado para calcular )(Aprimero para todos los no-terminales A en

ausencia de producciones (Tabla 11).

for todos_los_no_terminales_A do

{};:)( Aprimero

while existan_cambios_para_cualquier_primero(A) do

for cada_seleccion_de_producción nxxxA 21 do

Agregue )( 1xprimero a )(Aprimero ;

Tabla 11: Algoritmo

Calcular los conjuntos )(Aprimero para la siguiente CFG

Page 60: Libro alumnos

Compiladores Nicandro Cruz Ramírez

60

númerofactor

factor

opMult

opMult

factorterm

factoropMulttermterm

opSuma

opSuma

term

termopSuma

(exp)

/

*

exp

expexp

Algoritmo para calcular )(Aprimero para todos los no-terminales A (Tabla 12).

for todos_los_no_terminales_A do

{};:)( Aprimero

while existan_cambios_para_cualquier_primero(A) do

for cada_seleccion_de_producción nxxxA 21 do

;1:k

;: verdaderocontinuar

While (continuar=verdadero) and ( nk ) do

agregue }{)( kxprimero a )(Aprimero ;

if ( )( kxprimero ) then

continuar:=falso;

k=k+1;

if(continuar=verdadero) then

agregue a )(Aprimero ;

Tabla 12: Algún título

Page 61: Libro alumnos

Compiladores Nicandro Cruz Ramírez

61

4. Análisis Léxico

4.1 Planteamiento del problema

Ada es un lenguaje de programación orientado a objetos, diseñado por Jean Ichbiah de CII

Honeywell Bull por encargo del Departamento de Defensa de los Estados Unidos. Los siguientes

son tokens de un muy pequeño subconjunto de ADA:comienza

termina

;

:=

+

-

*

/

(

)

mod

rem

identificadores

enteros

Se pretende realizar un Analizador Léxico para determinar las clasificaciones de los tokens

anteriores como sigue:

1. Un identificador se ha definido como una letra seguida por un número arbitrario de letras

o dígitos

2. Las palabras comienza y termina son palabras reservadas.

3. Los símbolo punto y coma (;), paréntesis abierto y paréntesis cerrado son símbolos

permitidos.

4. Un entero ha sido definido como un dígito seguido por una secuencia de dígitos

5. Los símbolos + y - han sido denominados como OpSuma

6. Los símbolos *, / y las palabras mod y rem serán identificados como OpMultiplicacion.

7. El símbolo := se definió como Asignación.

En la etapa de pruebas se ejecutará el analizador léxico en (a) el programa siguiente, y (b)

un programa arbitrario.

comienza

a:=b3;

xyz := a + b + c

-p / q;

a := xyz * (p + q);

p:= a - xyz - p;

termina;

La salida esperada es la siguiente:

Tipo Lexema

Palabra clave comienza

Identificador a

Asignación :=

… …

Page 62: Libro alumnos

Compiladores Nicandro Cruz Ramírez

62

5. Análisis sintáctico

5.1 Planteamiento del problema

Considere la siguiente gramática

Programa Comienza SecuenciaDeSentencias termina; ;aZbS

SecuenciaDeSentencias Sentencia{Sentencia} YZ Sentencia SentenciaSimple XY

SentenciaSimple SentenciaDeAsignacion WX

SentenciaDeAsignacion Nombre:=Expresion; ;: EVW

Nombre NombreSimple TV NombreSimple Identificador cT

Expresion Relación RE

Relacion ExpresionSimple QR

ExpresionSimple Termino{OpSuma Termino} PMPQ

Termino Factor{OpMul Factor} FNFP Factor Primario LF

Primario Nombre VL Primario LiteralNumerica KL

Primario (Expresion) )(EL

OpSuma + M OpSuma - M

OpMul * *N OpMul / /N OpMul Mod mN OpMul rem rN

LiteralNumerica LiteralDecimal JK LiteralDecimal Entero eJ

Tabla 13: Gramática

Diseñe un analizador sintáctico LR(1) que implementará esta gramática. Ejecute su

analizador sintáctico con el programa siguiente, y en un programa de su propio diseño:

comienza

a:=b3;

xyz := a + b + c

-p / q;

a := xyz * (p + q);

p:= a - xyz - p;

termina;