Actividad 2 Analizador léxico, sintáctico y semántico
1. Actividad 2 Programación
Tema: Conocer la importancia de un
analizador léxico, sintáctico y semántico a
partir de un generador de código.
2. Análisis Léxico. El analizador léxico es la primera fase de un
compilador, lee caracteres de entrada para formar componentes e
identificarlos o clasificarlos y pasar la información de los componentes al
analizador sintáctico.
Realiza además funciones como eliminar espacios en blanco, saltos de
línea, tabuladores, ignorar comentarios, detección y recuperación de
errores. Los errores que un analizador léxico reconoce son símbolos no
válidos o no reconocidos por el léxico del lenguaje o que no forman parte
de ningún componente léxico.
3. También entre sus funciones están .
Manejar el fichero fuente
Leer los caracteres de la entrada
Generar una secuencia de componentes léxicos
(TOKENS)
Eliminar comentarios, delimitadores (espacios,
símbolos de puntación, fin de línea)
Relacionar los mensajes de error con las líneas del
programa fuente
Introducir los identificadores en la tabla de símbolos
Manejar macros
Controlar si es de formato libre o no
Libre: PASCAL, ALGOL
No libre: FORTRAN, BASIC
4. Análisis Sintáctico.
Gramática
_ Permite definir un lenguaje mediante reglas que nos permiten generar
o producir cadenas de un lenguaje.
_ Estas gramáticas son similares a las gramáticas de los lenguajes
naturales, pero mucho más restrictivas y sencillas.
_ Un ejemplo de regla de una gramática:
Oración _ Sujeto predicado
_ Estas reglas se suelen llamar reglas de reescritura: el símbolo Oración
se puede reescribir por el símbolo Sujeto seguido del símbolo Predicado.
Autómata:
Al igual que con los lenguajes regulares podemos definir un autómata
como una máquina reconocedora de cadenas (palabras) de un
determinado lenguaje.
Los autómatas con los que trabajaremos en este tema son algo más
complejos que los AF
5. Análisis Semántico.
La fase de análisis semántico de un procesador de lenguaje es
aquélla que computa la información adicional necesaria para el
procesamiento de un lenguaje, una vez que la estructura
sintáctica de un programa haya sido obtenida. Es por tanto la
fase posterior a la de análisis sintáctico y la última dentro del
proceso de síntesis de un lenguaje de programación.
Sintaxis de un lenguaje de programación es el conjunto de reglas
formales que especifican la estructura de los programas
pertenecientes a dicho lenguaje. Semántica de un lenguaje de
programación es el conjunto de reglas que especifican el
significado de cualquier sentencia sintácticamente válida.
Finalmente, el análisis semántico1 de un procesador de lenguaje
es la fase encargada de detectar la validez semántica de las
sentencias aceptadas por el analizador sintáctico.
6. 2. Que es el análisis léxico en cuanto
a:
a) manejo de localidades temporales de memoria (buffers)
La forma más fácil de leer un programa es carácter por carácter pero es ineficiente.
La forma más eficiente es realizar una copia a la memoria de todo el código fuente. Pero esto en la
gran mayoría de las ocasiones es impráctico por las dimensiones de los programas. Para solucionar
este problema se sugiere utilizar buffers.
Manejo de buffers:
Existen muchas formas de dividir el trabajo, pero siempre se deberá llevar dos punteros, uno al
carácter actual y otro al inicial del lexema.
El manejo de buffers es esencial para realizar el análisis de grandes programas de mejor manera
La diferente de velocidad entre los dos tipos de memoria es muy grande, por lo que resulta
interesante definir algún tipo de estrategia que reduzca este diferencial.
Un buffer se define como un conjunto de bytes que son leídos o escritos desde un dispositivo de
almacenamiento, en la memoria primaria. Cuando se desea leer una información, se lee un bloque
de información en el que aparece. La modificación de un dato se realiza sobre el buffer, que
posteriormente debe ser enviado al dispositivo de almacenamiento. La utilización de esta técnica
permite reducir el número de accesos a memoria secundaria.
7. Número de Buffers y Velocidad de Acceso:
El manejo de buffers por parte del administrador de ficheros
permite reducir el número de accesos a memoria secundaria.
Pero una cuestión fundamental es el número de buffers a utilizar.
Si sólo se utiliza un buffer, un problema que realice lecturas y
escrituras de modo alterno, debería leer un bloque en cada
operación. - Esto se resuelve mediante la utilización de un buffer
para escritura y otro para lectura. Pero la lectura, o escritura,
alterna sobre varios ficheros puede provocar el mismo problema.
Otra alternativa es la utilización de ambos bloques para lecturas y
escrituras de modo alternado.
La generalización de esta idea es el caso real, varios buffers que
se manejan de modo indistinto para lecturas y escrituras. La
gestión de estos buffers es realizada por el administrador de
ficheros, aunque el usuario puede controlar el número de buffers.
Si todos los buffers están ocupados, se debe vaciar uno de ellos
para posibilitar una lectura. Normalmente se utiliza al algoritmo
LRU, es decir, se vacía el buffer menos recientemente utilizado.
8. b) creación de tablas de símbolos
Tabla: conjunto de pares clave-valor, llamados elementos de la
tabla.
La tabla de símbolos es una componente necesaria de un
compilador. Al declarar un identificador (normalmente una sola
vez), éste es insertado en la tabla. Cada vez que se utilice el
identificador se realizará una búsqueda en la tabla para obtener
la información asociada (el valor). Verifican que la semántica sea
correcta y ayuda en la generación apropiada del código.
Búsqueda: dada la clave de un elemento, encontrar su valor.
Inserción: Dado un par clave-valor, añadir un elemento nuevo a la
tabla.
Cambio de valor: Buscar el elemento y cambiar su valor.
Borrado: Eliminar un elemento de la tabla.
9. Longitud de búsqueda (o tiempo de acceso):
De una clave: Li = número de comparaciones con elementos de la tabla para encontrar esa
clave. Máxima: LM = número máximo de comparaciones para encontrar cualquier clave.
Media (esperada): Lm = número medio de comparaciones para encontrar un valor. Si la
frecuencia de todas las claves es la misma:
Lm = (S Li)/N
Si la frecuencia de todas las claves no es la misma:
Lm = S pi.Li
Grado de ocupación:
s = n/N
donde n=número de elementos en la tabla y N=capacidad máxima de la tabla. Función de
búsqueda: B : K E asocia a cada clave k un elemento B(k).→
Valor asociado a una clave k: v(B(k)). Puede ser múltiple, en cuyo caso normalmente se
convierte en un puntero. Si está en la tabla puede almacenarse consecutivamente o en
subtablas paralelas.
Tablas de símbolos (identificadores) La clave es el identificador. El valor está formado por:
Atributos del identificador. Puntero a la posición de memoria asignada. La clave puede
sustituirse por un puntero.
Los identificadores pueden estar empaquetados.
La longitud del identificador puede especificarse en la tabla o delante del nombre, o ser
implícita.
Tablas consecutivas: Todos los elementos ocupan posiciones de memoria adyacentes.
Tablas ligadas: cada elemento apunta al siguiente. Tablas doblemente ligadas: cada
elemento apunta al siguiente y al anterior. Tablas no ordenadas Inserción: en el primer lugar
vacío.
10. c) Manejo de errores
léxicos El compilador tiene que reportar clara y exactamente la presencia de errores recuperarse de
cada error lo suficientemente rápido para poder detectar errores subsecuentes, debe tratar
de evitar mensajes falsos de error, Un error produce un token erróneo.
Un token o componente léxico es una cadena de caracteres que tiene un significado
coherente en cierto lenguaje de programación.
Recuperación en modo pánico: este tipo de estrategia es la más común. Consiste en que
cuando se detecta una cadena no reconocible, se siguen leyendo caracteres hasta que se
vuelve a detectar un token válido. Borrar un carácter extraño. Insertar un carácter que falta
(e.g. reemplazar 2C por 2*C). Reemplazar un carácter incorrecto por otro correcto (e.g.
reemplazar INTEJER por INTEGER si el lugar en donde aparece el primer lexema no es el
indicado para un identificador) Intercambiar dos caracteres, ó tokens, adyacentes (e.g. I
INTEGER por INTEGER I).
La recuperación de errores durante el AL puede producir otros en las siguientes fases. var
numero : integer; begin num?ero:=10; end el compilador podría producir los siguientes
mensajes de error: ERROR LÉXICO: carácter no reconocido (?) ERROR SEMÁNTICO:
identificador no declarado (num) ERROR SINTÁCTICO: falta operador entre identificadores
ERROR SEMÁNTICO: identificador no declarado (ero)
Otras veces no: var i,j: integer; begin i:=1; ? j:=2; end
11. 3. Que es el análisis sintáctico en
cuanto a:
a) Diagramas de sintaxis.
Es una forma que los árboles de derivación, su principal característica es
que permite ver ls derivaciones al instante de que ocurren
Ejemplo 1:Dado el siguiente ejemplo de código en C:
superficie = base * altura / 2;
La sintaxis del lenguaje C indica que las expresiones se pueden formar
con un conjunto de operadores y un conjunto de elementos básicos.
Entre los operadores, con sintaxis binaria infija, se encuentran la
asignación, el producto y la división. Entre los elementos básicos de una
expresión existen los identificadores y las constantes enteras sin signo
(entre otros).
Su semántica identifica que en el registro asociado al identificador
superficie se le va a asociar el valor resultante del producto de los
valores asociados a base y altura, divididos por dos (la superficie de un
triángulo).
12. b) Precedencia de operadores.
La precedencia de operadores es de vital importancia en el
proceso de análisis sintáctico ya que nos representará la
forma en que debe construirse el árbol de derivación.
En aritmética existen prioridades, por ejemplo: * y / tienen
preferencia sobre + y -. () indican la máxima prioridad.
Prioridad de operadores
• La instrucción a = b + c / 2 en la mayoría de los lenguajes
no se evalúa de la forma a = (b + c) /2, sino de la forma a = b
+ (c/2)
• La forma de evaluación depende de cómo se construyan
los operadores, ya sea en infijo, postfijo o prefijo.
• Las operaciones se realizan de abajo hacia arriba.
13. Analizador sintáctico: analizador
descendente (LL), analizador
ascendente (LR, LALR).
Un analizador sintáctico ( Parser ) es un programa que reconoce
si una o varias cadenas de caracteres forman parte de un
determinado lenguaje. Los lenguajes habitualmente reconocidos
por los analizadores sintácticos son los lenguajes libres de
contexto. Cabe notar que existe una justificación formal que
establece que los lenguajes libres de contexto son aquellos
reconocibles por un autómata de pila, de modo que todo
analizador sintáctico que reconozca un lenguaje libre de contexto
es equivalente en capacidad computacional a un autómata de
pila. Los analizadores sintácticos fueron extensivamente
estudiados durante los años 70 del siglo XX, detectándose
numerosos patrones de funcionamiento en ellos, cosa que
permitió la creación de programas generadores de analizadores
sintáticos a partir de una especificación de la sintaxis del
lenguaje, tales y como yacc, GNU bison y javacc.
14. Analizador Descendente Ll
Análisis Sintáctico Predictivo Recursivo
La siguiente fase en la construcción del analizador es la fase
de análisis sintáctico. Esta toma como entrada el flujo de
terminales y construye como salida el árbol de análisis
sintáctico abstracto.
El árbol de análisis sintáctico abstracto es una
representación compactada del árbol de análisis sintáctico
concreto que contiene la misma información que éste.
Existen diferentes métodos de análisis sintáctico. La
mayoría caen en una de dos categorías: ascendentes y
descendentes. Los ascendentes construyen el árbol desde
las hojas hacia la raíz. Los descendentes lo hacen en modo
inverso. El que usaremos aqui es uno de los más sencillos:
se denomina método de análisis predictivo descendente
recursivo.
15. Analizador Ascendente Lr Lalr
Analizador Ascendente LR
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.
LL (left to left) leen la cadena de izquierda a
derecha y derivan por la izquierda
16. Administración de tablas de
símbolos.
Ésta poseerá una entrada por cada identificador declarado en el
contexto que se esté analizando. Con este tipo de estructuras de datos
adicionales, los desarrolladores de compiladores acostumbran a suplir
las carencias de las gramáticas libres de contexto.
La tabla de símbolos registra información acerca de cada nombre de
símbolo en un programa. Históricamente, los nombres se llamaron
símbolos, más que de una tabla de nombres. En este capítulo, la palabra
símbolo significa nombre. La fase de análisis semántico crea la tabla de
símbolos, puesto que no es sino hasta este análisis que se tiene la
suficiente información sobre un nombre para describirlo. La generación
de código usa la tabla de símbolos para extraer las directivas del
ensamblador, para el tipo y para el tamaño apropiados.”
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.
17. Análisis sintáctico. Como se muestra en la Figura 1, la entrada del analizador
semántico es la salida generada por el analizador sintáctico. La estructura empleada
para intercambiar la información entre estas dos fases es lo que se conoce como árbol
sintáctico –o una simplificación del mismo, denominada árbol sintáctico abstracto (§
2.2). Una vez validada la sintaxis de un programa, el análisis semántico aplicará reglas
semánticas para validar dicho árbol.
e) Manejador de errores. Si la validación del árbol sintáctico descrita en el párrafo
anterior no fuese satisfactoria, es decir, existiese un error semántico, la fase de análisis
semántico debería notificar dicho error al manejador de errores para que éste se
encargase de su gestión. El proceso de análisis podría seguir ejecutándose o no, en
función de si el procesador de lenguaje implementa algún mecanismo de recuperación
de errores.
− Generación de código (intermedio). La salida del análisis semántico se suele
emplear como entrada para la generación de código12. La estructura de datos
empleada para intercambiar información entre las dos fases mencionadas es un árbol
sintáctico decorado (§ 2.2). Este árbol posee información adicional al árbol generado
por el analizador sintáctico, como por ejemplo la información relativa al tipo de cada
una de las expresiones del programa. El empleo de dicha información es útil para llevar
a cabo el proceso de generación de código (a bajo nivel, el tipo de una expresión es
necesario, por ejemplo, para saber el número de bytes que ocupa su valor).
− Tabla de símbolos. Como hemos mencionado previamente, la utilización de
gramáticas libres de contexto (de tipo 2) no permite expresar características
representables con gramáticas sensibles al contexto –como la necesidad de que la
utilización de una variable en el lenguaje Pascal requiera la declaración previa de la
variable utilizada. Para poder implementar un procesador del lenguaje Pascal
empleando gramáticas de tipo 2 e implementaciones de autómatas de pila, es
necesario emplear una estructura de datos auxiliar denominada tabla de símbolos. Esta
estructura de datos, a su nivel más básico, es un diccionario (memoria asociativa) que
asocia identificadores a la información requerida por el compilador. Sus dos
operaciones básicas son insertar y buscar. En nuestro ejemplo, la declaración de un
identificador en Pascal requerirá una inserción del mismo en la tabla de símbolos; cada
vez que se utilice un identificador en una sentencia, el analizador semántico buscará
éste en la tabla de símbolos (llamando al manejador de errores si no existiere).
18. 4. Que es el análisis semántico
en cuanto a:
Se compone de un conjunto de rutinas independientes, llamadas por los
analizadores morfológico y sintáctico.
El análisis semántico utiliza como entrada el árbol sintáctico detectado
por el análisis sintáctico para comprobar restricciones de tipo y otras
limitaciones semánticas y preparar la generación de código.
En compiladores de un solo paso, las llamadas a las rutinas semánticas
se realizan directamente desde el analizador sintáctico y son dichas
rutinas las que llaman al generador de código. El instrumento más
utilizado para conseguirlo es la gramática de atributos.
En compiladores de dos o más pasos, el análisis semántico se realiza
independientemente de la generación de código, pasándose
información a través de un archivo intermedio, que normalmente
contiene información sobre el árbol sintáctico en forma linealizada (para
facilitar su manejo y hacer posible su almacenamiento en memoria
auxiliar).
19. En cualquier caso, las rutinas semánticas suelen hacer uso de una pila (la pila
semántica) que contiene la información semántica asociada a los operandos
(y a veces a los operadores) en forma de registros semánticos.
a) Verificación de tipos en expresiones.
b) Conversión de tipos.
Acciones agregadas en un analizador sintáctico descendente (top-down).
Un analizador sintáctico (Parser) es un programa que reconoce si una o
varias cadenas de caracteres forman parte de un determinado lenguaje. Los
lenguajes habitualmente reconocidos por los analizadores sintácticos s
on los lenguajes libres de contexto.
Los analizadores pueden clasificarse dependiendo de la forma en como se
construyen los nodos del árbol de derivación sintáctico: ascendentes y
descendentes.
LL (left to left) leen la cadena de izquierda a derecha y derivan por la
izquierda
• LR (left to right)
• SàaA
• AàaBbC
• Bàb
• Càc
20. Pila semántica en un analizador
sintáctico ascendente (bottom-up).
Los ascendentes construyen el árbol desde las hojas hacia la raíz.
Los descendentes lo hacen en modo inverso.
• Un analizador ampliamente utilizado se denomina método de análisis
predictivo descendente recursivo que es muy sencillo.
Derivación izquierda:
• SàAaàaaBbCàaabbCàaabbc (1234)
• SàaAàaaBbCàaaBbcàaabbc (3421)
• LL(k) traductores “top-down”
• Un análisis anticipado de k caracteres
.SàaS|cA
• AàbA|cB|vacia
• BàcB|a| vacia
Construir cadena acbb
• SàaS o SàcA; al anticipar el primer símbolo
21. Administración de la tabla de
símbolos.
La tabla de símbolos se crea durante la fase de análisis léxico a
través de los componentes léxicos, pero en el proceso de análisis
sintáctico sufren algunas modificaciones.
• Generalmente se agregan valores de tipo y significado para el
análisis sintáctico
Es una estructura de datos que usa el proceso de traducción de
un lenguaje de programación, por un compilador o un intérprete,
donde cada símbolo en el código fuente de un programa está
asociado con información tal como la ubicación, el tipo de datos,
y el ámbito de cada variable, constante o procedimiento.
Los símbolos en la tabla de símbolos pueden referirse a
constantes, a funciones o a tipos de datos en el código fuente de
un programa.
El administrador de la tabla de símbolos se encarga de manejar
los accesos a l tabla de símbolos, en cada una de las etapas de
compilación de un programa.
22. Manejo de errores
semánticos Los errores semánticos son más sútiles. Un error semántico se produce cuando la
sintaxis del código es correcta pero la semántica o significado no es le que se
pretendía. La construcción obedece las reglas del lenguaje y por ellos el compilado o
interprete no detectan los errores semánticos, los copiladores o interpretes sólo se
ocupan de la estructura del código que se escribe, y no de su significado, Un error
semántic puede hacer que el programa termine de forma anormal, cn o sin un
mensaje de error.
Si los traductores tuvieran que procesar programas correctas el proceso de
implantación se simplificaría mucho.
• ¿Cómo debe de responder un compilador de pascal a un código Fortran?
• Ningún método de recuperación de errores resuelve todos los problemas
Tipos de errores
• Léxicos: como escribir mal un identificador, palabra clave u operador.
• Sintácticos: como una expresión aritmética con paréntesis no equilibrados.
• Semánticos: como un operador aplicado a un operadorando incompatible.
• Lógicos: como una llamada infinitamente recursiva
• La mayoría de los errores se centra en la fase de análisis sintáctico.
23. El manejador de errores debe:
• Informar la presencia de errores con claridad y exactitud., recuperar de cada error con la suficiente rapidez como para
detectar errores posibles., No debe retrasar de manera significativa el procesamiento de programas correctos., Debe
indicar la línea del error y algún mensaje informativo, estrategias de recuperación de errores, modo Pánico, nivel de
Frase, Producciones de error, Corrección global, recuperación en modo pánico, es el más sencillo de implantar.
• El analizador sintáctico desecha componentes léxicos hasta encontrar un carácter de sincronización. Estos caracteres
son el punto y como (;) entre otros.
Recuperación en modo pánico
int a.b,c;
struct c {
….
}
main()
{
int a;
}
Recuperación a nivel de frase
• Esta técnica utiliza una corrección de caracteres adyacentes, ya sea por inserción, eliminación o intercambio.
• Esta técnica permite sustituir , por ;, etc. Son traductores que corrigen errores. Desafortunadamente para muchos casos
no aplican por lo que no se utilizan demasiados.
Producciones de error
• Se pueden generar gramáticas para generar producciones de error y así de esta forma seguir con el proceso.
• La dificultad radica en el sentido de encontrar esas reglas gramaticales para generar error. En algunos casos sería
inclusiva más extensa que la gramática del propio lenguaje.
• Corrección global
• Idealmente, sería recomendable que un traductor hiciera el mínimo de cambios para procesar una entrada inválida. Este
algoritmo genera menores costos globales para realizar cambios.
• El problema radica en que el implementar estas estrategias son muy costosas en tiempo y espacio.