You are on page 1of 72

FACULTAD DE INGENIERÍA

E.A.P. INGENIERÍA DE SISTEMAS E INFORMÁTICA

MANUAL DEL CURSO:

Teoría de Lenguajes
(Unidad III)

Tema:
Descripción de lenguajes de programación

Dictado por:
DIANA CECILIA MUÑOZ CASANOVA
M.S. en Ingeniería de Sistemas e Informática

CHIMBOTE – PERÚ
2007
CAPÍTULO I: INTRODUCCIÓN A LOS COMPILADORES

1.1. Introducción 101


1.2. Historia 104
1.3. Conceptos básicos 108
1.4. Concepto de compilador 109
1.5. Interpretar vs compilar 110
1.6. Tipos de compiladores 112

CAPÍTULO II: CONCEPTOS FUNDAMENTALES DE LOS COMPILADORES

2.1. Estructura de un compilador. 114


2.1.1.Preprocesador 115
2.1.2.Compilación 115
2.1.3.Ensamblado 115
2.1.4.Enlazado 116
2.2. Proceso de compilación 117
2.2.1.Fase de análisis 118
2.2.2.Fase de síntesis 119
2.3. Ejemplo del proceso de compilación 120
2.4. Herramientas para construcción de compiladores 125
2.5. El lenguaje y la herramienta 126
2.6. Aspectos académicos y de investigación de compiladores 127

CAPÍTULO III: ANÁLISIS LÉXICO

3.1. Análisis léxico (scanner) 128


3.2. El proceso del análisis léxico 128
3.3. ¿que es un analizador léxico? 129
3.4. Funciones del analizador léxico 130
3.5. Necesidad del analizador léxico 131
3.6. Ventajas de separar el análisis léxico y el análisis sintáctico: 133
3.7. Componentes léxicos, patrones, lexemas 133
3.7.1.Componente léxico o token 133
3.7.2.Patrón o expresión regular 134
3.7.3.Lexema 134
3.8. Descripción de un analizador léxico 136
3.9. Unidades de léxico 137
3.10. El rol del analizador léxico 138
3.11. Tratamiento de los errores 139
3.12. Tratamiento de palabras reservadas 140
3.13. Construcción de un analizador léxico 140
3.14. Concepto de expresión regular 141
3.15. Definición de expresión regular 141
3.16. Operaciones de expresiones regulares 142
3.17. Lenguaje descrito por una expresión regular 142
3.18. Teoremas de equivalencia 143
3.19. Matrices de transición 143
3.20. Representación de los autómatas 144
3.21. Autómata finito determinista 145
3.22. Autómata finito no determinista 147

CAPÍTULO IV: ANÁLISIS SINTÁCTICO

4.1. Gramáticas 149


4.2. Gramáticas libres de contexto y análisis sintáctico 151
4.3. Gramáticas libres de contexto 151
4.4. Conceptos sobre GLCS 152
4.4.1.Árbol de derivación 152
4.4.2.Gramáticas no ambiguas 153
4.4.3.Gramáticas ambiguas 154
4.5. Gramática BNF 156
4.6. Árboles de análisis sintácticos 157
4.7. Extensión de la notación BNF 158
4.8. La notación BNF extendida 158
4.9. El proceso de análisis sintáctico 158
4.10. Análisis sintáctico ascendente 159
4.11. Analizador sintáctico SLR 161
4.12. Análisis sintáctico descendente 163
4.13. Analizador con retroceso 164
4.14. Técnicas de análisis predictivo 165
4.15. Conjuntos de predicción 165
4.16. Conjunto primero 165
4.17. Conjunto siguiente 166
4.18. Factorización por la izquierda 166
4.19. Eliminación de la recursividad 166
CAPÍTULO I: INTRODUCCIÓN A LOS COMPILADORES

1.1. INTRODUCCIÓN

En un mundo informatizado como en el que vivimos, en el que cada día que pasa
dependemos más y más de un sistema informático eficiente, el cual debe estar
preparado para brindarnos la más alta calidad de servicios y prestaciones. Además
de ser sencillo en su manejo y sobre todo confiable, siendo estas características
indispensables para el usuario final. Quien no se fija, ni pregunta como se realiza
determinada tarea, pero si es buen critico a la hora de ver resultados, pero hay
otros que contrarios a estos, se hace la pregunta del millón, "¿Cómo se logra hacer
tal y tal cosa? ,"¿Cómo es posible que un graficador trabaje tan rápido?, ¿Cómo es
posible que un procesador de palabra a la hora de usar un diccionario sea tan
eficiente?, ¿Cómo es posible llevar los resultados de una aplicación a otra?, o
¿Cómo es posible que un programa que fue creado por una empresa puede trabajar
con los datos de obtenidos de otro programa, echo por otra empresa?.
Muchas pueden ser las respuestas, algunos argumentaran que es el sistema
operativo, otros dirán que son las normas y estándares establecidos, otros dirán
irónicamente que es más sencillo de lo que se piensa, dirán que se hace clac con la
rata aquí, se arrastra y se lleva a donde se quiere. Todos ellos tienen razón, sin
embargo si indagamos más a fondo. Surgirán preguntas más directas como por
ejemplo "¿Cómo se logra tal velocidad, con tan buen manejo de gráfico?", claro
que a todas ellas se puede responder diciendo, que todo se logra gracias al
Hardware, y no estaríamos totalmente errados, porque un buen Hardware conlleva
a un buen resultado, a una buena calidad de impresión en caso de volcado al papel,
una buena imagen si hablamos de gráficos, o un buen tiempo de respuesta a la
hora de realizar algún calculo matemático, pero siempre el Hardware será una
parte, solo una parte.
Es en este punto donde surge el Software, los programas, o como el modismo
denota hoy en día, las aplicaciones. Es decir que para obtener un buen resultado no
solo hace falta un buen Hardware acorde a los requerimientos de la tarea que se
quiere realizar, sino que calidad, eficiencia, velocidad, y confiabilidad hoy en día
son sinónimos de un buen trabajo en conjunto de la dupla Hardware y Software.
Dejando de lado lo que es el Hardware, y profundizando lo que representa su par,
palabra ya adoptada en nuestro idioma, y muy usada en el mundo de la
informática, deducimos que para obtener un buen software, ante todo esta el
aspecto creador de quien lo realiza, luego hay que ver cual será el entorno de
trabajo en que actuara, cuales serán los requerimientos del mismo, hay que saber
elegir que paradigma de programación se usará.
Después de formuladas estas preguntas y de haber respondido a las mismas de
manera apropiada. Hay que elegir cual es el lenguaje de programación más
conveniente para realizar dicha tarea.
Un lenguaje de programación o más genéricamente de computación, es el medio
por el cual el hombre interactúa con un ordenador. Pero el lenguaje de
computación no es lo único que se necesita para que se produzca la comunicación,
hace falta otro componente importante para completar el medio de comunicación,
y es en este punto donde surge la palabra "interprete", cuya definición dice
"persona que explica a otra en lengua que entienda" en el ámbito informático,
olvidémonos por un instante de la palabra persona, e "interpretemos" su
significado. Se puede decir entonces que a través de un intérprete podríamos pedir
a un ordenador que realice una tarea determinada sin preocuparnos de los detalles.
Claro que este interprete no es más que una aplicación (programa), que realiza la
traducción de lo que pedimos que comunique a un ordenador, por lo tanto un
interprete es capaz de conocer dos lenguajes el del emisor y el del receptor.
En el mundo de la informática existen muchos lenguajes de programación, que
trabajan con uno o varios paradigmas de programación (estilos, formas, métodos
de programación), por lo tanto es de suponer que existen distintas reglas de
sintaxis y semántica para cada lenguaje de programación como lo existe en
cualquier otro lenguaje, sea cualquiera el tipo y contexto al que pertenezca. En
tiempos ya muy remotos según cuentan los que saben proliferaban los intérpretes,
quienes tomaban la petición del usuario (hombre) e "interpretaban" la misma y se
la comunicaban al ordenador quien la ejecutaba, y esperaba una nueva petición. La
petición era declarada dentro los parámetros de definición del lenguaje usado. Con
el tiempo se vio que el estar realizando una interpretación y traducción cada vez
que se necesita realizar algo era poco efectivo, en cuanto a tiempo de trabajo del
Hardware se refiere, y más aun si se trataba de un conjunto grande de
instrucciones (peticiones), es aquí donde entraron en juego los compiladores,
quien al igual que sus antecesores realizan una traducción de los programas
(conjunto de intrusiones de un lenguaje) revisando que este dentro del marco de
definición del lenguaje de programación utilizado. Con la diferencia que la
traducción se realiza una sola vez y de todo el programa.
Después de todos estos tópicos previos, podemos decir que la calidad de un buen
software es producto de un lenguaje de computación versátil, flexible y veloz,
todo sinónimo de buen compilador, claro sin dejar de lado la capacidad creadora
del programador (usuario, nosotros).
"¿Cómo funciona un compilador? ", es la pregunta de todo aquel que se hace
llamar programador, a grandes rasgos un compilador toma un programa escrito en
un lenguaje de programación con una gramática, sintaxis y semántica definida,
revisa que este dentro de las pautas de definición del lenguaje, y lo traduce en
programa de salida escrito en lenguaje binario, el cual es entendido por el
ordenador y por lo tanto puede ser ejecutado por el mismo (recordar que un
interprete a diferencia de un compilador no traduce todo el programa sino que va
realizando la traducción e interpretación de la misma paso a paso, sin crear ningún
programa de salida ejecutable). Las partes del proceso de compilación se dividen
en dos: una llamada fase de Análisis y otra llamada fase de Sintaxis, las cuales
interactúan entre si para la creación de la tabla de símbolos y el control del
manejador de errores, dentro del análisis hay tres etapas llamadas análisis
lexicográfico, análisis sintáctico, análisis semántico. Dentro de la fase de Síntesis
existen las etapas de generación de código intermedio, optimización de código
intermedio, y generación de código.
Al tener que describir como funciona un compilador tenemos que tener en claro en
no confundir los términos compilador y compilación, se debe entender que al decir
compilador nos referimos al programa, y al decir compilación al proceso en sí. En
esencia ambos términos cumplen con la definición más simple de un compilador,
es decir, sea el programa compilador o el proceso de compilación, ambos reciben
como entrada un código escrito en algún lenguaje y producen como salida otro
código escrito en otro lenguaje.
1.2. HISTORIA

En 1946 se desarrolló el primer ordenador digital. En un principio, estas máquinas


ejecutaban instrucciones que consistían en códigos numéricos que señalan a los
circuitos de la máquina los estados correspondientes a cada operación. Pronto los
primeros usuarios de estos ordenadores descubrieron la ventaja de escribir sus
programas mediante claves más fáciles de recordar que esos códigos numéricos; al
final, todas esas claves juntas se traducían manualmente a Lenguaje Máquina.
Estas claves constituyen los llamados lenguajes ensambladores, que se
generalizaron en cuanto se dio el paso decisivo de hacer que las propias máquinas
realizaran el proceso mecánico de la traducción. A este trabajo se le llama
ensamblar el programa.
Las instrucciones de los lenguajes ensambladores obligan a programar cualquier
función de una manera minuciosa e iterativa. A pesar de ello, el lenguaje
ensamblador seguía siendo el de una máquina, pero más fácil de manejar. Los
trabajos de investigación se orientaron entonces hacia la creación de un lenguaje
que expresara las distintas acciones a realizar de una manera lo más sencilla
posible para el hombre.
Así, en 1950, desarrollaron el FORTRAN con el cual surgió así por primera vez el
concepto de un traductor (como un programa que traducía un lenguaje a otro
lenguaje). En el caso particular de que el lenguaje a traducir es un lenguaje de alto
nivel y el lenguaje traducido de bajo nivel, se emplea el término compilador.

Inicio del Compilador


La tarea de realizar un compilador no fue fácil. El primer compilador de
FORTRAN tardó 18 años en realizarse y era muy sencillo. El FORTRAN estaba
muy influenciado por la máquina objeto en la que iba a ser implementado. Como
un ejemplo de ello tenemos el hecho de que los espacios en blanco fuesen
ignorados, debido a que el periférico que se utilizaba como entrada de programas
(una lectora de tarjetas perforadas) no contaba correctamente los espacios en
blanco.
Paralelamente al desarrollo de FORTRAN en América, en Europa surgió una
corriente más universitaria, que pretendía que la definición de un lenguaje fuese
independiente de la máquina y en donde los algoritmos se pudieran expresar de
forma más simple.
Esta corriente estuvo muy influida por los trabajos sobre gramáticas de contexto
libre publicados por Chomsky. Con estas ideas surgió un grupo europeo los cuales
pidieron colaboración a la asociación americana y se formó un comité elcual
desarrolló el lenguaje llamado ALGOL 58. En 1969, el lenguaje fue revisado y
llevó a una nueva versión que se llamó ALGOL 60 y posteriormente ALGOL 68.

Avances en Compilación
Junto a este desarrollo en los lenguajes (ALGOL), también se iba avanzando en la
técnica de compilación. En 1958 proponían una solución al problema de que un
compilador fuera utilizable por varias máquinas objeto. Para ello, se dividía por
primera vez el compilador en dos fases, designadas como el "front end" y el "back
end". La primera fase (front end) es la encargada de analizar el programa fuente y
la segunda fase (back end) es la encargada de generar código para la máquina
objeto. El puente de unión entre las dos fases era un lenguaje intermedio que se
designó con el nombre de UNCOL (UNiversal Computer Oriented Language).
Para que un compilador fuera utilizable por varias máquinas bastaba únicamente
modificar su back end. Aunque se hicieron varios intentos para definir el UNCOL,
el proyecto se ha quedado simplemente en un ejercicio teórico. De todas formas,
la división de un compilador en front end y back end fue un adelanto importante.

Bases de las Tareas en un Compilador


En los años 1958 y 1959 se van poniendo las bases para la división de tareas en un
compilador. Así, en 1959 se propone el empleo de autómatas deterministas y no
deterministas para el reconocimiento lexicográfico de los lenguajes. Rápidamente
se aprecia que la construcción de analizadores léxicos a partir de expresiones
regulares es muy útil en la implementación de los compiladores. En 1975 surge el
concepto de un generador automático de analizadores léxicos a partir de
expresiones regulares, basado en el sistema operativo UNIX.
A partir de los trabajos de Chomsky ya citados, se produce una sistematización de
la sintaxis de los lenguajes de programación, y con ello un desarrollo de diversos
métodos de análisis sintáctico.
Con la aparición de la notación BNF, se tiene una guía para el desarrollo del
análisis sintáctico.
 1959: Se describe un método de parsing de FORTRAN que introducía
paréntesis adicionales alrededor de los operandos para ser capaz de analizar las
expresiones.
 1960: Se desarrollan los diversos métodos de parsers ascendentes y
descendentes
 1961: Se realiza el uso por primera vez de un parsing descendente recursivo.
 1965: Se define las gramáticas LR y describe la construcción de una tabla
canónica de parser LR.
 1968: Se estudian y definen las gramáticas LL así como los parsers
predictivos.
 1970: Se describen los métodos SLR y LALR de parser LR. Debido a su
sencillez y a su capacidad de análisis para una gran variedad de lenguajes, la
técnica de parsing LR va a ser la elegida para los generadores automáticos de
parsers.
 1975: Se crea el generador de analizadores sintácticos YACC para funcionar
bajo un entorno UNIX . Junto al análisis sintáctico, también se fue
desarrollando el análisis semántico.

En los primeros lenguajes (FORTRAN y ALGOL 60) los tipos posibles de los
datos eran muy simples, y la comprobación de tipos era muy sencilla. No se
permitía la corrección de tipos, pues ésta era una cuestión difícil.
Con la aparición del ALGOL 68 se permitía que las expresiones de tipo fueran
construidas sistemáticamente. Más tarde, de ahí surgió la equivalencia de tipos por
nombre y estructural.
También se desarrollaron estrategias para mejorar las rutinas de entrada y de salida
de un procedimiento . Así mismo, y ya desde los años 60, se estudió el paso de
parámetros a un procedimiento por nombre, valor y variable.

Optimización en la Compilación
La técnica de la optimización apareció desde el desarrollo del primer compilador
de FORTRAN. Backus comenta cómo durante el desarrollo del FORTRAN se
tenía el miedo de que el programa resultante de la compilación fuera más lento
que si se hubiera escrito a mano. Para evitar esto, se introdujeron algunas
optimizaciones en el cálculo de los índices dentro de un bucle. Pronto se
sistematizan y se recoge la división de optimizaciones independientes de la
máquina y dependientes de la máquina.
 Entre las primeras están la propagación de valores , el arreglo de expresiones,
la eliminación de redundancias, etc.
 Entre las segundas se podría encontrar la localización de registros, el uso de
instrucciones propias de la máquina y el reordenamiento de código.

A partir de 1970 comienza el estudio sistemático de las técnicas del análisis de


flujo de datos. Su repercusión ha sido enorme en las técnicas de optimización
global de un programa.

Compilación en la Actualidad
En la actualidad, el proceso de la compilación ya está muy asentado. Un
compilador es una herramienta bien conocida, dividida en diversas fases. Algunas
de estas fases se pueden generar automáticamente (analizador léxico y sintáctico)
y otras requieren una mayor atención por parte del escritor de compiladores (las
partes de traducción y generación de código).
De todas formas, y en contra de lo que quizá pueda pensarse, todavía se están
llevando a cabo varias vías de investigación en este fascinante campo de la
compilación:
 Por una parte, se están mejorando las diversas herramientas disponibles (por
ejemplo, el generador de analizadores léxicos Aardvark para el lenguaje
PASCAL).
 También la aparición de nuevas generaciones de lenguajes -ya se habla de la
quinta generación, como de un lenguaje cercano al de los humanos-ha
provocado la revisión y optimización de cada una de las fases del compilador.

El último lenguaje de programación de amplia aceptación que se ha diseñado, el


lenguaje Java, establece que el compilador no genera código para una máquina
determinada sino para una virtual, la Java Virtual Machine (JVM), que
posteriormente será ejecutado por un intérprete, normalmente incluido en un
navegador de Internet. El gran objetivo de esta exigencia es conseguir la máxima
portabilidad de los programas escritos y compilados en Java, pues es únicamente
la segunda fase del proceso la que depende de la máquina concreta en la que se
ejecuta el intérprete.

Gráfico Nº 1: Historia de la compilación

1.3. CONCEPTOS BÁSICOS

 Traductor.
Cualquier programa que toma como entrada un texto escrito en un lenguaje
llamado fuente y da como salida un programa equivalente en otro lenguaje, el
lenguaje objeto. Si el lenguaje fuente de un lenguaje de programación de alto
nivel y el objeto un lenguaje de bajo nivel (ensamblador o código de máquina),
al traductor se le denomina compilador.

 Ensamblador.
Es un programa traductor cuyo lenguaje fuente es el lenguaje ensamblador.

Gráfico Nº 2: Ensambladores
 Intérprete.
Es un programa que no genera un programa equivalente, sino que toma una
sentencia del programa fuente en un lenguaje de alto nivel y la traduce al
código equivalente y al mismo tiempo lo ejecuta.
En un principio debido a la escasez de memoria se utilizaban más los
intérpretes, ahora se usan más los compiladores (a excepción de JAVA)

1.4. CONCEPTO DE COMPILADOR

En el caso de que el lenguaje fuente sea un lenguaje de programación de alto nivel


y el objeto sea un lenguaje de bajo nivel (ensamblador o código de máquina), a
dicho traductor se le denomina compilador. Un ensamblador es un compilador
cuyo lenguaje fuente es el lenguaje ensamblador.
Históricamente, con la escasez de memoria de los primeros ordenadores, se puso
de moda el uso de intérpretes frente a los compiladores, pues el programa fuente
sin traducir y el intérprete juntos daban una ocupación de memoria menor que la
resultante de los compiladores. Por ello los primeros ordenadores personales iban
siempre acompañados de un intérprete de BASIC (Spectrum, Commodore VIC-
20, PC XT de IBM, etc.).
La mejor información sobre los errores por parte del compilador así como una
mayor velocidad de ejecución del código resultante hizo que poco a poco se
impusieran los compiladores. Hoy en día, y con el problema de la memoria
prácticamente resuelto, se puede hablar de un gran predominio de los
compiladores frente a los intérpretes, aunque intérpretes como los incluidos en los
navegadores de Internet para interpretar el código JVM de Java son la gran
excepción. La razón principal para querer usar un compilador es querer traducir un
programa de un lenguaje de alto nivel, a otro lenguaje de nivel inferior
(típicamente lenguaje máquina). De esta manera un programador puede diseñar un
programa en un lenguaje mucho más cercano a como piensa un ser humano, para
luego compilarlo a un programa más manejable por una computadora.
Programa Programa
Compilador
fuente objeto

Mensaje de error

Gráfico Nº 3: Resultado de la compilación

1.5. INTERPRETAR VS COMPILAR

Hay dos maneras de ejecutar un programa escrito en un lenguaje de alto nivel


 Compilación: traducir todo el programa a otro programa equivalente en
código máquina. Entonces se ejecuta el programa obtenido
 Interpretación: interpretar las instrucciones del programa escrito en lenguaje
de alto nivel y ejecutarla una por una.

Ventajas de compilar frente a interpretar:


 Se compila una vez, se ejecuta n veces.
 En bucles, la compilación genera código equivalente al bucle, pero
interpretándolo se traduce tantas veces una línea como veces se repite el bucle.
 El compilador tiene una visión global del programa, por lo que la información
de mensajes de error es más detallada.

Ventajas del intérprete frente al compilador:


 Un intérprete necesita menos memoria que un compilador. En principio eran
más abundantes dado que los ordenadores tenían poca memoria.
 Permiten una mayor interactividad con el código en tiempo de desarrollo.

Un compilador no es un programa que funciona de manera aislada, sino que


necesita de otros programas para conseguir su objetivo: obtener un programa
ejecutable a partir de un programa fuente en un lenguaje de alto nivel. Algunos de
esos programas son el preprocesador, el linker, el depurador y el ensamblador.
 El preprocesador se ocupa (dependiendo del lenguaje) de incluir ficheros,
expandir macros, eliminar comentarios, y otras tareas similares.
 El linker (Enlazador) se encarga de construir el fichero ejecutable añadiendo
al fichero objeto generado por el compilador las cabeceras necesarias y las
funciones de librería utilizadas por el programa fuente.
 El depurador permite, si el compilador ha generado adecuadamente el
programa objeto, seguir paso a paso la ejecución de un programa. Finalmente,
 Muchos compiladores, en vez de generar código objeto, generan un programa
en lenguaje ensamblador que debe después convertirse en un ejecutable
mediante un programa ensamblador.

Gráfico Nº 4: Diagrama a bloques de la operación de un buen compilador.


1.6. TIPOS DE COMPILADORES

El programa compilador traduce las instrucciones en un lenguaje de alto nivel a


instrucciones que la computadora puede interpretar y ejecutar. Para cada lenguaje
de programación se requiere un compilador separado. El compilador traduce todo
el programa antes de ejecutarlo. Los compiladores son, pues, programas de
traducción insertados en la memoria por el sistema operativo para convertir
programas de cómputo en pulsaciones electrónicas ejecutables (lenguaje de
máquina). Los compiladores pueden ser de:
 una sola pasada: examina el código fuente una vez, generando el código o
programa objeto.
 pasadas múltiples: requieren pasos intermedios para producir un código en
otro lenguaje, y una pasada final para producir y optimizar el código
producido durante los pasos anteriores.
 Optimación: lee un código fuente, lo analiza y descubre errores potenciales
sin ejecutar el programa.
 Compiladores incrementales: generan un código objeto instrucción por
instrucción (en vez de hacerlo para todo el programa) cuando el usuario teclea
cada orden individual. El otro tipo de compiladores requiere que todos los
enunciados o instrucciones se compilen conjuntamente.
 Ensamblador: el lenguaje fuente es lenguaje ensamblador y posee una
estructura sencilla.
 Compilador cruzado: se genera código en lenguaje objeto para una máquina
diferente de la que se está utilizando para compilar. Es perfectamente normal
construir un compilador de Pascal que genere código para MS-DOS y que el
compilador funcione en Linux y se haya escrito en C++.
 Compilador con montador: compilador que compila distintos módulos de
forma independiente y después es capaz de enlazarlos.
 Autocompilador: compilador que está escrito en el mismo lenguaje que va a
compilar. Evidentemente, no se puede ejecutar la primera vez. Sirve para hacer
ampliaciones al lenguaje, mejorar el código generado, etc.
 Metacompilador: es sinónimo de compilador de compiladores y se refiere a
un programa que recibe como entrada las especificaciones del lenguaje para el
que se desea obtener un compilador y genera como salida el compilador para
ese lenguaje. El desarrollo de los metacompiladores se encuentra con la
dificultad de unir la generación de código con la parte de análisis. Lo que sí se
han desarrollado son generadores de analizadores léxicos y sintácticos. Por
ejemplo, los conocidos:
LEX: generador de analizadores léxicos
YACC: generador de analizadores sintácticos desarrollados para UNIX. Los
inconvenientes que tienen son que los analizadores que generan no son muy
eficientes.
 Descompilador: es un programa que acepta como entrada código máquina y
lo traduce a un lenguaje de alto nivel, realizando el proceso inverso a la
compilación.
Esta taxonomía de los tipos de compiladores no es excluyente, por lo que
puede haber compiladores que se adscriban a varias categorías:
 Compiladores cruzados: generan código para un sistema distinto del que
están funcionando.
 Compiladores optimizadores: realizan cambios en el código para mejorar su
eficiencia, pero manteniendo la funcionalidad del programa original.
 Compiladores de una sola pasada: generan el código máquina a partir de una
única lectura del código fuente.
 Compiladores de varias pasadas: necesitan leer el código fuente varias veces
antes de poder producir el código máquina.
 Compiladores JIT (Just In Time): forman parte de un intérprete y compilan
partes del código según se necesitan.
CAPÍTULO II: CONCEPTOS FUNDAMENTALES DE LOS
COMPILADORES

2.1. ESTRUCTURA DE UN COMPILADOR.

Cualquier compilador debe realizar dos tareas principales: análisis del programa a
compilar y síntesis de un programa en lenguaje maquina que, cuando se ejecute,
realizara correctamente las actividades descritas en el programa fuente. Para el
estudio de un compilador, es necesario dividir su trabajo en fases. Cada fase
representa una transformación al código fuente para obtener el código objeto. La
siguiente figura representa los componentes en que se divide un compilador. Las
tres primeras fases realizan la tarea de análisis, y las demás la síntesis. En cada una
de las fases se utiliza un administrador de la tabla de símbolos y un manejador de
errores.

Gráfico Nº 5: Estructura de un compilador


2.1.1.PREPROCESADOR
Es el encargado de transformar el código fuente de entrada original en el
código fuente puro. Es decir en expandir las macros, incluir las librerías,
realizar un preprocesado racional (capacidad de enriquecer a un lenguaje
antiguo con recursos más modernos), extender el lenguaje y todo aquello que
en el código de entrada sea representativo de una abreviatura para facilitar la
escritura del mismo.

2.1.2.COMPILACIÓN
Recibe el código fuente puro, este es él modulo principal de un compilador,
pues si ocurriera algún error en esta etapa el compilador no podría avanzar. En
esta etapa se somete al código fuente puro de entrada a un análisis léxico
gráfico, a un análisis sintáctico, a un análisis semántico, que construyen la
tabla de símbolos, se genera un código intermedio al cual se optimiza para así
poder producir un código de salida generalmente en algún lenguaje
ensamblador.

2.1.3.ENSAMBLADO
Este modulo no es ni más mi menos que otro compilador pues recibe un
código fuente de entrada escrito en ensamblador, y produce otro código de
salida, llamado código binario no enlazado. Si por un momento viéramos a
este modulo como un programa independiente, veríamos que en este caso los
términos programa compilador y proceso de compilación son los mismos.
Pues este modulo no es mas que un compilador, que en su interior realiza
como su antecesor un análisis léxico gráfico, un análisis sintáctico, un análisis
semántico, crea una tabla de símbolos, genera un código intermedio lo
optimiza y produce un código de salida llamado código binario no enlazado, y
a todo este conjunto de tares se los denomina proceso de compilación. Como
se puede ver este compilador (llamado ensamblador) a diferencia de los demás
compiladores no realiza una expansión del código fuente original(código
fuente de entrada), tiene solamente un proceso de compilación y por supuesto
no enlaza el código fuente. Es un compilador que carece de los módulos de
preprocesado y enlazado, y donde los módulos de compilación y ensamblado
son los mismos.

2.1.4.ENLAZADO
El cuarto y ultimo modulo es el encargado de realizar el enlazado del código
de fuente de entrada (código maquina relocalizable) con las librerías que
necesita, como así también de proveer al código de las rutinas necesarias para
poder ejecutarse y cargarse a la hora de llamarlo para su ejecución, modifica
las direcciones relocalizables y ubica los datos en las posiciones apropiadas de
la memoria. Este ultimo modulo es el que produce como salida el código
binario enlazado. Ya sea dinámico o estático, al decir dinámico se refiere a que
el código producido utiliza librerías dinámicas (librerías ya cargadas en el
sistema), esto implica que se obtendrá un código más corto y que se
actualizara automáticamente si aparece alguna nueva versión de las librerías,
mientras que el estático se refiere al echo que no se realiza enlace con ninguna
librería y por lo tanto se obtendrá un código mas largo con una copia de las
rutinas de librería que necesita.
Biblioteca
(Archivos objeto relocalizables)

2.2. PROCESO DE COMPILACIÓN

Gráfico Nº 6: estructura del proceso de compilación


Analizando en detalle el proceso de compilación, se divide en dos grandes fases:

 Fase de Análisis
 Análisis Léxico
 Análisis Sintáctico
 Análisis Semántico
 Fase de Síntesis
 Etapa de Generación de Código Intermedio
 Etapa de Optimización de Código
 Etapa de Generación de Código

2.2.1.FASE DE ANÁLISIS
En esta fase se crea una representación intermedia de código

 Análisis léxico (scanner).


En la fase de análisis léxico se leen los caracteres del programa fuente y se
agrupan en cadenas que representan los componentes léxicos. Cada
componente léxico es una secuencia lógicamente coherente de caracteres
relativa a un identificador, una palabra reservada, un operador o un
carácter de puntuación. A la secuencia de caracteres que representa un
componente léxico se le llama lexema (o con su nombre en inglés token).
En el caso de los identificadores creados por el programador no solo se
genera un componente léxico, sino que se genera otro lexema en la tabla de
símbolos.

 Análisis sintáctico (parser).


En esta fase, los componentes léxicos se agrupan en frases gramaticales
que el compilador utiliza para sintetizar la salida.

 Análisis semántico.
La fase de análisis semántico se intenta detectar instrucciones que tengan
la estructura sintáctica correcta, pero que no tengan significado para la
operación implicada.
2.2.2.FASE DE SÍNTESIS
Genera un código a partir de la representación intermedia

 Generación de código intermedio.


Algunos compiladores generan una representación intermedia explícita del
programa fuente, una vez que se han realizado las fases de análisis. Se
puede considerar esta operación intermedia como un subprograma para
una máquina abstracta. Esta representación intermedia debe tener dos
propiedades importantes: debe ser fácil de producir y fácil de traducir al
programa objeto.

 Optimización de código.
En esta fase se trata de mejorar el código intermedio, de modo que resulte
un código de máquina más rápido de ejecutar.

 Generación de código.
Esta constituye la fase final de un compilador. En ella se genera el código
objeto que por lo general consiste en código en lenguaje máquina (código
relocalizable) o código en lenguaje ensamblador.

Además existen:
 Administrador de la tabla de símbolos.
Una tabla de símbolos es una estructura de datos que contiene un registro
por cada identificador. El registro incluye los campos para los atributos del
identificador. El administrador de la tabla de símbolos se encarga de
manejar los accesos a la tabla de símbolos, en cada una de las etapas de
compilación de un programa.

 Manejador de errores.
En cada fase del proceso de compilación es posibles encontrar errores. Es
conveniente que el tratamiento de los errores se haga de manera
centralizada a través de un manejador de errores. De esta forma podrán
controlarse más eficientemente los errores encontrados en cada una de las
fases de la compilación de un programa.
2.3. EJEMPLO DEL PROCESO DE COMPILACIÓN

Supongamos que un compilador tiene que analizar la siguiente preposición:


suma= var1 + var2 + 10

Análisis Léxico
El analizador léxico lee los caracteres del programa fuente, y verifica que
correspondan a una secuencia lógica (identificador, palabra reservada etc.). Esta
secuencia de caracteres recibe el nombre componente léxico o lexema. En este
caso el analizador léxico verifica si el identificador id1 (nombre interno para
"suma") encontrado se halla en la tabla de símbolos, si no esta produce un error
porque todavía no fue declarado, si la preposición hubiese sido la declaración del
identificador "suma" en lenguajes C, C++ (int suma;) el analizador léxico
agregaria un identificador en la tabla de símbolos, y así sucesivamente con todos
los componentes léxicos que aparezcan, los componentes léxicos resultantes de la
expresión son:
Identificador: suma.
El símbolo de asignación: =
Identificador: var1
Operador: +
Identificador: var2
Operador: +
Numero: 10
Que en el análisis léxico y con la tabla de símbolos es:
id1= id2+ id3 * 10

Análisis Sintáctico
El analizador sintáctico impone una estructura jerárquica a la cadena de
componentes léxicos, generada por el analizador léxico, que es representada en
forma de un árbol sintáctico.
=
/ \
id1 +
/ \
id2 +
/ \
id3 10

Análisis Semántico
El analizador semántico verificara en este caso que cada operador tenga los
operandos permitidos.
=
/ \
id1 +
/ \
id2 +
/ \
id3 tipo_ent
|
10

Generador de código intermedio


En esta etapa se lleva la preposición a una representación intermedia como un
programa para una maquina abstracta.
temp1= tipo_ent(10)
temp2= id3 * temp1
temp3= id2 + tem2
id1= temp3
Optimización de código
El código intermedio obtenido es representado de una forma mas optima y
eficiente.
temp1= id3 * 10.0
id1= id2 + temp1

Generador de código
Finalmente lleva el código intermedio a un código objeto que en este caso es un
código relocalizable o código ensamblador (también llamado código no enlazado).
MOVF id3, R2
MULT #10.0, R2
MOVF id2, R1
ADDF R2, R1
MOVF R1, id1

Este el código objeto obtenido que es enviado al modulo de ensamblado.


Para entender todo esto veamos un ejemplo utilizando como lenguaje en este caso
al popular lenguaje de programación C creado por Kernighan y Ritchie. El
siguiente código esta definido de acuerdo al standard ANSI C.
#include<stdio.h>
void main()
{
char* frase= " Hola Mundo...!!!";
printf("%s", frase );
};

En la primer línea se encuentra una directiva de preprocesador, esta línea le indica


al compilador que tiene que incluir la librería stdio.h, es decir transformar el
código fuente de entrada en el código fuente puro (expandido).
Al pasar por el modulo de preprocesado, el código fuente queda de la siguiente
manera.

# 1 "hmundo.c"
# 1 "c:/compilador/include/stdio.h" 1 3
# 1 " c:/compilador/include/sys/types.h" 1 3
# 12 " c:/compilador/include/stdio.h" 2 3
typedef void *va_list;
typedef long unsigned int size_t;
typedef struct {
int _cnt;
char *_ptr;
char *_base;
int _bufsiz;
int _flag;
int _file;
char *_name_to_remove;
} FILE;
typedef unsigned long fpos_t;
extern FILE __stdin, __stdout, __stderr;
void clearerr(FILE *_stream);
int fclose(FILE *_stream);
int feof(FILE *_stream);
int ferror(FILE *_stream);
int fflush(FILE *_stream);
int fgetc(FILE *_stream);
int fgetpos(FILE *_stream, fpos_t *_pos);
char * fgets(char *_s, int _n, FILE *_stream);
FILE * fopen(const char *_filename, const char *_mode);
int fprintf(FILE *_stream, const char *_format, ...);
int fputc(int _c, FILE *_stream);
int fputs(const char *_s, FILE *_stream);
size_t fread(void *_ptr, size_t _size, size_t _nelem, FILE
*_stream);
FILE * freopen(const char *_filename, const char *_mode, FILE
*_stream);
int fscanf(FILE *_stream, const char *_format, ...);
int fseek(FILE *_stream, long _offset, int _mode);
int fsetpos(FILE *_stream, const fpos_t *_pos);
long ftell(FILE *_stream);
size_t fwrite(const void *_ptr, size_t _size, size_t _nelem,
FILE *_stream);
int getc(FILE *_stream);
int getchar(void);
char * gets(char *_s);
void perror(const char *_s);
int printf(const char *_format, ...);
int putc(int _c, FILE *_stream);
int putchar(int _c);
int puts(const char *_s);
int remove(const char *_filename);
int rename(const char *_old, const char *_new);
void rewind(FILE *_stream);
int scanf(const char *_format, ...);
void setbuf(FILE *_stream, char *_buf);
int setvbuf(FILE *_stream, char *_buf, int _mode, size_t _size);
int sprintf(char *_s, const char *_format, ...);
int sscanf(const char *_s, const char *_format, ...);
FILE * tmpfile(void);
char * tmpnam(char *_s);
int ungetc(int _c, FILE *_stream);
int vfprintf(FILE *_stream, const char *_format, va_list _ap);
int vprintf(const char *_format, va_list _ap);
int vsprintf(char *_s, const char *_format, va_list _ap);
int fileno(FILE *_stream);
FILE * fdopen(int _fildes, const char *_type);
int pclose(FILE *_pf);
FILE * popen(const char *_command, const char *_mode);
extern FILE _stdprn, _stdaux;
void _stat_describe_lossage(FILE *_to_where);
int _doprnt(const char *_fmt, va_list _args, FILE *_f);
int _doscan(FILE *_f, const char *_fmt, void **_argp);
int _doscan_low(FILE *, int (*)(FILE *_get), int (*_unget)(int,
FILE *), const char *_fmt, void **_argp);
int fpurge(FILE *_f);
int getw(FILE *_f);
int mkstemp(char *_template);
char * mktemp(char *_template);
int putw(int _v, FILE *_f);
void setbuffer(FILE *_f, void *_buf, int _size);
void setlinebuf(FILE *_f);
char * tempnam(const char *_dir, const char *_prefix);
int _rename(const char *_old, const char *_new);
# 1 "hmundo.c" 2
void main()
{
char* frase= " Hola Mundo...!!!";
printf("%s", frase );
};

El nuevo código contiene el encabezado o prototipo de la/s función/es que se


encuentran en el archivo de cabecera stdio.h, y que serán posiblemente utilizadas
en el código fuente original. Este código es pasado al modulo de compilación
quien luego de analizarlo y verificar si se encuentra correcto, transformara el
código fuente puro (expandido) en código ensamblador y lo envía al modulo de
ensamblado.

.file "hmundo.c"
compiler_compiled.:
___compiled_c:
.text
LC0:
.ascii " Hola Mundo...!!!\0"
LC1:
.ascii "%s\0"
.align 2
.globl _main
_main:
pushl %ebp
movl %esp,%ebp
subl $4,%esp
call ___main
movl $LC0,-4(%ebp)
movl -4(%ebp),%eax
pushl %eax
pushl $LC1
call _printf
addl $8,%esp
L1:
leave
ret

Este código será analizado por él modulo de ensamblado, que lo llevara a código
binario no enlazado, y lo enviara al modulo de enlazado. El código de salida
enviado al modulo de enlazado es el siguiente.

L&#0;³Ú(7ô&#0;.text&#0;
@&#0;Œ&#0;Ì&#0;&#0; &#0;
.............
.data&#0;@&#0;@&#0;@&#0;
.bss&#0;@&#0;@&#0;&#128;
Hola Mundo...!!!&#0;%s&#0;ヘ v&#0;
U‰åƒìèÝÿÿÿÇEü&#0;‹EüPh&#0;
èÈÿÿÿƒÄ Éà ヘ v&#0;.file&#0;þÿ&#0;
ghmundo.c&#0;&#0;&#0;&#0;.
.............
_main&#0;&#0;&#0;&#0;
___main&#0;&#0;
_printf&#0;&#0;%&#0;
_compiled.&#0;
___compiled_c&#0;
Finalmente él modulo de enlazado, vincula el código binario sin enlazar con las
librerías dinámicas necesarias según sea el caso o no. Y produce como salida el
código binario enlazado o código binario ejecutable.
MZ&#0;&#0; &#0;'&#0;ÿÿ&#0;`&#0;T&#0;
$Id: xxx.asm built mm/dd/aa 00:00:00 by ...asm $
@(#) xxx.asm built mm/dd/aa 00:00:00 by ...asm
.............
]ヘ v&#0; Hola Mundo...!!!&#0;%s&#0;ヘ v&#0;
U‰åƒìèý &#0;ÇEüx"‹EüPhŠ"è, &#0;ƒÄ Éà ヘ v&#0;387
No &#0;80387 detected.
Warning: Coprocessor not present and DPMI setup failed!
If application attempts floating operations system may hang!
¸'&#0;ÉÃ ヘ v&#0;¸"&#0;ÉÃCall frame traceback EIPs:
0x&#0;
0x&#0;
Alignment Check&#0;Coprocessor Error&#0;Page fault&#0;General
Protection Fault&#0;Stack
Fault&#0;Segment Not Present&#0;Invalid TSS&#0;Coprocessor
overrun&#0;Double
............
Division by Zero&#0;: sel=&#0; invalid&#0; base=&#0; limit=&#0;
ヘ v&#0;U‰åƒìS‹]jÿu jè?&#0;
............

Este es el código final conocido como código maquina.

2.4. HERRAMIENTAS PARA CONSTRUCCIÓN DE COMPILADORES

 Sistema de ayuda para escribir compiladores


 Generadores de compiladores
 Sist. Generadores de traductores

 Herramientas generales para el diseño automático de componentes


específicos de un comp.
 Utilizan lenguajes específicos para especificar e implementar la
componente
 Ocultan detalles del algoritmo de generación
 Producen componentes que se pueden integrar al resto del compilador

 Generadores de analizadores sintácticos


 Producen AS a partir de una Gramática Libre de Contexto
 Hoy esta es una de las fases más fáciles de aplicar

 Generadores de analizadores léxicos


 Producen AL a partir de una especificación en Expresiones Regulares.
 El AL resultante es un Autómata Finito

 Dispositivos de traducción dirigida por la sintaxis


 Producen grupos de rutinas que recorren el árbol de AS generando código
intermedio

 Generadores automáticos de código


 Las proposiciones en cod. Int. se reemplazan por plantillas que representan
secuencia de instrucciones de máquina

 Dispositivos para análisis de flujo de datos


 Inf. sobre como los valores se transmiten de una parte a otra del programa

 LEX y YACC
 Herramientas que nos permiten desarrollar componentes o la mayor parte
de un compilador
 Son un recurso invaluable para el profesional y el investigador
 Existen paquetes freeware

2.5. EL LENGUAJE Y LA HERRAMIENTA

Tabla Nº 1: lenguajes y la herramienta


MODELO LENGUAJE CARACTERÍSTICAS
Fortran, COBOL, C/C++, Sintaxis rigurosa, velocidad y
Compilado
Pascal tamaño
Lisp, AWK; BASIC; Desempeño lento, actividades no
Interpretado
SQL planeadas, sintaxis relajadas
Transportabilidad absoluta,
Pseudocompilado Java desempeño intermedio, sintaxis
rigurosa

2.6. ASPECTOS ACADÉMICOS Y DE INVESTIGACIÓN DE COMPILADORES

Tabla Nº 2: Aspectos académicos de la investigación de compiladores


ÁREA BENEFICIOS
 Lenguaje de programación Principios para su desarrollo
Herramientas para la implementación

 Inteligencia artificial Interfases de reconocimiento de lenguaje


natural

 Sistemas operativos Desarrollo de interfases de control y


usuario final. Interpretes de comandos
(Shell)

 Diseño de interfases Desarrollo de interfases orientadas a


comando y carácter. Voz o escritura.

 Administración de proyectos Selección de herramientas de desarrollo.


informáticos Evaluación de costo de beneficio
CAPÍTULO III: ANÁLISIS LÉXICO

3.1. ANÁLISIS LÉXICO (SCANNER)

El analizador léxico, también conocido como scanner, lee los caracteres uno a uno
desde la entrada y va formando grupos de caracteres con alguna relación entre sí
(tokens), que constituirán la entrada para la siguiente etapa del compilador. Cada
token representa una secuencia de caracteres que son tratados como una única
entidad. Por ejemplo, en Pascal un token es la palabra reservada BEGIN, en C:
WHILE, etc.
Las tiras específicas sólo tienen tipo (lo que representan), mientras que las tiras no
específicas tienen tipo y valor. Por ejemplo, si “Contador” es un identificador, el
tipo de token será identificador y su valor será la cadena “Contador”.
El Analizador Léxico es la etapa del compilador que va a permitir saber si es un
lenguaje de formato libre o no. Frecuentemente va unido al analizador sintáctico
en la misma pasada, funcionando entonces como una subrutina de este último. Ya
que es el que va leyendo los caracteres del programa, ignorará aquellos elementos
innecesarios para la siguiente fase, como los tabuladores, comentarios, espacios en
blanco, etc.

3.2. EL PROCESO DEL ANÁLISIS LÉXICO

El proceso de análisis léxico se refiere al trabajo que realiza el scanner con


relación al proceso de compilación. El scanner representa una interfaz entre el
programa fuente y el analizador sintáctico o parser. El scanner, a través del
examen carácter por carácter del texto, separa el programa fuente en piezas
llamadas tokens, los cuales representan los nombres de las variables, operadores,
etiquetas, y todo lo que comprende el programa fuente.
El parser, usualmente genera un árbol de sintaxis del programa fuente como ha
sido definido por una gramática. Las hojas del árbol son símbolos terminales de la
gramática. Son esos símbolos terminales o tokens los que el scanner extrae del
código fuente y se los pasa al parser. Es posible para el parser usar el conjunto de
caracteres terminales del lenguaje como el conjunto de tokens, pero ya que los
tokens pueden ser definidos en términos de gramáticas regulares más simples que
en las gramáticas más complejas utilizadas por los parsers, es deseable usar
scanners. Usar solo parsers es costoso en términos de tiempo de ejecución y
requerimientos de memoria, y la complejidad y el tiempo de ejecución puede
reducirse con el uso de un scanner.
La separación entre análisis léxico (scanning) y análisis sintáctico (parsing) puede
tener también otras ventajas. El análisis léxico de caracteres generalmente es lento
en los compiladores, y separándolo del componente de análisis semántico de la
compilación, el énfasis particular puede darse para hacer más eficiente el proceso.
Un analizador de léxico tiene como función principal el tomar secuencias de
caracteres o símbolos del alfabeto del lenguaje y ubicarlas dentro de categorías,
conocidas como unidades de léxico. Las unidades de léxico son empleadas por el
analizador gramatical para determinar si lo escrito en el programa fuente es
correcto o no gramaticalmente. Algunas de las unidades de léxico no son
empleadas por el analizador gramatical sino que son descartadas o filtradas. Tal es
el caso de los comentarios, que documentan el programa pero que no tienen un
uso gramatical, o los espacios en blanco, que sirven para dar legibilidad a lo
escrito.
En la terminología empleada en la construcción de un analizador de léxico se
encuentran los siguientes términos.

3.3. ¿QUE ES UN ANALIZADOR LÉXICO?

Se encarga de buscar los componentes léxicos (tokens En ingles) o palabras que


componen el programa fuente, según unas reglas o patrones. La entrada del
analizador léxico podemos definirla como una secuencia de caracteres.

Gráfico Nº 7: Analizador léxico

El analizador léxico tiene que dividir la secuencia de caracteres en palabras con


significado propio y después convertirlo a una secuencia de terminales desde el
punto de vista del analizador sintáctico, que es la entrada del analizador sintáctico.
El analizador léxico reconoce las palabras en función de una gramática regular de
manera que sus NO TERMINALES se convierten en los elementos de entrada de
fases posteriores. En LEX, por ejemplo, esta gramática se expresa mediante
expresiones regulares.

3.4. FUNCIONES DEL ANALIZADOR LÉXICO

El analizador léxico es la primera fase de un compilador. Su principal función


consiste en leer los caracteres de entrada y elaborar como salida una secuencia de
componentes léxicos que utiliza el analizador sintáctico para hacer el análisis. Esta
interacción, suele aplicarse convirtiendo al analizador léxico en una subrutina o
corrutina del analizador sintáctico. Recibida la orden “Dame el siguiente
componente léxico” del analizador sintáctico, el analizador léxico lee los
caracteres de entrada hasta que pueda identificar el siguiente componente léxico.
Estos componentes léxicos representan:

 palabras reservadas: if, while, do, . . .


 identicadores: asociados a variables, nombres de funciones, tipos definidos
por el usuario, etiquetas,... Por ejemplo:
 Forma: una letra seguida de letras o números. Ej. a, b1, c3D
 Atributo nombre: string con la secuencia de caracteres que forma el
identificador en mayúsculas. Ej. “A”, “B1”, “C3D”
 operadores: = * + - / == > < &! = . . .
 símbolos especiales: ; ( ) [ ] f g ...
 constantes numéricas: literales que representan valores enteros, en coma
flotante, etc, 982, 0xF678, -83.2E+2,...
 Forma: secuencia de dígitos que puede empezar con el signo menos y
puede contener un punto. Ej. 10, -3, 15.4, -54.276, .10
 Atributo valor: Double con el valor numérico.
 Precisión: entero o real.
 constantes de caracteres: literales que representan cadenas concretas de
caracteres, \hola mundo",...
Gráfico Nº 8: Interacción de un analizador léxico con el analizador sintáctico

Otras funciones que realiza:


 Eliminar los comentarios del programa.
 Eliminar espacios en blanco, tabuladores, retorno de carro, etc, y en general,
todo aquello que carezca de significado según la sintaxis del lenguaje.
 Reconocer los identificadores de usuario, números, palabras reservadas del
lenguaje,..., y tratarlos correctamente con respecto a la tabla de símbolos (solo
en los casos que debe de tratar con la tabla de símbolos).
 Llevar la cuenta del número de línea por la que va leyendo, por si se produce
algún error, dar información sobre donde se ha producido.
 Avisar de errores léxicos. Por ejemplo, si @ no pertenece al lenguaje, avisar de
un error.
 Puede hacer funciones de preprocesador.

3.5. NECESIDAD DEL ANALIZADOR LÉXICO

Un tema importante es el porqué se separan los dos análisis lexicográfico y


sintáctico, en vez de realizar sólo el análisis sintáctico, del programa fuente, cosa
perfectamente posible aunque no plausible. Algunas razones de esta separación
son:
 Un diseño sencillo es quizás la consideración más importante. Separar el
análisis léxico del análisis sintáctico a menudo permite simplificar una u otra
de dichas fases. El analizador léxico nos permite simplificar el analizador
sintáctico.
Gráfico Nº 9: Necesidad del analizador léxico

Si el sintáctico tuviera la gramática de la Opción 1 , el lexicográfico sería:


Opción 1: ( 0 | 1 | 2 | ... | 9) +  NUM
(“+” | “-” | ”*“ | ”/“)  OPARIT

Si en cambio el sintáctico toma la Opción 2, el lexicográfico sería:


Opción 2: ( 0 | 1 | 2 | ... | 9) +  NUM
“+”  MAS
“-”  MENOS
“*”  MULT
“/”  DIV

Es más, si ni siquiera hubiera análisis léxico, el propio análisis sintáctico vería


incrementado su número de reglas:
NUM  0
|1
|2
|3
....
| NUM NUM
3.6. VENTAJAS DE SEPARAR EL ANÁLISIS LÉXICO Y EL ANÁLISIS
SINTÁCTICO:

 Facilita transportabilidad del traductor (por ejemplo, si decidimos en un


momento dado cambiar las palabras reservadas begin y end de inicio y en de
bloque, por f y g, solo hay que cambiar este modulo.
 Se simplifica el diseño: el analizador es un objeto con el que se interactúa
mediante ciertos métodos. Se localiza en un único modulo la lectura física de
los caracteres, por lo que facilita tratamientos especializados de E/S.
 Se mejora la eficiencia del compilador. Un analizador léxico independiente
permite construir un procesador especializado y potencialmente más eficiente
para esa función.
 Gran parte del tiempo se consume en leer el programa fuente y dividirlo en
componentes léxicos. Con técnicas especializadas de manejo de buffers para la
lectura de caracteres de entrada y procesamiento de componentes léxicos se
puede mejorar significativamente el rendimiento de un compilador.
 Otra razón por la que se separan los dos análisis es para que el analizador
léxico se centre en el reconocimiento de componentes básicos complejos.

3.7. COMPONENTES LÉXICOS, PATRONES, LEXEMAS

3.7.1.COMPONENTE LÉXICO O TOKEN


El valor asociado a una categoría o unidad de léxico. Se representa como un
número entero o una constante de un byte. Ejemplo: el token de un
identificador puede ser 1 ó id (si id fue definida como 1).
Los tokens son las unidades léxicas básicas de igual forma que las palabras y
signos de puntuación son las unidades básicas de un enunciado. Los tokens
varían del lenguaje al lenguaje e incluso de compilador a compilador para el
mismo lenguaje, la elección de los tokens es tarea del diseñador del
compilador.
En la mayoría de lenguajes tendremos tokens para :
 palabras clave: IF THEN THEN THEN = ELSE; ELSE ELSE = THEN;
 operadores
 identificadores
 constantes (reales, enteras y de tipo carácter), strings de caracteres y
signos de puntuación

1. Tipos de tokens:
 tiras específicas, tales como palabras reservadas (if, while, begin,
etc.), el punto y coma, la asignación, los operadores aritméticos o
lógicos, etc.
 tiras no específicas, como identificadores, constantes o etiquetas.

2. Prioridad de los tokens


 Se da prioridad al token con el lexema más largo:
Si se lee “>=” y “>” se reconoce el primero.
 Si el mismo lexema se puede asociar a dos tokens, estos patrones
estarán definidos en un orden determinado.

Ejemplo:

 While → palabra reservada “while”


 letra (letra | digito)* → identificador
 Si en la entrada aparece “while”, se elegirá la palabra reservada por
estar primero.
 Si estas especificaciones iniciales aparecieran en orden inverso, se
reconocería un token identificador.

3.7.2.PATRÓN O EXPRESIÓN REGULAR


Definen las reglas que permiten identificar los componentes léxicos o tokens.

3.7.3.LEXEMA
Es cada secuencia de caracteres concreta que encaja con un patrón, es decir, es
como una instancia de un patrón.
Ejm: 8, 23, 50 (son lexemas que encajan con el patrón ( 0 | 1 | 2 | ... | 9) + )
Una vez detectado que un grupo de caracteres coincide con un patrón, se ha
detectado un lexema. A continuación se le asocia un número, que se le pasará
al sintáctico, y, si es necesario, información adicional, como puede ser una
entrada en la tabla de símbolos.
La tabla de símbolos suelen ser listas encadenadas de registros con parte
variable: listas ordenadas, árboles binarios de búsqueda, tablas hash, etc.
Ejm: Hacer un analizador léxico que nos reconozca los números enteros, los
números reales y los identificadores de usuario. Vamos a hacer este ejemplo en
C.
Terminales Expresión Regular
( 0 ... 9) +  NUM_ENT
(0 ... 9)*. (0 ... 9) +  NUM_REAL
(a ... z) (a ... z 0 ... 9) *  ID

Asociado a la categoría gramatical de número entero tendremos el token


NUM_ENT que puede equivaler por ejemplo al número 280; asociado a la
categoría gramatical número real tendremos el token NUM_REAL que
equivale al número 281; asociado a la categoría gramatical identificador de
usuario tendremos el token ID que equivale al número 282.
( 0 ... 9) + { return 280;}
(0 ... 9)*. (0 ... 9) + { return 281;}
(a ...z) (a ...z 0...9) * { return 282;}

Si tuviéramos como texto de entrada el siguiente:


95.7 99 hola
El analizador léxico intenta leer el lexema más grande; el 95 encaja con el
primer patrón, pero sigue, al encontrarse el punto, se da cuenta de que también
encaja con el segundo patrón, entonces como este es más grande, toma la
acción del segundo patrón, return NUM_REAL. El 99 coincide con el patrón
NUM_ENT, y la palabra con ID. Los espacios en blanco no coinciden con
ningún patrón.

En vez de trabajar con los números 280, 281, 282, se definen mnemotécnicos.
# define NUM_ENT 280
# define NUM_REAL 281
# define NUM_ID 282
(“ ”\t \n)
(0 ... 9) + {return NUM_ENT;}
(0 ... 9) *. (0 ... 9) + {return NUM_REAL;}
(a ... z) (a ... z 0 ... 9)* {return ID;}

Las palabras que entran por el patrón (“ ”\t \n) no tienen acción asociada, por
lo que , por defecto, se consideran solo espaciadores.

Ejemplo:
Programa UNO;
Inicio
Escribe (“Hola”);
Fin.
Cuya tabla de tokens es la siguiente:

Programa Palabra reservada


UNO Identificador
; Delimitador
Inicio Palabra reservada
Escribe Palabra reservada
( Símbolo
“Hola” Identificador compuesto
) Símbolo
; Delimitador
Fin. Palabra reservada

3.8. DESCRIPCIÓN DE UN ANALIZADOR LÉXICO

El análisis léxico es un análisis de los caracteres:


 Parte de éstos y por medio de patrones reconoce los lexemas
 Envía al analizador sintáctico el componente léxico y sus atributos
 Puede hacer tareas adicionales: eliminar blancos, control líneas.
Gráfico Nº 10: Descripción del analizador léxico

El analizador léxico y el sintáctico forman un par productor-consumidor.


En algunas situaciones, el analizador léxico tiene que leer Algunos caracteres por
adelantado para decidir de qué token se trata.

Gráfico Nº 11: Par Productor-Consumidor

3.9. UNIDADES DE LÉXICO

Categorías en que se clasifican las cadenas de caracteres válidos en un lenguaje.


Los caracteres válidos reciben el nombre de alfabeto. Por ejemplo, el alfabeto de
Pascal es:
A-Z, a-z, 0-9, _, =, :, ;, ,, , -, ', ", *, /, (, ), [, ], ., <, > y las unidades de léxico para
pascal son:
 identificadores
 literales numéricas
 operadores aritméticos
 cadenas de caracteres
 separadores
 operadores relacionales
 operadores lógicos
 comentarios
 Con respecto al lenguaje para controlar al ROBOT, tenemos que su alfabeto
es: n,o,r,t,e,s, ,u,i,c y las unidades de léxico son:
 órdenes
 (norte, sur, este, oeste, inicio)
 y espacios en blanco.

3.10.EL ROL DEL ANALIZADOR LÉXICO

Aunque el analizador de léxico es la primera etapa del proceso de compilación, no


es quien lo inicia. Pudiera considerarse que el analizador de léxico hace su
procesamiento y envía sus resultados al analizador gramatical, como
secuencialmente se aprecia en el proceso de compilación; no es así: La
compilación empieza con el analizador gramatical quien solicita un token para
realizar su trabajo; el analizador de léxico reune símbolos y envía el token
correspondiente a la unidad de léxico que conformó al analizador gramatical y
espera una nueva solicitud de token. Como se aprecia en la figura siguiente, el
analizador de léxico está supeditado por el analizador gramatical.

Gráfico Nº 12: Rol del analizador léxico

Durante estas etapas se tiene comunicación con la tabla de símbolos que concentra
información de las entidades empleadas en el programa.
3.11.TRATAMIENTO DE LOS ERRORES

Un traductor debe adoptar alguna estrategia para detectar, informar y recuperarse


para seguir analizando hasta el final.
Las respuestas ante el error pueden ser:
 Inaceptables: Provocadas por fallos del traductor, entrada en lazos infinitos,
producir resultados erróneos, y detectar sólo el primer error y detenerse.
 Aceptables: Evitar la avalancha de errores (mala recuperación) y, aunque
más complejo, informar y reparar el error de forma automática.
La conducta de un Analizador de Léxico es el de un Autómata finito o
“scanner”.
 Detección del error: El analizador de Léxico detecta un error cuando no
existe transición desde el estado que se encuentra con el símbolo de la
entrada. El símbolo en la entrada no es el esperado.

Los errores léxicos se detectan cuando el analizador léxico intenta reconocer


componentes léxicos y la cadena de caracteres de la entrada no encaja con ningún
patrón. Son situaciones en las que usa un carácter invalido (@,$,",>,...), que no
pertenece al vocabulario del lenguaje de programación, al escribir mal un
identificador, palabra reservada u operador.
Errores léxicos típicos son:

1. nombre ilegales de identificadores: un nombre contiene caracteres inválidos.


2. Números incorrectos: un numero contiene caracteres inválidos o no esta
formado correctamente, por ejemplo 3,14 en vez de 3.14 o 0.3.14.
3. errores de ortografía en palabras reservadas: caracteres omitidos, adicionales o
cambiados de sitio, por ejemplo la palabra while en vez de while.
4. fin de archivo: se detecta un fin de archivo a la mitad de un componente
léxico.
Los errores léxicos se deben a descuidos del programador. En general, la
recuperación de errores léxicos es sencilla y siempre se traduce en la generación
de un error de sintaxis que será detectado mas tarde por el analizador sintáctico
cuando el analizador léxico devuelve un componente léxico que el analizador
sintáctico no espera en esa posición.
Los métodos de recuperación de errores léxicos se basan bien en
saltarse caracteres en la entrada hasta que un patrón se ha podido reconocer; o bien
usar otros métodos más sofisticados que incluyen la inserción, borrado, sustitución
de un carácter en la entrada o intercambio de dos caracteres consecutivos. Una
buena estrategia para la recuperación de errores léxicos:
 si en el momento de detectar el error ya hemos pasado por algún estado final
ejecutamos la acción correspondiente al ultimo estado final visitado con el
lexema formado hasta que salimos de el; el resto de caracteres leídos se
devuelven al flujo de entrada y se vuelve al estado inicial;
 Si no hemos pasado por ningún estado final, advertimos que el carácter
encontrado no se esperaba, lo eliminamos y proseguimos con el análisis.

3.12.TRATAMIENTO DE PALABRAS RESERVADAS

Son aquellas que los lenguajes de programación “reservan” para usos particulares.
¿Cómo diferenciarlas de los identificadores?

 Resolución implícita: reconocerlas todas como identificadores, utilizando


una tabla adicional con las palabras reservadas que se consulta para ver si el
lexema reconocido es un identificador o una palabra reservada.
 Resolución explícita: se indican todas las expresiones regulares de todas las
palabras reservadas y se integran los diagramas de transiciones resultantes de
sus especificaciones léxicas en la máquina reconocedora.

3.13.CONSTRUCCIÓN DE UN ANALIZADOR LÉXICO

Los analizadores léxicos pueden construirse:


 Usando generadores de analizadores léxicos: Es la forma más sencilla
pero el código generado por el analizador léxico es más difícil de mantener y
puede resultar menos eficiente.
 Escribiendo el analizador léxico en un lenguaje de alto nivel: permite
obtener analizadores léxicos con más esfuerzo que con el método anterior
pero más eficientes y sencillos de mantener.
 Escribiendo el analizador léxico en un lenguaje ensamblador: Sólo se
utiliza en casos específicos debido a su alto coste y baja portabilidad.

3.14.CONCEPTO DE EXPRESIÓN REGULAR

El objetivo de las expresiones regulares es representar todos los posibles lenguajes


definidos sobre un alfabeto ∑, basándose en una serie de lenguajes primitivos, y
unos operadores de composición. Lenguajes primitivos serian el lenguaje vació, el
lenguaje formado por la palabra vacía, y los lenguajes correspondientes a los
distintos símbolos del alfabeto. Los operadores de composición son la unión, la
concatenación, el cierre y los paréntesis.

3.15.DEFINICIÓN DE EXPRESIÓN REGULAR

Dado un alfabeto finito ∑, las expresiones regulares sobre ∑ se definen de forma


recursiva por las siguientes reglas:
1. Las siguientes expresiones son expresiones regulares primitivas:
 φ
 λ
 α, con α ∈ ∑
2. Sean α y β expresiones regulares, entonces son expresiones regulares
derivadas:
 α + β (union)
 α . β (o simplemente αβ) (concatenacion)
 α* (cierre) (A* repetición l|A|AA|AAA..)
 (α)
3. No hay más expresiones regulares sobre ∑ que las construidas mediante estas
reglas.

Observación: La precedencia de los operadores es la siguiente (de mayor a


menor):
1. ( )
2. * cierre
3. . concatenación
4. + unión

Ejemplo: Algunos ejemplos de expresión regular son:


(0 + 1)*01
(aa + ab + ba + bb)*
a*(a + b)
(aa)*(bb)*b

3.16.OPERACIONES DE EXPRESIONES REGULARES

 Selección entre alternativas. la cual se indica mediante el metacaracter |


 Concatenación. La concatenacion entre dos expresiones regulares R y S se
expresa por RS.
 Repetición. Se indica mediante el metacaracter *

3.17.LENGUAJE DESCRITO POR UNA EXPRESIÓN REGULAR

Sea r una expresión regular sobre ∑. El lenguaje descrito por r, L(r), se define
recursivamente de la siguiente forma:

Ejemplo:
L(a*(a+b)) = L(a*)L((a+b)) = L(a)*L(a+b) = L(a)*(L(a) ∪ L(b)) = {a}*({a}∪
{b})
= {λ, a, aa, aaa,...}{a,b}
= {a, aa, ..., b, ab, aab, ...} = {an| n ≥ 1}∪{ an b | n ≥ 0}.
L((aa)*(bb)*b)= {a2nb2m+1 | n,m ≥ 0}.

Si ∑ = {a,b,c},entonces L(( a + b + c) )= ∑*
 L(a*(b + c))
 L(0*10*)

3.18.TEOREMAS DE EQUIVALENCIA

Tal como indica su nombre, mediante expresiones regulares se pueden representar


lenguajes regulares. De hecho, la clase de lenguajes que se pueden representar
mediante una expresión regular, es equivalente a la clase de lenguajes regulares.
Hasta ahora hemos visto que los lenguajes regulares pueden describirse mediante:
 Gramáticas lineales por la izquierda,
 Gramáticas lineales por la derecha,
 Autómatas finitos deterministas,
 Autómatas finitos no deterministas.

Por tanto, deben existir algoritmos que permitan obtener un autómata o una
gramática regular a partir de una expresión regular y viceversa.

3.19.MATRICES DE TRANSICIÓN

Una matriz o tabla de transiciones es un arreglo bidimensional cuyos elementos


proporcionan el resumen de un diagrama de transiciones. Para elaborar una tabla
de transiciones, debe colocarse cada estado del diagrama de transiciones en una
fila del arreglo y cada símbolo o categoría de símbolos con posibilidades de
ocurrencia en la cadena de entrada, en una columna. El elemento que se encuentra
en la fila m columna n es el estado que se alcanzaría en el diagrama de
transiciones al dejar al estado m a través de un arco de etiqueta n. Al no existir
algún arco que salga del estado m, entonces la casilla correspondiente de la tabla
se marca como un estado de error. En la siguiente figura se presenta un ejemplo de
un diagrama de transiciones que representa la sintaxis para un número de punto
flotante, seguido de la tabla de transiciones correspondiente.

Estado Dígito . E + - FDC


1 2 Error Error Error Error Error
2 2 3 5 Error Error Error
3 4 Error Error Error Error Error
4 4 Error 5 Error Error Aceptar
5 7 Error Error 6 6 Error
6 7 Error Error Error Error Error
7 7 Error Error Error Error Aceptar

Existe también La matriz de transición de estados se creó dando valores de estado


a cada uno de los tipos de palabra y utilizando las reglas de la gramática respecto a
la relación que existe entre cada tipo de palabra

3.20.REPRESENTACIÓN DE LOS AUTÓMATAS

Gráfico Nº 13: representación de un autómata

Acepta las secuencias: abc(dc)*


Ej. abc, abcdc, abcdcdc, abcdcdc...

3.21.AUTÓMATA FINITO DETERMINISTA

Un autómata finito determinista consiste en un dispositivo que puede estar en un


estado de entre un número finito de los mismos; uno de ellos será el estado inicial
y por lo menos uno será estado de aceptación. Tiene un flujo de entrada por el cual
llegan los símbolos de una cadena que pertenecen a un alfabeto determinado. Se
detecta el símbolo y dependiendo de este y del estado en que se encuentre hará una
transición a otro estado o permanece en el mismo. El mecanismo de control
(programa) es que determina cual es la transición a realizar. La palabra finito se
refiere a que hay un número finito de estados.
La palabra determinista es porque el mecanismo de control (programa) no debe
tener ambigüedades, es decir, en cada estado solo se puede dar una y solo una (ni
dos ni ninguna) transición para cada símbolo posible (en el ejemplo anterior, la
tabla de transiciones era determinista en ese caso, no así el diagrama, aunque
podría serlo como veremos mas tarde).
El autómata acepta la cadena de entrada si la máquina cambia a un estado de
aceptación después de leer el último símbolo de la cadena. Si después del último
símbolo la máquina no queda en estado de aceptación, se ha rechazado la cadena.
Si la máquina llega al final de su entrada antes de leer algún símbolo la entrada es
una cadena vacía (cadena que no contiene símbolos) y la representaremos con λ.
Solo aceptará λ si su estado inicial es de aceptación.

Un autómata finito determinista (AFD) consiste en una quíntupla (Q, ∑, δ, q0, F)


donde:
 Q es un conjunto finito de estados
 ∑ es el alfabeto de la máquina
 δ :Q x ∑,Q (es la función total de transición)
 q0 ∈ Q es el estado inicial
 F ⊆ Q es el conjunto de los estados de aceptación (estados finales).

Lee los caracteres de derecha a izquierda


Para cada carácter leído, si para un a ∈ ∑ y q, p ∈ S se tiene que δ(q, a) = p,
significa que siempre que el automata este en el estado q y le llega el carácter a
pasará al estado p

Ejemplo:
Automata que acepta cadenas con un número par de ceros y un numero par de
unos:
AFD = {S, A, B, C}, {0,1},δ, Q, {Q}

M se puede definir extensivamente con:

Representación gráfica:

3.22.AUTÓMATA FINITO NO DETERMINISTA

La única diferencia con los AFD está en que en la transición en un estado


determinado puede haber, para un mismo símbolo, más de un arco o no haber
ninguno.
Decimos que un autómata finito no determinista acepta una cadena si es posible
que su análisis deje a la máquina en un estado de aceptación. Decimos si es
posible, pues si se toma el camino equivocado no se aceptaría una cadena que
podría ser válida (una cadena del lenguaje aceptado por este autómata, designado
por L(M).
Un autómata finito no determinista (AFN o AFND) consiste en una quíntupla:
(Q, ∑, δ, q0, F) donde:
 Q es un conjunto finito de estados posibles del automata
 ∑ es el alfabeto de la máquina
 δ :Q x ∑,2Q (es la función total de transición)
 q0 ∈ Q es el estado inicial
 F ⊆ Q es el conjunto de los estados de aceptación (estados finales).

Ejemplo:
AFND que reconoce en {a, b, c}* tales que el ultimp símbolo en la caden de
entrada aparecía también anteriormente en la cadena. En este AFND, seria
F = {q0, q1, q2, q3, q4}, {a, ,b, c}, δ, q0, { q4}

Sea la entrada aca:


Concluyendo:

δ(q0, aca) = { q0, q1, q3} ∪ { q1, q4}


= { q0, q1, q3, q4}
Y como δ(q0, aca) ∩ F = {q4}, la cadena se acepta.
CAPÍTULO IV: ANÁLISIS SINTÁCTICO

4.1. GRAMÁTICAS

 Tipo 0 o Sin Restricciones o Estructuradas por Fase (MT: Maquinas de


Turing )
G = (N, T, P, S)
N: Conjunto de Símbolos No Terminales
T: Conjunto de Símbolos Terminales
P: Conjunto de Reglas de Producción
S∈N: Símbolo Inicial

 Tipo 1 o Sensibles al Contexto (CSG) (ALA: Autómata Linealmente


Acotado )
G = (N, T, P, S)
N: Conjunto de Símbolos No Terminales
T: Conjunto de Símbolos Terminales
P: Conjunto de Reglas de Producción
S∈N: Símbolo Inicial
P⊆ (T∪Vn)* Vn (T∪Vn)* x (T∪Vn)*
α β con |α| ≤ |β|

 Tipo 2 o Libres de Contexto (CFG) (AP: Autómatas de Pila)


G = (N, T, P, S)
N: Conjunto de Símbolos No Terminales
T: Conjunto de Símbolos Terminales
P: Conjunto de Reglas de Producción
S∈N: Símbolo Inicial
P⊆ N x (T∪Vn)*

Ejemplo:
Supóngase que utilizamos E en lugar de <expresion> para la variable de la
gramática. Podemos expresar esta gramática de la manera formal como:
G = (N,T,P,S) Donde: T = { + , * , ( , ) , id }
N={E }
P={EE+E
EE*E
E(E)
E  id }
S=E

 Tipo 3 o Regulares (AF: Autómatas Finitos)


Definen la sintaxis de los identificadores, números, cadenas y otros símbolos
básicos del lenguaje.

G = (N, T, P, S)
N: Conjunto de Símbolos No Terminales
T: Conjunto de Símbolos Terminales
P: Conjunto de Reglas de Producción
S∈N: Símbolo Inicial

Regular a Derecha: P ⊆ N x (TN ∪ T ∪{λ})


A  a | aB (lineal por la derecha)

Regular a Izquierda: P ⊆ N x (NT ∪ T ∪{λ})


A  a | Ba (lineal por la izquierda)
Donde: A , B ε N , a ε T*

Las gramáticas regulares guardan estrecha relación con los autómatas finitos.
Las gramáticas estudiadas en la teoría de lenguajes son la 2 y 3.
4.2. GRAMÁTICAS LIBRES DE CONTEXTO Y ANÁLISIS SINTÁCTICO

El análisis gramatical es determinar la sintaxis, o estructura, de un programa. Por


esta razón también se le conoce como análisis sintáctico. La sintaxis de un
lenguaje de programación por lo regular se determina mediante las reglas
gramaticales de una gramática libre de contexto. Una gramática libre de contexto
utiliza convenciones para nombrar y operaciones muy similares a las
correspondientes en las expresiones regulares. Con la única diferencia de que las
reglas de una gramática libre de contexto son recursivas.
Las estructuras de datos utilizadas para representar la estructura sintáctica de un
lenguaje ahora también deben ser recursivas en lugar de lineales. La estructura
básica empleada es por lo regular alguna clase de árbol, que se conoce como árbol
de análisis gramatical o árbol sintáctico. Existen dos categorías generales de
algoritmos: de análisis sintáctico descendente y de análisis sintáctico ascendente
(por la manera en que construyen el árbol de análisis gramatical o árbol
sintáctico).

4.3. GRAMÁTICAS LIBRES DE CONTEXTO


Como sabemos, en una gramática libre de contexto (GLC) G = (N, T, P, S) las
producciones tienen la siguiente forma:

Las GLCs tienen una gran importancia en la definición de lenguajes de


programación, interpretación del lenguaje natural, construcción de compiladores,
etc...
Por comodidad a la hora de definir lenguajes, la definición anterior se extiende a
la siguiente:

En este tema veremos que una GLC escrita de esta forma más general, siempre
puede escribirse en la versión más restrictiva (sólo S puede producir ).
4.4. CONCEPTOS SOBRE GLCs

4.4.1.ÁRBOL DE DERIVACIÓN
Sea G=(N,T,P,S) una GLC. Un árbol es un árbol derivación para G si:
1. Todo vértice tiene una etiqueta tomada de T ∪ N ∪ {λ}

2. La etiqueta de la raíz es el símbolo inicial S.


3. Los vértices interiores tienen etiquetas de N.
4. Si un nodo n tiene etiqueta A y n1, n2, n3, n4,...., nk, respectivamente
son hijos del vértice n ordenados de izquierda a derecha, con
etiquetas x1, x2, x3,.... xk, respectivamente, entonces:

Debe ser una producción en P


5. Si el vértice n tiene etiqueta λ, entonces n es ona hoja y es el unico
hijo de su padre.

Ejemplo:
Sea G = {N, T, P, S} una GLC cobn P: S ab | aSb.
La derivación de la cadena aaabbb sera S ⇒ aSb ⇒ aaSbb ⇒ aaabbb y el
arbol de derivación:
4.4.2.GRAMÁTICAS NO AMBIGUAS
Sea G = ( T , N , P , S ) que acepta expresiones aritméticas como:
X+Y–X*Y
T={X,Y,+,-,*,/,(,)}

N = { EXPR , TERM , FACTOR }

P = { EXPR  TERM | EXPR + TERM | EXPR – TERM


TERM  FACTOR | TERM * FACTOR | TERM / FACTOR
FACTOR  X | Y | ( EXPR )
S = {EXPR}
G no es ambigua, porque tiene un solo árbol de derivación

 Derivación por la izquierda


Se realiza el reemplazo de cada N que esta más a la izquierda

EXPR  EXPR – TERM


 EXPR + TERM – TERM
 TERM + TERM – TERM
 FACTOR + TERM – TERM
 X + TERM – TERM
 X + FACTOR – TERM
 X + Y – TERM
 X + Y – TERM * FACTOR
 X + Y – FACTOR * FACTOR
 X + Y – X * FACTOR
X +Y–X *Y

 Derivación por la derecha


Se realiza el reemplazo de cada N que esta más a la derecha

EXPR  EXPR – TERM


 EXPR - TERM * FACTOR
 EXPR - TERM * Y
 EXPR - FACTOR * Y
 EXPR - X * Y
 EXPR + TERM - X * Y
 EXPR + FACTOR - X * Y
 EXPR + Y - X * Y
 TERM + Y - X * Y
 FACTOR + Y - X * Y
X +Y–X *Y

Arbol de derivacion:

4.4.3.GRAMÁTICAS AMBIGUAS
Sea G = ( T , N , P , S ) que acepta expresiones aritméticas como:
X+Y–X*Y

T={X,Y,+,-,*,/,(,)}
N = { EXPR , OP }
P = { EXPR  EXPR OP EXPR | ( EXPR ) | X | Y
OP  + | - | * | /
S = {EXPR}
G es ambigua, porque tiene más de un árbol de derivación

 Derivación por la izquierda


Se realiza el reemplazo de cada N que esta más a la izquierda

EXPR  ( EXPR )
 ( EXPR OP EXPR )
 ( X OP EXPR )
 ( X + EXPR )
 ( X + ( EXPR ) )
 ( X + ( EXPR OP EXPR ) )
 ( X + ( Y OP EXPR ) )
 ( X + ( Y - EXPR ) )
 ( X + ( Y – ( EXPR OP EXPR ) ) )
 ( X + ( Y – ( X OP EXPR ) ) )
 ( X + ( Y – ( X * EXPR ) ) )
(X+(Y–(X*Y)))

 Derivación por la derecha


Se realiza el reemplazo de cada N que esta más a la derecha

EXPR  EXPR OP EXPR


 EXPR OP Y
 EXPR * Y
 ( EXPR ) * Y
 ( ( EXPR ) ) * Y
 ( ( EXPR OP EXPR ) ) * Y
 ( ( EXPR OP ( EXPR ) ) ) * Y
 ( ( EXPR OP ( EXPR OP EXPR ) ) ) * Y
 ( ( EXPR OP ( EXPR OP X ) ) ) * Y
 ( ( EXPR OP ( EXPR - X ) ) ) * Y
 ( ( EXPR OP ( Y - X ) ) ) * Y
 ( ( EXPR + ( Y - X ) ) ) * Y
((X+(Y - X)))*Y

4.5. GRAMÁTICA BNF

La Notación BNF o gramática libre de contexto, es una de las gramáticas más


usadas en los lenguajes de programación.
BNF ( Backus Naur Form ) fue desarrollada para la definición sintáctica de Algol
en 1960 por John Backus. Esta gramática maneja una sintaxis de una forma muy
sencilla ya que solo se interesa por la forma como están estructuradas las distintas
oraciones basándose en diversas reglas.
El lenguaje a analizar se toma como una serie de cadenas de caracteres de longitud
finita bajo un cierto orden. Cada cadena debe tener ciertas reglas para poder
analizar su estructura. Por ejemplo la notación de una cadena de un dígito definido
puede ser:

< dígito > ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

Aquí podemos ver que esta regla maneja las distintas alternativas que puede
contener un dígito. El símbolo ' | ' significa ' o ' , ::= significa un implica, <dígito>
significa la lo que estamos definiendo ( identificador ) y dígito es una categoría
sintáctica o un no terminal . Con esto podemos empezar a definir algunas
estructuras como el IF.

< enunciado condicional > ::=


if < expresión booleana > then < enunciado >
else
< enunciado > |
if < espresión booleana > then < enunciado >
Una característica de todo esto es que una categoría sintáctica se puede volver a
llamar así mismo para ir definiendo estructuras recursivas y/o más grandes. Por
ejemplo :

< entero sin signo > ::= < dígito > | < entero sin signo >< dígito >

Con esto ,vemos que la notación BNF maneja una serie de reglas gramaticales que
empiezan desde a lo más pequeño hasta lo más grande.
Ahora bien, existen otras gramáticas que se basan en esta gramática BNF.

4.6. ÁRBOLES DE ANÁLISIS SINTÁCTICOS

En esta gramática podemos usar identificadores que se llamen a sí mismos, pero


en otra notación : S -> SS | ( S ) | ( )
En esta expresión se usa -> en vez de ::= , pero sin embargo significan lo mismo.
El símbolo ( ) nos dice el final de este llamamiento recursivo, el cual puede formar
una serie de cadenas, donde S es cualquier expresión. Por decir :S forma a (S) y
este a (SS) y este a ( ( ) S ) y este a ( ( ) ( ) ) . Ya que existe él | y las posibles
terminaciones o símbolos terminales ( S ) y ( ).
Cada derivación se llama forma sentencial. Con ello aquí se puede decir que un
lenguaje es un conjunto de formas senténciales, dónde cada una tiene símbolos
terminales.
Cada cadena tiene la posibilidad de formar estructuras más grandes y con ello se
debe tener cuidado al formar las estructuras que puedan ser entendibles y no
provoquen confusión para los programadores. Cada cadena es analizada por las
reglas gramaticales, y el análisis provocara una salida buena o una mala, si es lo
ultimo, se produce un error en el análisis sintáctico.
A medida que se vayan produciendo análisis y creando estructuras más grandes y
recursivas , estas ultimas van formando árboles que pueden terminar en símbolos
terminales en sus ramas ( árbol de análisis sintáctico ) pero existiendo una relación
entre ellas, así que cada identificador es el mismo en cada rama ( según su
estructura propia de cada uno ) . Este árbol nos muestra la mayor parte de nuestro
programa.

4.7. EXTENSIÓN DE LA NOTACIÓN BNF

La sencillez de la notación BNF puede llegar a complicarse, ya que cuando se van


definiendo los identificadores, estos se pueden reducir a partes más pequeñas
provocando problemas para definir todos los identificadores. Para ello se usa una
notación un poco más fácil.

4.8. LA NOTACIÓN BNF EXTENDIDA

Esta usa nuevas reglas y símbolos:


1.- [ ... ] significa cualquier elemento optativo
2.- [ , ] significa la posibilidad de escoger de una serie de alternativas solo una
opción
3.- { ... }* significa poder escoger identificadores arbitrarios.
Ejemplos :
< entero con signo > ::= [ + | - ] < dígito > { dígito ] *
< identificador > ::= < letra > { < letra > | < dígito > } *

4.9. EL PROCESO DE ANÁLISIS SINTÁCTICO

La tarea del analizador sintáctico es determinar la estructura sintáctica de un


programa a partir de los tokens producidos por el analizador léxico y, ya sea de
manera explícita o implícita, construir el árbol de análisis gramatical o árbol
sintáctico que represente esta estructura.
Dos tipos principales de análisis:
 De arriba a abajo ("top-down").
 De abajo a arriba ("bottom-up").
Que sea una gramática de tipo 2 (lenguaje independiente del contexto) no nos
asegura que el autómata a pila sea determinista. Estos sólo representan un
subconjunto de los lenguajes i.c.. A lo largo de las páginas siguientes iremos
imponiendo diversas restricciones a las gramáticas. Las restricciones de un
método no tienen porqué ser las mismas que las de otro (complementariedad).

4.10.ANÁLISIS SINTÁCTICO ASCENDENTE

Analizador sintáctico ascendente: Intenta construir un árbol de análisis sintáctico,


empezando desde la raíz y descendiendo hacia las hojas. Lo que es lo mismo que
intentar obtener una derivación por la izquierda para una cadena de entrada,
comenzando desde la raíz y creando los nodos del árbol en orden previo.
Parte de la cadena de entrada para construir la inversa de una derivación por la
derecha. Genera el árbol de Análisis sintáctico partiendo de las hojas hasta
alcanzar el axioma.

 ¿Cuando puede reducirse por una parte izquierda lo que parece ser la parte
derecha de una regla?
 Puede haber partes derechas comunes
 Puede haber producciones ε

Tipos de gramáticas

Gramáticas LR
 Conjunto más amplio de gramáticas que LL(1)
 Expresión más sencillas
 LR(1) Analizadores LR(1)
 LALR Analizadores LALR
 SLR Analizadores SLR
 Condiciones SLR

Se comprueban al construir el analizador

Analizadores reducción-desplazamiento
 Analizadores por reducción – desplazamiento

 Los analizadores son tabulares


 Se diferencian por el algoritmo de construcción de la tabla

• Analizador LR
• Analizador LALR
• Analizador SLR
 El algoritmo de análisis es común

 Utiliza una tabla para decidir


• Un estado de pila por fila
• Una función Acción (s, a) = dj | rk
 dj. Desplazar al siguiente token y apilar el estado j
 rk. Reducir por regla k.
• Una función IrA(si, A) = sj
 Algoritmo de análisis

4.11.ANALIZADOR SINTÁCTICO SLR

 Términos comunes

o Elemento. Una regla de producción que incorpora en alguna posición de su


parte derecha un punto

o Prefijo viable. La parte situada a la izquierda del punto de algún elemento


 Funciones comunes
o Clausura (I)

o IrA (s, A)

 Colección Canónica del conjunto de elementos

o Colección de todos los conjuntos de elementos


o C = { I0, I1, …}

 Construcción

 Autómatas reconocedores de prefijos viables


o Los estados Sj del autómata son los conjuntos Ij de C
o Las transiciones son los símbolos N u T u {$}
 Construcción

 Construcción de tablas SLR

 Construcción de tablas SLR a partir del autómata

4.12.ANÁLISIS SINTÁCTICO DESCENDENTE

Analizador sintáctico descendente: Intenta construir un árbol de análisis


sintáctico, empezando desde las hojas (la cadena) y ascendiendo hacia la raíz. Lo
que es lo mismo que intentar obtener una reducción desde una cadena hasta llegar
al axioma. El analizador sintáctico tanto ascendente como descendente puede
representarse de dos formas: mediante tabla de análisis sintáctico o mediante
autómata de pilas.
 Partir del axioma de la gramática
 Escoger reglas gramaticales
 Hacer derivaciones por la izquierda
 Procesar la entrada de izquierda a derecha
 Obtener el árbol de análisis sintáctico o error

4.13.ANALIZADOR CON RETROCESO

 Usa retroceso para resolver la incertidumbre


 Sencillo de implementar
 Muy ineficiente
4.14.TÉCNICAS DE ANÁLISIS PREDICTIVO

 Propósito
 Crear un analizador descendente O(n)
 Debe decidir qué regla aplicar según token
 La gramática debe ser LL(1)
 L. Análisis de izquierda a derecha
 L. Derivaciones por la izquierda
 1. Un token permite decidir la regla de producción
 Se elimina la recursividad

4.15.CONJUNTOS DE PREDICCIÓN

 Conjuntos de predicción. Ayudan a decidir qué regla utilizar en cada paso

 Construcción
 Conjunto Primero PRIM (α)
 Conjunto Siguiente SIG (A)
 Regla

4.16.CONJUNTO PRIMERO

Si α es una forma sentencial compuesta por una concatenación de símbolos PRIM


(α) es el conjunto terminales (o ε) que pueden aparecer iniciando las cadenas que
pueden derivar de α

 Construcción
4.17.CONJUNTO SIGUIENTE

Si A es un símbolo no Terminal de la gramática SIG (A) es el conjunto de


terminales (y $) que pueden aparecer a continuación de A en alguna forma
sentencial derivada del axioma.

4.18.FACTORIZACIÓN POR LA IZQUIERDA

Se trata de rescribir las producciones de la gramática con igual comienzo para


retrasar la decisión hasta haber visto lo suficiente de la entrada como para elegir la
Opción correcta.

4.19.ELIMINACIÓN DE LA RECURSIVIDAD

 Tipos de recursividad
 Directa. Una gramática G es recursiva si tiene alguna regla de producción
que sea recursiva por la izquierda
Eliminación de la recursividad

 Indirecta. Si, a partir de una forma sentencial que empieza por un no


terminal se puede derivar una nueva forma no sentencial donde reaparece
al principio el no terminal

Eliminación de la recursividad
 http://html.rincondelvago.com/lenguajes-de-programacion_historia-y-
evolucion.html
 http://www.desarrolloweb.com/articulos/2358.php
 http://www.monografias.com/trabajos16/lenguaje-miranda/lenguaje-miranda.shtml
 http://enciclopedia.us.es/index.php/Lenguaje_imperativo
 http://www.monografias.com/trabajos/tendprog/tendprog.shtml
 http://www.monografias.com/trabajos/lengprog/lengprog.shtml
 http://www.zonatenisatp.com/index.php
 http://www.zonatenisatp.com/tema1_1_01_abstraccion.php
 http://www.zonatenisatp.com/tema1_2_00_abstraccion_datos.php
 http://www.zonatenisatp.com/tema1_2_03_lenguajes_programacion.php
 http://ultimaorbita.com/wiki//index.php?title=Abstraccion_de_datos_y_abstraccion_
de_control._Evolucion_segun_los_paradigmas
 -http://yalma.fime.uanl.mx/~elisa/teaching/prog/herencia.pdf
 http://ib.cnea.gov.ar/CursoOO/tipos.htm
 http://es.wikipedia.org/wiki/Encapsulamiento_(programaci%C3%B3n_orientada_a_
objetos)
 http://www.cs.uu.nl/~jeroen/courses/fp-sp.pdf
 http://www.inf.unitru.edu.pe/~pelm/Modelos/Funcion.html
 http://www.dsic.upv.es/users/elp/temas/ProgFunc.html
 http://juanfc.lcc.uma.es/EDU/EP/trabajos/T201.Clasificaciondelostiposdelenguajes.
pdf
 http://www.esimez.ipn.mx/acadcompu/apuntes_notas%20breves/programacion_orie
ntada_objetos.pdf
 http://www.monografias.com/trabajos20/paradigmas-de-programacion/paradigmas-
de-programacion.shtml
 http://static.scribd.com/docs/7a6vhoquhqs22.pdf
 http://www.scribd.com/doc/9762/Programacion-Orientada-a-Objetos
 http://platea.cnice.mecd.es/~jmarti2/materiales/resumenLePr.pdf
 http://horustealth.tripod.com/pascal.htm
 http://decsai.ugr.es/~dpelta/ProgOrdenadores/tema5.pdf
 http://ar.geocities.com/luis_pirir/cursos/procedimiento.htm
 http://www.inf.udec.cl/~mvaras/estprog/cap41.html
 http://www.monografias.com/trabajos/objetos/objetos.shtml
 http://www.gnacademy.org/text/cc/Tutorial/Spanish/node5.html
 http://www.gnacademy.org/text/cc/Tutorial/Spanish/node6.html
 -http://www.desarrolloweb.com/articulos/2358.php
 http://www.desarrolloweb.com/articulos/2387.php
 http://es.wikipedia.org/wiki/Compilador
 http://www.investigacion.frc.utn.edu.ar/labsis/Publicaciones/InvesDes/Compiladore
s/rxc.htm
 http://arantxa.ii.uam.es/~alfonsec/docs/compila1.htm
 http://www.monografias.com/trabajos11/compil/compil.shtml
 http://mx.geocities.com/alfonsoaraujocardenas/compiladores.html
 http://platon.escet.urjc.es/grupo/docencia/automatas/
 http://kataix.umag.cl/~jaguila/Iec/Compiladores/Automatas/ta_cap1_2.html
 http://arantxa.ii.uam.es/~alfonsec/docs/compila4.htm

You might also like