SlideShare una empresa de Scribd logo
1 de 62
Descargar para leer sin conexión
Í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.
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.
Í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 A1  A2 .......................................................... 27
Fig. 22: Construcción de M para reconocer A1  A2 .................................................................... 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 z  y ........................................................................................ 30
Fig. 28: Autómata que reconoce ( z  y)* .................................................................................... 31
Fig. 29: Autómata que reconoce
.................................................................................. 32
Fig. 30: Ejemplo de un NFA ......................................................................................................... 35
Fig. 31: Estado inicial del DFA q1 ............................................................................................... 37
Fig. 32: Segundo estado del DFA ................................................................................................. 37
Fig. 33: Siguiente estado del DFA ................................................................................................ 37
Fig. 34: NFA correspondiente a ( z  y)* x .................................................................................. 38
Fig. 35: Estado inicial del DFA .................................................................................................... 38
Fig. 36: Nuevo estado del DFA generado por la transición x del NFA en q1 ............................ 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
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 (q0 ,  , ; p, # ) ................................................... 47
Fig. 50: Introducción de la segunda transición ( p,  , ; q, S ) .................................................... 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 S '  S ..................................................................... 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 A  ( A) | a ........................................................................................................ 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.
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.
Í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.
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
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
MUL
MOV
ADD

R0,
R0,
R1,
R1,

index
2
&a
R0

;;
;;
;;
;;

valor de index  R0
duplica el valor en R0
dirección de a R1
sumar R0 a R1
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).
:=
suma

+

deposito_inicial

*
60

interes

Fig. 2: Un compilador

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
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.
Expresion de asignacion

identificador

:=

expresion

identificador

posicion

Expresion aditiva

expresion

+

expresion

*

expresion

identificador

expresion

inicial

identificador

numero

velocidad

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).
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 (Q, ,  , q0 , F ) donde:
 Q es un conjunto finito llamado estados
  es un conjunto finito llamado alfabeto
  : Q Q es la función de transición



q0  Q es el estado inicial
F  Q 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
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 (Q  , q , F) donde:
, ,
0
 Q es un conjunto finito de estados
  es un alfabeto finito
  : Q   P (Q ) es la función de transición





q0  Q 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
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.




q0  Q es el estado inicial.



F  Q es el conjunto de estados de aceptación.
0, ε  0

q1

ε, ε  $

q2
1, 0  ε

q4

ε, $  ε

q3
1, 0  ε

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
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 reescritura 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 noterminal, 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
YX  WyZ

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 (V , , R, S ) 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.
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 (V , , R, S ) 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.
S  zMNz

M
aMa

M z

N  bNb
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.).
Código fuente

Analizador léxico
o rastreador

Tokens

Analizador
sintáctico

Árbol sintáctico

Analizador
semántico

Tabla de
literales

Árbol con anotaciones
Tabla de
símbolos

Optimizador de
código fuente

Código intermedio

Manejador
de errores

Generador de
código

Código objetivo

Optimizador de
código objetivo

Código objetivo

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
1

Tipo de token

Un lexema es un conjunto de caracteres del programa fuente que representan una secuencia
significativa
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).
expresion

Expresion de asignacion

expresion

=

expresion

Expresion de subindice

expresion

Identificador
a

[

expresion

Identificador
index

Expresion aditiva

]

expresion

+

numero
4

expresion

numero
2

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.
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
MUL
MOV
ADD

R0,
R0,
R1,
R1,

index
2
&a
R0

;; Valor de index  R0
;; Doble valor en R0
;; Dirección de a  R1
;; Sumar R0 a R1
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
SHL R0
MOV &a[R0], 6

;; Valor de index  R0
;; doble valor en R0
;; 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.
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

;
termina

Símbolo especial
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
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:
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: A  B  { x | x  A  x  B }
CONCATENACIÓN: A  B  { x y | x  A  y  B }
ESTRELLA: A*  {x1 x2 x3  xk | k  0 y cada xi  A}
Ejemplo: Sea  el alfabeto estándar de 26 letras { a, b, c, , x, y, z } . Si A  { good, bad } y
B  { boy, girl } entonces:
A  B  { good , bad , boy, girl }
A  B  { goodboy, goodgirl , badboy, badgirl }

A*  {  , good , bad , goodgood , goodbad , badgood , badbad , goodgoodbad , goodgoodgood , }
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. ( R1  R2 ) , donde R1 y R2 son expresiones regulares
5. (R1  R2 )
6. ( R *1 ) donde R1 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.
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 (Q  , q0, F) donde:
, ,
 Q es un conjunto finito de estados
  es un alfabeto finito
  : Q   P (Q ) es la función de transición
 q 0  Q es el estado inicial
 F  Q es el conjunto de estados de aceptación
Un ejemplo de un NFA aparece en la Fig. 20.
q1
a

b
ε

a

q2

a, b

q3

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
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 A1 y A2 dos lenguajes regulares, queremos probar que A1  A2 es regular. La idea
es tomar dos NFA‟s, M 1 y M 2 para A1 y A2 , respectivamente, y combinarlos en un nuevo NFA
que llamaremos M .
La máquina M debe aceptar una estrada si M 1 o M 2 aceptan esa entrada. La nueva
máquina tiene una nuevo estado inicial, con una transición  al estado inicial de M 1 y otra
transición  al estado inicial de M 2 . 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 M 1 y M 2 , 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 M 1 como a M 2 , además de tener un estado “adicional” que contiene dos
transiciones  , una a M 1 y otra a M 2 .
Fig. 21: Construcción de un

NFA para reconocer A1  A2

Demostración
Sean M1  (Q1, , 1, q1, F1 ) que reconoce a A1 y M 2  (Q2 , ,  2 , q2 , F2 ) que reconoce a
A2 . Construyamos M  (Q, ,  , q0 , F ) para que reconozca a A1  A2
1. Q  {q0 }  Q1  Q2
Los estados de M son todos los estados de M 1 y M 2 , con la adición de un nuevo estado
q0 .
2. El estado q0 es el estado inicial de M .
3. El conjunto de estados de aceptación F  F1  F2 .
Los estados de aceptación M son todos los estados de aceptación de M 1 y M 2 . De esta
manera M acepta si lo hacen M 1 o M 2
4. Definimos  de manera tal que para cualquier q  Q y cualquier a    .

 1 (q, a) q  Q1
 (q, a) q  Q

2
 ( q, a )   2
{q1 , q2 } q  q0 y a  

q  q0 y a  

Teorema 2: La clase de lenguajes regulares es cerrada bajo la concatenación.
Tenemos dos lenguajes regulares A1 y A2 queremos probar que A1  A2 es regular. La
idea es tomar dos NFA' s , M 1 y M 2 para A1 y A2 , respectivamente, y combinarlos en un nuevo
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.
Asignaremos a M en estado inicial
de M 1 . Los estados de aceptación de
M 1 tendrán transiciones  que
permitan
no
determinísticamente
“anclar” a M 2 con M 1 , es decir, dichas
transiciones irán de los estados finales
de M 1 al estado inicial de M 2 ; de esta
manera, cada vez que nos encontremos
en un estado de aceptación de M 1
significa que éste ha encontrado una
pieza inicial de la entrada que
constituye un carácter en A1 . Los
estados de aceptación de M serán sólo
los estados de aceptación de M 2 . Por lo
tanto, M
Fig. 22: Construcción de M para reconocer

A1  A2

Acepta una cadena cuando la entrada puede ser dividida en dos partes, la primera
aceptada por M 1 y la segunda por M 2 .
Demostración
Sean M1  (Q1, , 1, q1, F1 ) que reconoce a A1 y M 2  (Q2 , ,  2 , q2 , F2 ) que reconoce
a A2 . Construyamos M  (Q, ,  , q0 , F ) para que reconozca a A1  A2 .
1. Q  Q1  Q2
Los estados de M son todos los estados de M 1 y M 2 .
2. El estado q1 es el estado inicial de M 1 .
3. El conjunto de estados de aceptación F  F2 .
Los estados de aceptación M son todos los estados de aceptación de M 2
4. Definimos  de manera tal que para cualquier q  Q y cualquier a    .

 1 (q, a)
 (q, a)

 ( q, a )   1
 1 (q, a)  {q2 }
 2 (q, a)


q  Q1

y q  F1

q  F1

y a

q  F1

y a 

q  Q2

Teorema 3: La clase de lenguajes regulares es cerrada bajo la estrella de Kleene.
Tenemos
un lenguaje
regular A1 y lo modificamos
para que reconozca A1* , como se
muestra en la figura 3.

Fig. 23: Construimos M para que reconozca

A*

Demostración
Sea M1  (Q1, , 1, q1, F1 ) que reconoce a A1 . Construyamos M  (Q, ,  , q0 , F ) para
que reconozca a A1* .
1. Q  {q0 }  Q1
Los estados de M son todos los estados de M 1 mas un nuevo estado inicial.
2. El estado q0 es el nuevo estado inicial.
3. El conjunto de estados de aceptación F  {q0 }  F1 .
Los estados de aceptación M son todos los estados de aceptación de M 1 , con los que ya
contaba, más el nuevo estado inicial.
4. Definimos  de manera tal que para cualquier q  Q y cualquier a    .

 1 (q, a)
 (q, a)

 ( q, a )   1
 1 (q, a)  {q1}



q  Q1

y q  F1

q  F1

y a

q  F1

y a 

q  q0 y a  

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:
( z  y)* x

1.1. Construimos los autómatas que reconocen a

(Fig. 24),

(Fig. 25) y

(Fig. 26)
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 z  y .
Siguiendo el
Teorema 1,
agregamos un nuevo estado inicial,
q 2 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 z  y
1.3. Construimos el autómata que reconoce ( z  y)*
Siguiendo el Teorema 3,
agregaremos un nuevo estado inicial
q1 que además será un estado final.
Agregamos transiciones vacías de
todos los estados finales de z  y al
estado inicial de z  y ( q 2 ), además
de una transición vacía de q1 a
(Fig. 28)

Fig. 28: Autómata que reconoce ( z  y )

*
Compiladores

Nicandro Cruz Ramírez

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 ( z  y)*
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 ( q8 ),
y el estado inicial del nuevo autómata será el estado inicial de ( z  y)* , a saber
29).

Fig. 29: Autómata que reconoce (

(Fig.

)

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. ( z  y)* x*

32
Compiladores

Nicandro Cruz Ramírez

3. x* ( y  z )*

4. 0*10*

5. 0110 .

6. 01* 1* .

7. (a  b)* aba

33
Compiladores

Nicandro Cruz Ramírez

8. (0 1)0*

9. letra ( letra | digito )* .

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

34
Compiladores

Nicandro Cruz Ramírez

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 (Q, ,  , q0 , F ) donde:
 Q es un conjunto finito llamado estados
  es un conjunto finito llamado alfabeto
  : Q Q 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:

q1  {q1, q2, q4 }

q2  {q2}

q3  {q2, q3, q4 }

q4  {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
y
arriba.
 entonces encontrar la cerradura  de q2, q3 q4 como se muestra

Ahora definimos la cerradura  ya no de un solo estado sino de un conjunto de estados
como sigue:

S  { U s}
sS

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

{ q1q3}  q1 q3  {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.
35
Compiladores

Nicandro Cruz Ramírez

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

q1  {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 = {q 3} = {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:

q1  {q1, q2, q4 }



q2  {q2}

q3  {q2, q3, q4 }

q4  {q4 }

2. El estado inicial de nuestro DFA será la cerradura  del estado inicial del NFA
(cerradura de q1 ) , como se muestra en la Fig. 31. En este momento verificamos si



alguno de los estados de q1 es un estado de aceptación en el DFA, de ser así también lo
será q1 en el NFA. Para este ejemplo q 4 es estado de aceptación del DFA y como
q4  q1 , entonces q1 será estado de aceptación.

36
Compiladores

Nicandro Cruz Ramírez

Fig. 31: Estado inicial del DFA

q1

3. Para definir el segundo estado en el DFA (Fig. 32), verificamos para cada estado de q1 si
existen transiciones “no vacías” en el NFA a otros estados con cada uno de los elementos
de  ; es decir, q1  {q1 , q2 , q4 } , verificamos si en q1 existe alguna transición distinta de
 hacia algún estado en el NFA, cómo esto no sucede seguimos con el siguiente estado
de q1 . Ahora verificaremos si q2 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 q2 a q3 a través de una a . Por último, verificamos si q 4 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á q3 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 q3 es un
estado de aceptación en el NFA también lo será q3 en el DFA.

Fig. 32: Segundo estado del DFA

4. Verificamos las transiciones existentes en el NFA con los elementos de q3  {q2 , q3 , q4 } .
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 q2 a q3 con a . Así el siguiente estado
será de q3 a q3 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   {x, y, z}

37
Compiladores

Nicandro Cruz Ramírez

Fig. 34: NFA correspondiente a ( z  y) x
*

1. Obtenemos las cerraduras  para cada estado del NFA.
q1  {q1 , q2 , q3 , q5 , q7 }

q2  {q2 , q3 , q5 }

q3  {q3 }

q4  {q2 , q3 , q4 , q5 , q7 }

q5  {q5 }

q6  {q2 , q3 , q5 , q6 , q7 }

q7  {q7 }

q8  {q8 }

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

Fig. 35: Estado inicial del DFA

2. Verificamos las transiciones de cada elemento de q1 con cada elemento del alfabeto  .
2.1. Verificamos si los elementos de q1 tienen transiciones a otros estados en el NFA para
x   , en este caso sólo q7 va a q8 con x , por lo tanto q8  {q8 } será el estado 2 del
DFA (Fig. 36). Por ser q8 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 q1

2.2. Verificamos si los elementos de q1 tienen transiciones a otros estados en el NFA para
y   , en este caso sólo q5 va a q6 con y , por lo tanto q6  {q2 , q3 , q5 , q6 , q7 } será el
estado 3 del DFA (Fig. 37).

38
Compiladores

Nicandro Cruz Ramírez

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 q3 va a q4 con y , por lo tanto
q4  {q2 , q3 , q4 , q5 , q7 } 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, q8 , verificamos en el NFA si q8
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 q 7 tiene una transición a q8 con x , por lo tanto, el DFA va a q8 ,
que es el estado 2 (Fig. 40).

39
Compiladores

Nicandro Cruz Ramírez

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

5.2. Hacemos el procedimiento anterior pero ahora para y   ; sólo q 5 tiene una transición
a q 6 con x , por lo tanto, el DFA va a q 6 , 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 q 3 tiene una transición a q 4 con x , por lo
tanto, el DFA va a q 4 , 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 q 7 tiene una transición a q8 con x , por lo tanto, el DFA va a q8 , que
es el estado 2 (Fig. 43).

40
Compiladores

Nicandro Cruz Ramírez

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

6.2. Hacemos el procedimiento anterior pero ahora para y   ; sólo q 5 tiene una transición
a q 6 con x , por lo tanto, el DFA va a q 6 , que es el estado 3.

Fig. 44: Algún título

6.3. Finalmente verificamos para z   . Sólo q 3 tiene una transición a q 4 con x , por lo
tanto, el DFA va a q 4 , 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
41
Compiladores

Nicandro Cruz Ramírez

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 (endof-string).

42
Compiladores

Nicandro Cruz Ramírez
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.

43
Compiladores

Nicandro Cruz Ramírez

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

44
Compiladores

Nicandro Cruz Ramírez

3. Análisis sintáctico
Verificar si la cadena
SzMN
z

M a M

a

z az abz es generada por la gramática mostrada en el
bz

cuadro de la izquierda.

M z
N  b Nb

N z

1. Comenzamos escribiendo la regla perteneciente a la
variable inicial:
2. Aplicamos, para M la regla

M a M

 zaM aN
 z

a

 za zaz
N

3. Aplicamos, para M la regla M  z
4. Aplicamos, para N la regla

Sz N
Mz


N  b Nb

 zazabbz
N

5. Aplicamos, para N la regla N  z

z a z a b z b z

Un autómata de pila (PDA – Push Down Automaton) es una 6-tupla (Q, , ,  , q 0 , F )
donde Q ,  ,  y F son todos conjuntos finitos y:
1.
2.
3.
4.
5.
6.

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.
q0  Q es el estado inicial.
F  Q es el conjunto de estados de aceptación.

Recuerde que      { } y     { }
0, ε  0

q1

ε, ε  $

q2
1, 0  ε

q4

ε, $  ε

q3
1, 0  ε

Fig. 47: PDA que reconoce lenguajes del tipo

Una gramática libre de contexto (CFG, Context Free Grammar) es una 4-tupla (V , , R, S )
donde:
1. V es un conjunto finito llamado las variables.
2.  es un conjunto finito llamado los terminales
45
Compiladores

Nicandro Cruz Ramírez

3. R es un conjunto finito de reglas, con cada regla siendo una variable y una cadena de
variables y terminales.
4. S  V es la variable inicial.
Teorema: Para cada CFG existe un PDA M tal que C (G)  L(M )
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 q0 , p, q y f , siendo q 0 el estado inicial y f el único
estado de aceptación.
3. Introduzca la transición (q0 ,  , ; p, # ) .
4. Introduzca una transición ( p,  , ; q, S ) , donde S es el símbolo inicial en G.
5. Introduzca una transición de la forma (q,  , N ; q, w) para cada regla de reescritura
N  w 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 (q, x, x; q,  ) para cada Terminal x en G (es
decir, para cada símbolo en el alfabeto de M).
7. Introduzca la transición (q,  , # ; f ,  ) .
Veamos el teorema anterior aplicado a la siguiente gramática:

S  zMNz
M  aMa
M z
N  bNb
Nz
1. Sea   {S , M , N , z, a, b, #} el alfabeto.
2. Designamos los estados q0 , p, q y f , siendo q 0 el estado inicial y f el único estado de
aceptación (Fig. 48).

Fig. 48: Estados del PDA

3. Introducimos la transición (q0 ,  , ; p, # ) , es decir, una transición de q0 a p que tenga
como entrada el par ( ,  ) y como “salida” el símbolo # que será introducido a la pila
(Fig. 49).

46
Compiladores

Nicandro Cruz Ramírez

Fig. 49: Introducción de la primera transición (q0 ,  , ; p, # )

4. Introducimos la transición ( p,  , ; q, S ) , 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 ( p,  , ; q, S )

5. Introducimos una transición de la forma (q,  , N ; q, w) para cada regla de reescritura
N  w en G. Para nuestro ejemplo, introduciremos las transiciones (q,  , S ; q, zMNz) ,
(q,  , M ; q, aMa) , (q,  , M ; q, z ) , (q,  , N ; q, bNb) , (q,  , N ; q, z) (Fig. 51).

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

6. Introducimos una transición de la forma (q, x, x; q,  ) 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 (q,  , # ; f ,  ) , es decir, la transición que une al estado q con el
estado f (Fig. 53).

47
Compiladores

Nicandro Cruz Ramírez

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

De esta manera hemos comprobado que para cada CFG existe un PDA M tal que
C (G)  L(M )
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 (m, n) -é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 .

S  zMNz
M  aMa
M z
N  bNb
Nz
Fig. 54: Gramática

S
M
N

a
ERROR
aMa
ERROR

b
ERROR
ERROR
bNb

z
zMNz
Z
z

EOS
ERROR
ERROR
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

48
Compiladores

Nicandro Cruz Ramírez

1. Ejercicio 1: Dibujar el PDA correspondiente a la gramática:

SxS y
S 

(Fig. 55)

Fig. 55: PDA del Ejercicio 1

SxS z
2. Ejercicio 2: Dibujar el PDA correspondiente a la gramática: S  y S z (Fig. 56)

S 

Fig. 56: PDA del ejercicio 2

3. Ejercicio 3: Dibujar el PDA correspondiente a la gramática:

S  xS y
(Fig. 57)
S xy

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 q 0 , un estado final llamado f y
otros dos estados p , q .
2. Introduzca la transiciones (q0 ,  , ; p, # ) y (q,  , # ; f ,  ) , donde asumimos que # es
un símbolo que no ocurre en la gramática.
49
Compiladores

Nicandro Cruz Ramírez

3. Para cada símbolo Terminal x de la gramática, introduzca la transición ( p, x, ; p, x) .
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 N  w (donde w representa una cadena de 1 o más
símbolos) de la gramática, introduzca la transición ( p,  , w; p, N ) (aquí permitimos a
una transición remover más de un símbolo de la pila). Así que para ejecutar la transición

( p,  , xy; p, z) 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 ( p,  , S ; q,  ) donde S es el símbolo inicial de la gramática.
Veamos el teorema anterior aplicado a la siguiente gramática:

S  zMNz
M  aMa
M z
N  bNb
Nz
1. Establecer cuatro estados, un estado inicial llamado q 0 , un estado final llamado f y
otros dos estados p , q (Fig. 58).

Fig. 58: Establecimiento de 4 estados

2. Introducir las transiciones (q0 ,  , ; p, # ) y (q,  , # ; f ,  ) , 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 ( p, x, ; p, x) .
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).

50
Compiladores

Nicandro Cruz Ramírez

Fig. 60: Una transición por cada símbolo terminal

4. Para cada regla de reescritura N  w (donde w representa una cadena de 1 o más
símbolos) de la gramática, introduzca la transición ( p,  , w; p, N ) (Fig. 61)

Fig. 61: Una transición por cada regla gramatical

5. Introducir la transición ( p,  , S ; q,  ) donde S es el símbolo inicial de la gramática (Fig.
62).

Fig. 62: Última transición

51
Libro alumnos
Libro alumnos
Libro alumnos
Libro alumnos
Libro alumnos
Libro alumnos
Libro alumnos
Libro alumnos
Libro alumnos
Libro alumnos
Libro alumnos

Más contenido relacionado

La actualidad más candente

Índice del libro de Windows Server 2016: Administración, Seguridad y Operaciones
Índice del libro de Windows Server 2016: Administración, Seguridad y OperacionesÍndice del libro de Windows Server 2016: Administración, Seguridad y Operaciones
Índice del libro de Windows Server 2016: Administración, Seguridad y OperacionesTelefónica
 
Aprenda a programar como si estuviera en primero
Aprenda a programar como si estuviera en primeroAprenda a programar como si estuviera en primero
Aprenda a programar como si estuviera en primeroEsteban Bedoya
 
Índice del libro "Hacking con Drones" de 0xWord
Índice del libro "Hacking con Drones" de 0xWordÍndice del libro "Hacking con Drones" de 0xWord
Índice del libro "Hacking con Drones" de 0xWordTelefónica
 
Índice el Libro "Hacking Web Applications: Client-Side Attacks"
Índice el Libro "Hacking Web Applications: Client-Side Attacks"Índice el Libro "Hacking Web Applications: Client-Side Attacks"
Índice el Libro "Hacking Web Applications: Client-Side Attacks"Telefónica
 
Guia para la operacion y mantenimiento de tanques septicos, tnaques imhoff y ...
Guia para la operacion y mantenimiento de tanques septicos, tnaques imhoff y ...Guia para la operacion y mantenimiento de tanques septicos, tnaques imhoff y ...
Guia para la operacion y mantenimiento de tanques septicos, tnaques imhoff y ...Rogelio Villalta
 
Hacking con Python
Hacking con PythonHacking con Python
Hacking con PythonChema Alonso
 
Java jedi pre
Java jedi preJava jedi pre
Java jedi prejtk1
 
Java jedi prev
Java jedi prevJava jedi prev
Java jedi prevjtk1
 
Índice del libro "Hacking Web Technologies" Silver Edition de 0xWord
Índice del libro "Hacking Web Technologies" Silver Edition de 0xWordÍndice del libro "Hacking Web Technologies" Silver Edition de 0xWord
Índice del libro "Hacking Web Technologies" Silver Edition de 0xWordTelefónica
 
PLC: Diseño e implementación de un modulo de entrenamiento de automatización ...
PLC: Diseño e implementación de un modulo de entrenamiento de automatización ...PLC: Diseño e implementación de un modulo de entrenamiento de automatización ...
PLC: Diseño e implementación de un modulo de entrenamiento de automatización ...SANTIAGO PABLO ALBERTO
 
Índice del libro "Malware moderno: Técnicas avanzadas y su influencia en la i...
Índice del libro "Malware moderno: Técnicas avanzadas y su influencia en la i...Índice del libro "Malware moderno: Técnicas avanzadas y su influencia en la i...
Índice del libro "Malware moderno: Técnicas avanzadas y su influencia en la i...Telefónica
 
Máxima Seguridad en WordPress
Máxima Seguridad en WordPressMáxima Seguridad en WordPress
Máxima Seguridad en WordPressTelefónica
 
Índice del libro: "Python para pentesters" [2ª Edición] de 0xWord
Índice del libro: "Python para pentesters" [2ª Edición] de 0xWordÍndice del libro: "Python para pentesters" [2ª Edición] de 0xWord
Índice del libro: "Python para pentesters" [2ª Edición] de 0xWordTelefónica
 

La actualidad más candente (16)

Índice del libro de Windows Server 2016: Administración, Seguridad y Operaciones
Índice del libro de Windows Server 2016: Administración, Seguridad y OperacionesÍndice del libro de Windows Server 2016: Administración, Seguridad y Operaciones
Índice del libro de Windows Server 2016: Administración, Seguridad y Operaciones
 
Aprenda a programar como si estuviera en primero
Aprenda a programar como si estuviera en primeroAprenda a programar como si estuviera en primero
Aprenda a programar como si estuviera en primero
 
Índice del libro "Hacking con Drones" de 0xWord
Índice del libro "Hacking con Drones" de 0xWordÍndice del libro "Hacking con Drones" de 0xWord
Índice del libro "Hacking con Drones" de 0xWord
 
Manual ppr
Manual pprManual ppr
Manual ppr
 
Índice el Libro "Hacking Web Applications: Client-Side Attacks"
Índice el Libro "Hacking Web Applications: Client-Side Attacks"Índice el Libro "Hacking Web Applications: Client-Side Attacks"
Índice el Libro "Hacking Web Applications: Client-Side Attacks"
 
Guia para la operacion y mantenimiento de tanques septicos, tnaques imhoff y ...
Guia para la operacion y mantenimiento de tanques septicos, tnaques imhoff y ...Guia para la operacion y mantenimiento de tanques septicos, tnaques imhoff y ...
Guia para la operacion y mantenimiento de tanques septicos, tnaques imhoff y ...
 
Hacking con Python
Hacking con PythonHacking con Python
Hacking con Python
 
Java jedi pre
Java jedi preJava jedi pre
Java jedi pre
 
Java jedi prev
Java jedi prevJava jedi prev
Java jedi prev
 
Índice del libro "Hacking Web Technologies" Silver Edition de 0xWord
Índice del libro "Hacking Web Technologies" Silver Edition de 0xWordÍndice del libro "Hacking Web Technologies" Silver Edition de 0xWord
Índice del libro "Hacking Web Technologies" Silver Edition de 0xWord
 
Manual de apoyo scouts v.1.0
Manual de apoyo scouts v.1.0Manual de apoyo scouts v.1.0
Manual de apoyo scouts v.1.0
 
PLC: Diseño e implementación de un modulo de entrenamiento de automatización ...
PLC: Diseño e implementación de un modulo de entrenamiento de automatización ...PLC: Diseño e implementación de un modulo de entrenamiento de automatización ...
PLC: Diseño e implementación de un modulo de entrenamiento de automatización ...
 
Índice del libro "Malware moderno: Técnicas avanzadas y su influencia en la i...
Índice del libro "Malware moderno: Técnicas avanzadas y su influencia en la i...Índice del libro "Malware moderno: Técnicas avanzadas y su influencia en la i...
Índice del libro "Malware moderno: Técnicas avanzadas y su influencia en la i...
 
Máxima Seguridad en WordPress
Máxima Seguridad en WordPressMáxima Seguridad en WordPress
Máxima Seguridad en WordPress
 
Tfm l150
Tfm l150Tfm l150
Tfm l150
 
Índice del libro: "Python para pentesters" [2ª Edición] de 0xWord
Índice del libro: "Python para pentesters" [2ª Edición] de 0xWordÍndice del libro: "Python para pentesters" [2ª Edición] de 0xWord
Índice del libro: "Python para pentesters" [2ª Edición] de 0xWord
 

Destacado (15)

Automátas de Pila
Automátas de PilaAutomátas de Pila
Automátas de Pila
 
Articulo Autómata (push down)
Articulo Autómata (push down)Articulo Autómata (push down)
Articulo Autómata (push down)
 
Automata de Pila y Maquina de Turing No Deterministas
Automata de Pila y Maquina de Turing No DeterministasAutomata de Pila y Maquina de Turing No Deterministas
Automata de Pila y Maquina de Turing No Deterministas
 
AnáLisis LéXico
AnáLisis LéXicoAnáLisis LéXico
AnáLisis LéXico
 
Ejercicios
EjerciciosEjercicios
Ejercicios
 
Automatas de pila_no_det
Automatas de pila_no_detAutomatas de pila_no_det
Automatas de pila_no_det
 
LENGUAJES LIBRES DE CONTEXTO Y GRAMATICA LIBRES DE CONTEXTO
LENGUAJES LIBRES DE CONTEXTO Y GRAMATICA LIBRES DE CONTEXTO LENGUAJES LIBRES DE CONTEXTO Y GRAMATICA LIBRES DE CONTEXTO
LENGUAJES LIBRES DE CONTEXTO Y GRAMATICA LIBRES DE CONTEXTO
 
Jerarquia de chomsky
Jerarquia de chomskyJerarquia de chomsky
Jerarquia de chomsky
 
Autómatas de Pila
Autómatas de PilaAutómatas de Pila
Autómatas de Pila
 
Compiladores, Analisis Lexico, Tabla de Transiciones
Compiladores, Analisis Lexico, Tabla de TransicionesCompiladores, Analisis Lexico, Tabla de Transiciones
Compiladores, Analisis Lexico, Tabla de Transiciones
 
Tutorial de JFLAP
Tutorial de JFLAPTutorial de JFLAP
Tutorial de JFLAP
 
Unidad 1 lenguajes regulares
Unidad 1 lenguajes regularesUnidad 1 lenguajes regulares
Unidad 1 lenguajes regulares
 
Expresiones regulares y gramáticas
Expresiones regulares y gramáticasExpresiones regulares y gramáticas
Expresiones regulares y gramáticas
 
Automatas finitos
Automatas finitosAutomatas finitos
Automatas finitos
 
Mini manual de JFlap
Mini manual de JFlapMini manual de JFlap
Mini manual de JFlap
 

Similar a Libro alumnos

Control digital: Control digital MATLAB.pdf
Control digital: Control digital MATLAB.pdfControl digital: Control digital MATLAB.pdf
Control digital: Control digital MATLAB.pdfSANTIAGO PABLO ALBERTO
 
Ig 300 400-500 instrucciones masber solar
Ig 300 400-500 instrucciones masber solarIg 300 400-500 instrucciones masber solar
Ig 300 400-500 instrucciones masber solarMasber Solar
 
Compiladores java a_tope
Compiladores java a_topeCompiladores java a_tope
Compiladores java a_topeEmmanuel Lara
 
Web hacking-101-es
Web hacking-101-esWeb hacking-101-es
Web hacking-101-esRuben Huanca
 
Introducción a la programación en c
Introducción a la programación en cIntroducción a la programación en c
Introducción a la programación en cvictdiazm
 
Curso de c# por entregas
Curso de c# por entregasCurso de c# por entregas
Curso de c# por entregasJosé Marce
 
Excel, Tecnicas Avanzadas.pdf
Excel, Tecnicas Avanzadas.pdfExcel, Tecnicas Avanzadas.pdf
Excel, Tecnicas Avanzadas.pdfhome
 
Introduccion a xhtml
Introduccion a xhtmlIntroduccion a xhtml
Introduccion a xhtmlall12cesar
 
Introduccion al lenguaje c
Introduccion al lenguaje cIntroduccion al lenguaje c
Introduccion al lenguaje cChucho E. Peña
 
introduccion al calculo.pdf
introduccion al calculo.pdfintroduccion al calculo.pdf
introduccion al calculo.pdfFranciscoBazan8
 
introduccion al calculo.pdf
introduccion al calculo.pdfintroduccion al calculo.pdf
introduccion al calculo.pdfFranciscoBazan8
 

Similar a Libro alumnos (20)

Intro vibespanol
Intro vibespanolIntro vibespanol
Intro vibespanol
 
Control digital: Control digital MATLAB.pdf
Control digital: Control digital MATLAB.pdfControl digital: Control digital MATLAB.pdf
Control digital: Control digital MATLAB.pdf
 
Ig 300 400-500 instrucciones masber solar
Ig 300 400-500 instrucciones masber solarIg 300 400-500 instrucciones masber solar
Ig 300 400-500 instrucciones masber solar
 
Compiladores java a_tope
Compiladores java a_topeCompiladores java a_tope
Compiladores java a_tope
 
Manual - Excel con Macros.pdf
Manual - Excel con Macros.pdfManual - Excel con Macros.pdf
Manual - Excel con Macros.pdf
 
Practica5 final
Practica5 finalPractica5 final
Practica5 final
 
Web hacking-101-es
Web hacking-101-esWeb hacking-101-es
Web hacking-101-es
 
Manual pc simu
Manual pc simu  Manual pc simu
Manual pc simu
 
Introducción a la programación en c
Introducción a la programación en cIntroducción a la programación en c
Introducción a la programación en c
 
Introducción a la programación en C
Introducción a la programación en CIntroducción a la programación en C
Introducción a la programación en C
 
Compiladores
CompiladoresCompiladores
Compiladores
 
Curso de c# por entregas
Curso de c# por entregasCurso de c# por entregas
Curso de c# por entregas
 
Excel, Tecnicas Avanzadas.pdf
Excel, Tecnicas Avanzadas.pdfExcel, Tecnicas Avanzadas.pdf
Excel, Tecnicas Avanzadas.pdf
 
Introduccion a xhtml
Introduccion a xhtmlIntroduccion a xhtml
Introduccion a xhtml
 
5. desarrollador web profesional
5. desarrollador web profesional5. desarrollador web profesional
5. desarrollador web profesional
 
Introduccion a xhtml
Introduccion a xhtmlIntroduccion a xhtml
Introduccion a xhtml
 
Introduccion a xhtml
Introduccion a xhtmlIntroduccion a xhtml
Introduccion a xhtml
 
Introduccion al lenguaje c
Introduccion al lenguaje cIntroduccion al lenguaje c
Introduccion al lenguaje c
 
introduccion al calculo.pdf
introduccion al calculo.pdfintroduccion al calculo.pdf
introduccion al calculo.pdf
 
introduccion al calculo.pdf
introduccion al calculo.pdfintroduccion al calculo.pdf
introduccion al calculo.pdf
 

Último

PROGRAMACIÓN CURRICULAR ANUAL DE CIENCIA Y TECNOLOGÍA
PROGRAMACIÓN CURRICULAR ANUAL DE CIENCIA Y TECNOLOGÍAPROGRAMACIÓN CURRICULAR ANUAL DE CIENCIA Y TECNOLOGÍA
PROGRAMACIÓN CURRICULAR ANUAL DE CIENCIA Y TECNOLOGÍAJoaqunSolrzano
 
La poesía del encarcelamiento de Raúl Zurita en el aula: una propuesta didáctica
La poesía del encarcelamiento de Raúl Zurita en el aula: una propuesta didácticaLa poesía del encarcelamiento de Raúl Zurita en el aula: una propuesta didáctica
La poesía del encarcelamiento de Raúl Zurita en el aula: una propuesta didácticaIGNACIO BALLESTER PARDO
 
GUÍA SIANET - Agenda - Tareas - Archivos - Participaciones - Notas.pdf
GUÍA SIANET - Agenda - Tareas - Archivos - Participaciones - Notas.pdfGUÍA SIANET - Agenda - Tareas - Archivos - Participaciones - Notas.pdf
GUÍA SIANET - Agenda - Tareas - Archivos - Participaciones - Notas.pdfNELLYKATTY
 
Ejemplo de trabajo de TIC´s CON VARIAS OPCIONES DE LAS TAREAS
Ejemplo de trabajo de TIC´s CON VARIAS OPCIONES DE LAS TAREASEjemplo de trabajo de TIC´s CON VARIAS OPCIONES DE LAS TAREAS
Ejemplo de trabajo de TIC´s CON VARIAS OPCIONES DE LAS TAREASJavier Sanchez
 
Revista digital primer ciclo 2024 colección ediba
Revista digital primer ciclo 2024 colección edibaRevista digital primer ciclo 2024 colección ediba
Revista digital primer ciclo 2024 colección edibaTatiTerlecky1
 
Kirpi-el-erizo libro descargar pdf 1 link
Kirpi-el-erizo libro descargar pdf 1 linkKirpi-el-erizo libro descargar pdf 1 link
Kirpi-el-erizo libro descargar pdf 1 linkMaximilianoMaldonado17
 
UNIDAD DE APRENDIZAJE MARZO 2024.docx para educacion
UNIDAD DE APRENDIZAJE MARZO 2024.docx para educacionUNIDAD DE APRENDIZAJE MARZO 2024.docx para educacion
UNIDAD DE APRENDIZAJE MARZO 2024.docx para educacionCarolVigo1
 
EL BRILLO DEL ECLIPSE (CUENTO LITERARIO). Autor y diseñador JAVIER SOLIS NOYOLA
EL BRILLO DEL ECLIPSE (CUENTO LITERARIO). Autor y diseñador JAVIER SOLIS NOYOLAEL BRILLO DEL ECLIPSE (CUENTO LITERARIO). Autor y diseñador JAVIER SOLIS NOYOLA
EL BRILLO DEL ECLIPSE (CUENTO LITERARIO). Autor y diseñador JAVIER SOLIS NOYOLAJAVIER SOLIS NOYOLA
 
CIENCIAS SOCIALES SEGUNDO TRIMESTRE CUARTO
CIENCIAS SOCIALES SEGUNDO TRIMESTRE CUARTOCIENCIAS SOCIALES SEGUNDO TRIMESTRE CUARTO
CIENCIAS SOCIALES SEGUNDO TRIMESTRE CUARTOCEIP TIERRA DE PINARES
 
Tema 4 Rocas sedimentarias, características y clasificación
Tema 4 Rocas sedimentarias, características y clasificaciónTema 4 Rocas sedimentarias, características y clasificación
Tema 4 Rocas sedimentarias, características y clasificaciónIES Vicent Andres Estelles
 
Anna Llenas Serra. El monstruo de colores. Doctor de emociones.pdf
Anna Llenas Serra. El monstruo de colores. Doctor de emociones.pdfAnna Llenas Serra. El monstruo de colores. Doctor de emociones.pdf
Anna Llenas Serra. El monstruo de colores. Doctor de emociones.pdfSaraGabrielaPrezPonc
 
U2_EA1_descargable TIC 2 SEM VIR PRE.pdf
U2_EA1_descargable TIC 2 SEM VIR PRE.pdfU2_EA1_descargable TIC 2 SEM VIR PRE.pdf
U2_EA1_descargable TIC 2 SEM VIR PRE.pdfJavier Correa
 
Presentación: Actividad de Diálogos adolescentes.pptx
Presentación: Actividad de  Diálogos adolescentes.pptxPresentación: Actividad de  Diálogos adolescentes.pptx
Presentación: Actividad de Diálogos adolescentes.pptxNabel Paulino Guerra Huaranca
 
ficha de aplicacion para estudiantes El agua para niños de primaria
ficha de aplicacion para estudiantes El agua para niños de primariaficha de aplicacion para estudiantes El agua para niños de primaria
ficha de aplicacion para estudiantes El agua para niños de primariamichel carlos Capillo Dominguez
 
Presentación del tema: tecnología educativa
Presentación del tema: tecnología educativaPresentación del tema: tecnología educativa
Presentación del tema: tecnología educativaricardoruizaleman
 
La Congregación de Jesús y María, conocida también como los Eudistas, fue fun...
La Congregación de Jesús y María, conocida también como los Eudistas, fue fun...La Congregación de Jesús y María, conocida también como los Eudistas, fue fun...
La Congregación de Jesús y María, conocida también como los Eudistas, fue fun...Unidad de Espiritualidad Eudista
 
la forma de los objetos expresión gráfica preescolar
la forma de los objetos expresión gráfica preescolarla forma de los objetos expresión gráfica preescolar
la forma de los objetos expresión gráfica preescolarCa Ut
 
Tecnología educativa en la era actual .pptx
Tecnología educativa en la era actual .pptxTecnología educativa en la era actual .pptx
Tecnología educativa en la era actual .pptxJulioSantin2
 

Último (20)

PROGRAMACIÓN CURRICULAR ANUAL DE CIENCIA Y TECNOLOGÍA
PROGRAMACIÓN CURRICULAR ANUAL DE CIENCIA Y TECNOLOGÍAPROGRAMACIÓN CURRICULAR ANUAL DE CIENCIA Y TECNOLOGÍA
PROGRAMACIÓN CURRICULAR ANUAL DE CIENCIA Y TECNOLOGÍA
 
La poesía del encarcelamiento de Raúl Zurita en el aula: una propuesta didáctica
La poesía del encarcelamiento de Raúl Zurita en el aula: una propuesta didácticaLa poesía del encarcelamiento de Raúl Zurita en el aula: una propuesta didáctica
La poesía del encarcelamiento de Raúl Zurita en el aula: una propuesta didáctica
 
GUÍA SIANET - Agenda - Tareas - Archivos - Participaciones - Notas.pdf
GUÍA SIANET - Agenda - Tareas - Archivos - Participaciones - Notas.pdfGUÍA SIANET - Agenda - Tareas - Archivos - Participaciones - Notas.pdf
GUÍA SIANET - Agenda - Tareas - Archivos - Participaciones - Notas.pdf
 
Ejemplo de trabajo de TIC´s CON VARIAS OPCIONES DE LAS TAREAS
Ejemplo de trabajo de TIC´s CON VARIAS OPCIONES DE LAS TAREASEjemplo de trabajo de TIC´s CON VARIAS OPCIONES DE LAS TAREAS
Ejemplo de trabajo de TIC´s CON VARIAS OPCIONES DE LAS TAREAS
 
Actividad de bienestar docente 2016 Pereira
Actividad de bienestar docente 2016 PereiraActividad de bienestar docente 2016 Pereira
Actividad de bienestar docente 2016 Pereira
 
Conducta ética en investigación científica.pdf
Conducta ética en investigación científica.pdfConducta ética en investigación científica.pdf
Conducta ética en investigación científica.pdf
 
Revista digital primer ciclo 2024 colección ediba
Revista digital primer ciclo 2024 colección edibaRevista digital primer ciclo 2024 colección ediba
Revista digital primer ciclo 2024 colección ediba
 
Kirpi-el-erizo libro descargar pdf 1 link
Kirpi-el-erizo libro descargar pdf 1 linkKirpi-el-erizo libro descargar pdf 1 link
Kirpi-el-erizo libro descargar pdf 1 link
 
UNIDAD DE APRENDIZAJE MARZO 2024.docx para educacion
UNIDAD DE APRENDIZAJE MARZO 2024.docx para educacionUNIDAD DE APRENDIZAJE MARZO 2024.docx para educacion
UNIDAD DE APRENDIZAJE MARZO 2024.docx para educacion
 
EL BRILLO DEL ECLIPSE (CUENTO LITERARIO). Autor y diseñador JAVIER SOLIS NOYOLA
EL BRILLO DEL ECLIPSE (CUENTO LITERARIO). Autor y diseñador JAVIER SOLIS NOYOLAEL BRILLO DEL ECLIPSE (CUENTO LITERARIO). Autor y diseñador JAVIER SOLIS NOYOLA
EL BRILLO DEL ECLIPSE (CUENTO LITERARIO). Autor y diseñador JAVIER SOLIS NOYOLA
 
CIENCIAS SOCIALES SEGUNDO TRIMESTRE CUARTO
CIENCIAS SOCIALES SEGUNDO TRIMESTRE CUARTOCIENCIAS SOCIALES SEGUNDO TRIMESTRE CUARTO
CIENCIAS SOCIALES SEGUNDO TRIMESTRE CUARTO
 
Tema 4 Rocas sedimentarias, características y clasificación
Tema 4 Rocas sedimentarias, características y clasificaciónTema 4 Rocas sedimentarias, características y clasificación
Tema 4 Rocas sedimentarias, características y clasificación
 
Anna Llenas Serra. El monstruo de colores. Doctor de emociones.pdf
Anna Llenas Serra. El monstruo de colores. Doctor de emociones.pdfAnna Llenas Serra. El monstruo de colores. Doctor de emociones.pdf
Anna Llenas Serra. El monstruo de colores. Doctor de emociones.pdf
 
U2_EA1_descargable TIC 2 SEM VIR PRE.pdf
U2_EA1_descargable TIC 2 SEM VIR PRE.pdfU2_EA1_descargable TIC 2 SEM VIR PRE.pdf
U2_EA1_descargable TIC 2 SEM VIR PRE.pdf
 
Presentación: Actividad de Diálogos adolescentes.pptx
Presentación: Actividad de  Diálogos adolescentes.pptxPresentación: Actividad de  Diálogos adolescentes.pptx
Presentación: Actividad de Diálogos adolescentes.pptx
 
ficha de aplicacion para estudiantes El agua para niños de primaria
ficha de aplicacion para estudiantes El agua para niños de primariaficha de aplicacion para estudiantes El agua para niños de primaria
ficha de aplicacion para estudiantes El agua para niños de primaria
 
Presentación del tema: tecnología educativa
Presentación del tema: tecnología educativaPresentación del tema: tecnología educativa
Presentación del tema: tecnología educativa
 
La Congregación de Jesús y María, conocida también como los Eudistas, fue fun...
La Congregación de Jesús y María, conocida también como los Eudistas, fue fun...La Congregación de Jesús y María, conocida también como los Eudistas, fue fun...
La Congregación de Jesús y María, conocida también como los Eudistas, fue fun...
 
la forma de los objetos expresión gráfica preescolar
la forma de los objetos expresión gráfica preescolarla forma de los objetos expresión gráfica preescolar
la forma de los objetos expresión gráfica preescolar
 
Tecnología educativa en la era actual .pptx
Tecnología educativa en la era actual .pptxTecnología educativa en la era actual .pptx
Tecnología educativa en la era actual .pptx
 

Libro alumnos

  • 1. Í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.
  • 2. 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.
  • 3. Í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 A1  A2 .......................................................... 27 Fig. 22: Construcción de M para reconocer A1  A2 .................................................................... 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 z  y ........................................................................................ 30 Fig. 28: Autómata que reconoce ( z  y)* .................................................................................... 31 Fig. 29: Autómata que reconoce .................................................................................. 32 Fig. 30: Ejemplo de un NFA ......................................................................................................... 35 Fig. 31: Estado inicial del DFA q1 ............................................................................................... 37 Fig. 32: Segundo estado del DFA ................................................................................................. 37 Fig. 33: Siguiente estado del DFA ................................................................................................ 37 Fig. 34: NFA correspondiente a ( z  y)* x .................................................................................. 38 Fig. 35: Estado inicial del DFA .................................................................................................... 38 Fig. 36: Nuevo estado del DFA generado por la transición x del NFA en q1 ............................ 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
  • 4. 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 (q0 ,  , ; p, # ) ................................................... 47 Fig. 50: Introducción de la segunda transición ( p,  , ; q, S ) .................................................... 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 S '  S ..................................................................... 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 A  ( A) | a ........................................................................................................ 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.
  • 5. 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.
  • 6. Í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.
  • 7. 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
  • 8. 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 MUL MOV ADD R0, R0, R1, R1, index 2 &a R0 ;; ;; ;; ;; valor de index  R0 duplica el valor en R0 dirección de a R1 sumar R0 a R1
  • 9. 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). := suma + deposito_inicial * 60 interes Fig. 2: Un compilador 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
  • 10. 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. Expresion de asignacion identificador := expresion identificador posicion Expresion aditiva expresion + expresion * expresion identificador expresion inicial identificador numero velocidad 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).
  • 11. 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 (Q, ,  , q0 , F ) donde:  Q es un conjunto finito llamado estados   es un conjunto finito llamado alfabeto   : Q Q es la función de transición   q0  Q es el estado inicial F  Q 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
  • 12. 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 (Q  , q , F) donde: , , 0  Q es un conjunto finito de estados   es un alfabeto finito   : Q   P (Q ) es la función de transición    q0  Q 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
  • 13. 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. 
  • 14.  q0  Q es el estado inicial.  F  Q es el conjunto de estados de aceptación. 0, ε  0 q1 ε, ε  $ q2 1, 0  ε q4 ε, $  ε q3 1, 0  ε 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
  • 15. 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 reescritura 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 noterminal, 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 YX  WyZ 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 (V , , R, S ) 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.
  • 16. 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 (V , , R, S ) 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. S  zMNz M aMa M z N  bNb 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.).
  • 17. Código fuente Analizador léxico o rastreador Tokens Analizador sintáctico Árbol sintáctico Analizador semántico Tabla de literales Árbol con anotaciones Tabla de símbolos Optimizador de código fuente Código intermedio Manejador de errores Generador de código Código objetivo Optimizador de código objetivo Código objetivo 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 1 Tipo de token Un lexema es un conjunto de caracteres del programa fuente que representan una secuencia significativa
  • 18. 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). expresion Expresion de asignacion expresion = expresion Expresion de subindice expresion Identificador a [ expresion Identificador index Expresion aditiva ] expresion + numero 4 expresion numero 2 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.
  • 19. 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 MUL MOV ADD R0, R0, R1, R1, index 2 &a R0 ;; Valor de index  R0 ;; Doble valor en R0 ;; Dirección de a  R1 ;; Sumar R0 a R1
  • 20. 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 SHL R0 MOV &a[R0], 6 ;; Valor de index  R0 ;; doble valor en R0 ;; 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.
  • 21. 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 ; termina Símbolo especial 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
  • 22. 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:
  • 23. 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: A  B  { x | x  A  x  B } CONCATENACIÓN: A  B  { x y | x  A  y  B } ESTRELLA: A*  {x1 x2 x3  xk | k  0 y cada xi  A} Ejemplo: Sea  el alfabeto estándar de 26 letras { a, b, c, , x, y, z } . Si A  { good, bad } y B  { boy, girl } entonces: A  B  { good , bad , boy, girl } A  B  { goodboy, goodgirl , badboy, badgirl }  A*  {  , good , bad , goodgood , goodbad , badgood , badbad , goodgoodbad , goodgoodgood , } 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. ( R1  R2 ) , donde R1 y R2 son expresiones regulares 5. (R1  R2 ) 6. ( R *1 ) donde R1 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.
  • 24. 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 (Q  , q0, F) donde: , ,  Q es un conjunto finito de estados   es un alfabeto finito   : Q   P (Q ) es la función de transición  q 0  Q es el estado inicial  F  Q es el conjunto de estados de aceptación Un ejemplo de un NFA aparece en la Fig. 20.
  • 25. q1 a b ε a q2 a, b q3 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
  • 26. 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 A1 y A2 dos lenguajes regulares, queremos probar que A1  A2 es regular. La idea es tomar dos NFA‟s, M 1 y M 2 para A1 y A2 , respectivamente, y combinarlos en un nuevo NFA que llamaremos M . La máquina M debe aceptar una estrada si M 1 o M 2 aceptan esa entrada. La nueva máquina tiene una nuevo estado inicial, con una transición  al estado inicial de M 1 y otra transición  al estado inicial de M 2 . 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 M 1 y M 2 , 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 M 1 como a M 2 , además de tener un estado “adicional” que contiene dos transiciones  , una a M 1 y otra a M 2 .
  • 27. Fig. 21: Construcción de un NFA para reconocer A1  A2 Demostración Sean M1  (Q1, , 1, q1, F1 ) que reconoce a A1 y M 2  (Q2 , ,  2 , q2 , F2 ) que reconoce a A2 . Construyamos M  (Q, ,  , q0 , F ) para que reconozca a A1  A2 1. Q  {q0 }  Q1  Q2 Los estados de M son todos los estados de M 1 y M 2 , con la adición de un nuevo estado q0 . 2. El estado q0 es el estado inicial de M . 3. El conjunto de estados de aceptación F  F1  F2 . Los estados de aceptación M son todos los estados de aceptación de M 1 y M 2 . De esta manera M acepta si lo hacen M 1 o M 2 4. Definimos  de manera tal que para cualquier q  Q y cualquier a    .  1 (q, a) q  Q1  (q, a) q  Q  2  ( q, a )   2 {q1 , q2 } q  q0 y a    q  q0 y a    Teorema 2: La clase de lenguajes regulares es cerrada bajo la concatenación. Tenemos dos lenguajes regulares A1 y A2 queremos probar que A1  A2 es regular. La idea es tomar dos NFA' s , M 1 y M 2 para A1 y A2 , respectivamente, y combinarlos en un nuevo
  • 28. 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. Asignaremos a M en estado inicial de M 1 . Los estados de aceptación de M 1 tendrán transiciones  que permitan no determinísticamente “anclar” a M 2 con M 1 , es decir, dichas transiciones irán de los estados finales de M 1 al estado inicial de M 2 ; de esta manera, cada vez que nos encontremos en un estado de aceptación de M 1 significa que éste ha encontrado una pieza inicial de la entrada que constituye un carácter en A1 . Los estados de aceptación de M serán sólo los estados de aceptación de M 2 . Por lo tanto, M Fig. 22: Construcción de M para reconocer A1  A2 Acepta una cadena cuando la entrada puede ser dividida en dos partes, la primera aceptada por M 1 y la segunda por M 2 . Demostración Sean M1  (Q1, , 1, q1, F1 ) que reconoce a A1 y M 2  (Q2 , ,  2 , q2 , F2 ) que reconoce a A2 . Construyamos M  (Q, ,  , q0 , F ) para que reconozca a A1  A2 . 1. Q  Q1  Q2 Los estados de M son todos los estados de M 1 y M 2 . 2. El estado q1 es el estado inicial de M 1 . 3. El conjunto de estados de aceptación F  F2 . Los estados de aceptación M son todos los estados de aceptación de M 2 4. Definimos  de manera tal que para cualquier q  Q y cualquier a    .  1 (q, a)  (q, a)   ( q, a )   1  1 (q, a)  {q2 }  2 (q, a)  q  Q1 y q  F1 q  F1 y a q  F1 y a  q  Q2 Teorema 3: La clase de lenguajes regulares es cerrada bajo la estrella de Kleene.
  • 29. Tenemos un lenguaje regular A1 y lo modificamos para que reconozca A1* , como se muestra en la figura 3. Fig. 23: Construimos M para que reconozca A* Demostración Sea M1  (Q1, , 1, q1, F1 ) que reconoce a A1 . Construyamos M  (Q, ,  , q0 , F ) para que reconozca a A1* . 1. Q  {q0 }  Q1 Los estados de M son todos los estados de M 1 mas un nuevo estado inicial. 2. El estado q0 es el nuevo estado inicial. 3. El conjunto de estados de aceptación F  {q0 }  F1 . Los estados de aceptación M son todos los estados de aceptación de M 1 , con los que ya contaba, más el nuevo estado inicial. 4. Definimos  de manera tal que para cualquier q  Q y cualquier a    .  1 (q, a)  (q, a)   ( q, a )   1  1 (q, a)  {q1}   q  Q1 y q  F1 q  F1 y a q  F1 y a  q  q0 y a   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: ( z  y)* x 1.1. Construimos los autómatas que reconocen a (Fig. 24), (Fig. 25) y (Fig. 26)
  • 30. 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 z  y . Siguiendo el Teorema 1, agregamos un nuevo estado inicial, q 2 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 z  y
  • 31. 1.3. Construimos el autómata que reconoce ( z  y)* Siguiendo el Teorema 3, agregaremos un nuevo estado inicial q1 que además será un estado final. Agregamos transiciones vacías de todos los estados finales de z  y al estado inicial de z  y ( q 2 ), además de una transición vacía de q1 a (Fig. 28) Fig. 28: Autómata que reconoce ( z  y ) *
  • 32. Compiladores Nicandro Cruz Ramírez 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 ( z  y)* 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 ( q8 ), y el estado inicial del nuevo autómata será el estado inicial de ( z  y)* , a saber 29). Fig. 29: Autómata que reconoce ( (Fig. ) 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. ( z  y)* x* 32
  • 33. Compiladores Nicandro Cruz Ramírez 3. x* ( y  z )* 4. 0*10* 5. 0110 . 6. 01* 1* . 7. (a  b)* aba 33
  • 34. Compiladores Nicandro Cruz Ramírez 8. (0 1)0* 9. letra ( letra | digito )* . 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 34
  • 35. Compiladores Nicandro Cruz Ramírez 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 (Q, ,  , q0 , F ) donde:  Q es un conjunto finito llamado estados   es un conjunto finito llamado alfabeto   : Q Q 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: q1  {q1, q2, q4 } q2  {q2} q3  {q2, q3, q4 } q4  {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 y arriba.  entonces encontrar la cerradura  de q2, q3 q4 como se muestra  Ahora definimos la cerradura  ya no de un solo estado sino de un conjunto de estados como sigue: S  { U s} sS Donde S es un conjunto de estados. Por ejemplo, para el NFA de la Fig. 30: { q1q3}  q1 q3  {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. 35
  • 36. Compiladores Nicandro Cruz Ramírez PASO 1: Calcular la cerradura  del estado inicial de N (consideramos el NFA de la Fig. 30): q1  {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 = {q 3} = {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: q1  {q1, q2, q4 }  q2  {q2} q3  {q2, q3, q4 } q4  {q4 } 2. El estado inicial de nuestro DFA será la cerradura  del estado inicial del NFA (cerradura de q1 ) , como se muestra en la Fig. 31. En este momento verificamos si    alguno de los estados de q1 es un estado de aceptación en el DFA, de ser así también lo será q1 en el NFA. Para este ejemplo q 4 es estado de aceptación del DFA y como q4  q1 , entonces q1 será estado de aceptación. 36
  • 37. Compiladores Nicandro Cruz Ramírez Fig. 31: Estado inicial del DFA q1 3. Para definir el segundo estado en el DFA (Fig. 32), verificamos para cada estado de q1 si existen transiciones “no vacías” en el NFA a otros estados con cada uno de los elementos de  ; es decir, q1  {q1 , q2 , q4 } , verificamos si en q1 existe alguna transición distinta de  hacia algún estado en el NFA, cómo esto no sucede seguimos con el siguiente estado de q1 . Ahora verificaremos si q2 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 q2 a q3 a través de una a . Por último, verificamos si q 4 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á q3 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 q3 es un estado de aceptación en el NFA también lo será q3 en el DFA. Fig. 32: Segundo estado del DFA 4. Verificamos las transiciones existentes en el NFA con los elementos de q3  {q2 , q3 , q4 } . 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 q2 a q3 con a . Así el siguiente estado será de q3 a q3 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   {x, y, z} 37
  • 38. Compiladores Nicandro Cruz Ramírez Fig. 34: NFA correspondiente a ( z  y) x * 1. Obtenemos las cerraduras  para cada estado del NFA. q1  {q1 , q2 , q3 , q5 , q7 } q2  {q2 , q3 , q5 } q3  {q3 } q4  {q2 , q3 , q4 , q5 , q7 } q5  {q5 } q6  {q2 , q3 , q5 , q6 , q7 } q7  {q7 } q8  {q8 } Dibujamos el estado inicial del DFA que será q1 (Fig. 35). Fig. 35: Estado inicial del DFA 2. Verificamos las transiciones de cada elemento de q1 con cada elemento del alfabeto  . 2.1. Verificamos si los elementos de q1 tienen transiciones a otros estados en el NFA para x   , en este caso sólo q7 va a q8 con x , por lo tanto q8  {q8 } será el estado 2 del DFA (Fig. 36). Por ser q8 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 q1 2.2. Verificamos si los elementos de q1 tienen transiciones a otros estados en el NFA para y   , en este caso sólo q5 va a q6 con y , por lo tanto q6  {q2 , q3 , q5 , q6 , q7 } será el estado 3 del DFA (Fig. 37). 38
  • 39. Compiladores Nicandro Cruz Ramírez 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 q3 va a q4 con y , por lo tanto q4  {q2 , q3 , q4 , q5 , q7 } 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, q8 , verificamos en el NFA si q8 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 q 7 tiene una transición a q8 con x , por lo tanto, el DFA va a q8 , que es el estado 2 (Fig. 40). 39
  • 40. Compiladores Nicandro Cruz Ramírez Fig. 40: Transición del estado 3 al estado 2 5.2. Hacemos el procedimiento anterior pero ahora para y   ; sólo q 5 tiene una transición a q 6 con x , por lo tanto, el DFA va a q 6 , 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 q 3 tiene una transición a q 4 con x , por lo tanto, el DFA va a q 4 , 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 q 7 tiene una transición a q8 con x , por lo tanto, el DFA va a q8 , que es el estado 2 (Fig. 43). 40
  • 41. Compiladores Nicandro Cruz Ramírez Fig. 43: Transición del estado 4 añ estado 2 6.2. Hacemos el procedimiento anterior pero ahora para y   ; sólo q 5 tiene una transición a q 6 con x , por lo tanto, el DFA va a q 6 , que es el estado 3. Fig. 44: Algún título 6.3. Finalmente verificamos para z   . Sólo q 3 tiene una transición a q 4 con x , por lo tanto, el DFA va a q 4 , 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 41
  • 42. Compiladores Nicandro Cruz Ramírez 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 (endof-string). 42
  • 43. Compiladores Nicandro Cruz Ramírez 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. 43
  • 44. Compiladores Nicandro Cruz Ramírez 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 44
  • 45. Compiladores Nicandro Cruz Ramírez 3. Análisis sintáctico Verificar si la cadena SzMN z M a M a z az abz es generada por la gramática mostrada en el bz cuadro de la izquierda. M z N  b Nb N z 1. Comenzamos escribiendo la regla perteneciente a la variable inicial: 2. Aplicamos, para M la regla M a M  zaM aN  z a  za zaz N 3. Aplicamos, para M la regla M  z 4. Aplicamos, para N la regla Sz N Mz  N  b Nb  zazabbz N 5. Aplicamos, para N la regla N  z z a z a b z b z Un autómata de pila (PDA – Push Down Automaton) es una 6-tupla (Q, , ,  , q 0 , F ) donde Q ,  ,  y F son todos conjuntos finitos y: 1. 2. 3. 4. 5. 6. 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. q0  Q es el estado inicial. F  Q es el conjunto de estados de aceptación. Recuerde que      { } y     { } 0, ε  0 q1 ε, ε  $ q2 1, 0  ε q4 ε, $  ε q3 1, 0  ε Fig. 47: PDA que reconoce lenguajes del tipo Una gramática libre de contexto (CFG, Context Free Grammar) es una 4-tupla (V , , R, S ) donde: 1. V es un conjunto finito llamado las variables. 2.  es un conjunto finito llamado los terminales 45
  • 46. Compiladores Nicandro Cruz Ramírez 3. R es un conjunto finito de reglas, con cada regla siendo una variable y una cadena de variables y terminales. 4. S  V es la variable inicial. Teorema: Para cada CFG existe un PDA M tal que C (G)  L(M ) 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 q0 , p, q y f , siendo q 0 el estado inicial y f el único estado de aceptación. 3. Introduzca la transición (q0 ,  , ; p, # ) . 4. Introduzca una transición ( p,  , ; q, S ) , donde S es el símbolo inicial en G. 5. Introduzca una transición de la forma (q,  , N ; q, w) para cada regla de reescritura N  w 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 (q, x, x; q,  ) para cada Terminal x en G (es decir, para cada símbolo en el alfabeto de M). 7. Introduzca la transición (q,  , # ; f ,  ) . Veamos el teorema anterior aplicado a la siguiente gramática: S  zMNz M  aMa M z N  bNb Nz 1. Sea   {S , M , N , z, a, b, #} el alfabeto. 2. Designamos los estados q0 , p, q y f , siendo q 0 el estado inicial y f el único estado de aceptación (Fig. 48). Fig. 48: Estados del PDA 3. Introducimos la transición (q0 ,  , ; p, # ) , es decir, una transición de q0 a p que tenga como entrada el par ( ,  ) y como “salida” el símbolo # que será introducido a la pila (Fig. 49). 46
  • 47. Compiladores Nicandro Cruz Ramírez Fig. 49: Introducción de la primera transición (q0 ,  , ; p, # ) 4. Introducimos la transición ( p,  , ; q, S ) , 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 ( p,  , ; q, S ) 5. Introducimos una transición de la forma (q,  , N ; q, w) para cada regla de reescritura N  w en G. Para nuestro ejemplo, introduciremos las transiciones (q,  , S ; q, zMNz) , (q,  , M ; q, aMa) , (q,  , M ; q, z ) , (q,  , N ; q, bNb) , (q,  , N ; q, z) (Fig. 51). Fig. 51: Introducir transiciones por cada regla de producción 6. Introducimos una transición de la forma (q, x, x; q,  ) 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 (q,  , # ; f ,  ) , es decir, la transición que une al estado q con el estado f (Fig. 53). 47
  • 48. Compiladores Nicandro Cruz Ramírez Fig. 53: PDA para la gramática dada. De esta manera hemos comprobado que para cada CFG existe un PDA M tal que C (G)  L(M ) 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 (m, n) -é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 . S  zMNz M  aMa M z N  bNb Nz Fig. 54: Gramática S M N a ERROR aMa ERROR b ERROR ERROR bNb z zMNz Z z EOS ERROR ERROR 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 48
  • 49. Compiladores Nicandro Cruz Ramírez 1. Ejercicio 1: Dibujar el PDA correspondiente a la gramática: SxS y S  (Fig. 55) Fig. 55: PDA del Ejercicio 1 SxS z 2. Ejercicio 2: Dibujar el PDA correspondiente a la gramática: S  y S z (Fig. 56) S  Fig. 56: PDA del ejercicio 2 3. Ejercicio 3: Dibujar el PDA correspondiente a la gramática: S  xS y (Fig. 57) S xy 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 q 0 , un estado final llamado f y otros dos estados p , q . 2. Introduzca la transiciones (q0 ,  , ; p, # ) y (q,  , # ; f ,  ) , donde asumimos que # es un símbolo que no ocurre en la gramática. 49
  • 50. Compiladores Nicandro Cruz Ramírez 3. Para cada símbolo Terminal x de la gramática, introduzca la transición ( p, x, ; p, x) . 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 N  w (donde w representa una cadena de 1 o más símbolos) de la gramática, introduzca la transición ( p,  , w; p, N ) (aquí permitimos a una transición remover más de un símbolo de la pila). Así que para ejecutar la transición  ( p,  , xy; p, z) 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 ( p,  , S ; q,  ) donde S es el símbolo inicial de la gramática. Veamos el teorema anterior aplicado a la siguiente gramática: S  zMNz M  aMa M z N  bNb Nz 1. Establecer cuatro estados, un estado inicial llamado q 0 , un estado final llamado f y otros dos estados p , q (Fig. 58). Fig. 58: Establecimiento de 4 estados 2. Introducir las transiciones (q0 ,  , ; p, # ) y (q,  , # ; f ,  ) , 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 ( p, x, ; p, x) . 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). 50
  • 51. Compiladores Nicandro Cruz Ramírez Fig. 60: Una transición por cada símbolo terminal 4. Para cada regla de reescritura N  w (donde w representa una cadena de 1 o más símbolos) de la gramática, introduzca la transición ( p,  , w; p, N ) (Fig. 61) Fig. 61: Una transición por cada regla gramatical 5. Introducir la transición ( p,  , S ; q,  ) donde S es el símbolo inicial de la gramática (Fig. 62). Fig. 62: Última transición 51