miércoles, 14 de abril de 2010

UNIDAD V.- ANALISIS SEMANTICO


5.1 ANALIZADOR SEMANTICO.

La semántica corresponde al significado asociado a las estructuras formales (sintaxis) del lenguaje.

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.

El objetivo principal del analizador semántico de un procesador de lenguaje es asegurarse de que el programa analizado satisfaga las reglas requeridas por la especificación del lenguaje, para garantizar su correcta ejecución. El tipo y dimensión de análisis semántico requerido varía enormemente de un lenguaje a otro.

Existen dos formas de describir la semántica de un lenguaje de programación: mediante especificación informal o natural y formal.

La descripción informal de un lenguaje de programación es llevada a cabo mediante el lenguaje natural. Esto hace que la especificación sea inteligible (en principio) para cualquier persona. La experiencia nos dice que es una tarea muy compleja, si no imposible, el describir todas las características de un lenguaje de programación de un modo preciso.

La descripción formal de la semántica de lenguajes de programación es la descripción rigurosa del significado o comportamiento de programas, lenguajes de programación, máquinas abstractas o incluso cualquier dispositivo hardware.

Revelar posibles ambigüedades existentes implementaciones de procesadores de lenguajes o en documentos descriptivos de lenguajes de programación.

Ser utilizados como base para la implementación de procesadores de lenguaje.

Verificar propiedades de programas en relación con pruebas de corrección o información relacionada con su ejecución.

Diseñar nuevos lenguajes de programación, permitiendo registrar decisiones sobre construcciones particulares del lenguaje, así como permitir descubrir posibles irregularidades u omisiones.

Facilitar la comprensión de los lenguajes por parte del programador y como mecanismo de comunicación entre diseñador del lenguaje, implementador y programador. La especificación semántica de un lenguaje, como documento de referencia, aclara el comportamiento del lenguaje y sus diversas construcciones.

Estandarizar lenguajes mediante la publicación de su semántica de un modo no ambiguo. Los programas deben poder procesarse en otra implementación de procesador del mismo lenguaje exhibiendo el mismo comportamiento.

5.2 VERIFICACIÓN DE TIPOS EN EXPRESIONES

Sistema de Tipos

Reglas de un lenguaje que permiten asignar tipos a las distintas partes de un programa y verificar su corrección.

  • Formado por las definiciones y reglas que permiten comprobar el dominio de un identificador, y en qué contextos puede ser usado.
  • Cada lenguaje tiene un sistema de tipos propio, aunque puede variar de una a otra implementación.
  • La comprobación de tipos es parte del análisis semántico.

Funciones Principales:

Reglas de un lenguaje que permiten asignar tipos a las distintas partes de un programa y verificar su corrección.

  • Inferencia de tipos: calcular y mantener la información sobre los tipos de datos.
  • Verificación de tipo: asegurar que las partes de un programa tienen sentido según las reglas de tipo del lenguaje.

La información de tipos puede ser estática o dinámica:

  • LISP, CAML o Smalltalk utilizan información de tipos dinámica.
  • En ADA, Pascal o C la información de tipos es estática.
  • También puede ser una combinación de ambas formas.

Cuantas más comprobaciones puedan realizarse en la fase de compilación, menos tendrán que realizarse durante la ejecución.

  • Mayor eficiencia del programa objeto.

Es parte de la comprobación de tipos:

  • Conversión de tipos explícita: transformación del tipo de una expresión con un propósito determinado.
  • Coerción: conversión de tipos que realiza de forma implícita el compilador.

Conversión de tipos explícita: el programador indica el tipo destino:

  • Funciona como una llamada a función: recibe un tipo y devuelve otro.

Conversión de tipos implícita: el compilador convierte automáticamente elementos de un tipo en elementos de otro:

  • La conversión se lleva a cabo en la acción semántica de la regla donde se realiza.

Comprobador de tipos seguro: Durante la compilación (comprobación estática) detecta todos los posibles errores de tipo.

Lenguaje fuertemente tipado: Si un fragmento de código compila es que no se van a producir errores de tipo.

En la práctica, ningún lenguaje es tan fuertemente tipado que permita una completa comprobación estática.

Información de tipos dinámica: El compilador debe generar código que realice la inferencia y verificación de tipos durante la ejecución del programa que se está compilando.

Unidad2_5_Fig1

Información de tipos estática:

  • Se utiliza para verificar la exactitud del programa antes de la ejecución.
  • Permite determinar la asignación de memoria necesaria para cada variable.

Unidad2_5_Fig2

Tipo de datos = conjunto de valores + operaciones aplicables

En el ámbito de los compiladores, un tipo se define mediante una expresión de tipo (información de tipos explícita):

  • Nombre de tipo: float.
  • Expresión estructurada explícita: set of integer.
  • Estas expresiones se utilizan en la construcción de otros tipos o para declarar variables.

También es posible incluir información de tipos implícita:

Unidad2_5_Fig3

La información de tipos, implícita o explícita, se mantiene en la tabla de símbolos:

  • Esta información se recupera de la tabla de símbolos mediante el verificador de tipo cuando se hace referencia al nombre asociado.

Ejemplo:

Unidad2_5_Fig4

Un lenguaje de programación contiene un conjunto de tipos predefinido denominados tipos simples:

  • Algunos lenguajes permiten definir nuevos tipos simples: enumerado, subrango.

Todos los lenguajes permiten crear nuevos tipos complejos a partir de otros más simples mediante constructores de tipos:

  • Matrices, productos, registros, punteros, funciones, …
  • En Pascal: array, set, record, ...
  • En C++: struct, class, union, ....

Para analizar los diferentes tipos que intervienen dentro de un programa, el compilador debe contar con una estructura interna que le permita manejar cómodamente las expresiones de tipos.

Esta estructura interna:

  • Debe ser fácilmente manipulable, pues su creación se realizará conforme se hace la lectura del programa fuente.
  • Debe permitir comparar fácilmente las expresiones asignadas a distintos trozos de código, especialmente a los identificadores de variables..

La forma más habitual de representación son los grafos acíclicos dirigidos (GADs).

  • La ventaja de estas representaciones es que ocupan poca memoria y por tanto la comprobación de equivalencia se efectúa con rapidez.

Ejemplos:

Unidad2_5_Fig5

Unidad2_5_Fig6

Unidad2_5_Fig7

5.3 GRAMATICA DE ATRIBUTOS

Una Gramática con Atributos es una generalización de las Gramáticas Libres de Contexto, denominada

Definición Dirigida por la Sintaxis:

Cada símbolo gramatical puede tener asociado un conjunto finito de atributos, que pueden ser de los siguientes tipos:

Sintetizados: su valor se calcula en función de los atributos de los nodos hijos.

Heredados: su valor se calcula en función de los atributos de los nodos hermanos y/o del nodo padre.

Cada atributo tomará valores en un dominio.

Cada producción llevará asociadas un conjunto de reglas semánticas.

Las relaciones de dependencia entre atributos, establecidas por las reglas semánticas, se representarán mediante el Grafo de Dependencias.

A partir de estas gramáticas se llevan a cabo las denominadas “Traducciones dirigidas por sintaxis”.

2. Atributos Sintetizados

En el caso de los símbolos terminales de la gramática, su atributo no es mas que el lexema asociado al token reconocido por el analizador léxico.

Una gramática con atributos se denomina Gramática

S-Atribuida si todos los atributos son sintetizados. Siempre es posible transformar una Gramática con Atributos en

Gramática S-Atribuida.

Ejemplo: Analizar la forma sentencial 12+3*6, a partir de la siguiente definición dirigida por la sintaxis:

Producción Regla semántica

E1 �� E2 + T E1.val = E2.val + T.val

E �� T E.val = T.val

T1 �� T2 * F T1.val = T2.val * F.val

T �� F T.val = F.val

F �� (E) F.val = E.val

F �� num F.val = valor (num)

3. Atributos Heredados

Una gramática con atributos se denomina Gramática

L-Atribuida si cada atributo que se evalúa cumple una de las siguientes condiciones:

Es un atributo sintetizado.

Dada una producción A �� X1X2..Xj..Xn, el atributo heredado asociado a Xj depende únicamente de los atributos de X1,..., Xj-1 y/o de atributos heredados asociados al símbolo A.

4. Grafo de dependencias

Para calcular el valor de un atributo es necesario calcular en primer lugar los valores de los atributos de los que depende, para lo cual se deberá establecer una dependencia entre atributos mediante un Grafo de Dependencias.

Ejemplo: Analizar la forma sentencial real id1, id2, id3 a partir de la siguiente definición dirigida por la sintaxis:

Producción Reglas semánticas

D �� T L L.her = T.tipo

T �� int T.tipo = integer

T �� real T.tipo = real

L �� L1, id L1.her = L.her

AñadeTipoTS(id.entrada, L.her)

L �� id AñadeTipoTS(id.entrada, L.her)

Ejemplo: Dada la gramática G = ({L, A}, {a}, {L��AL | A,

A��a}, L), obtener una gramática con atributos que al analizar una cadena calcule el número de a’s que la componen.

Ejemplo: Dada la gramática G = ({L, E, R}, {“,”, id, [, ]}, {L��id | id[E], E �� R| E,R , R ��id}, L), definir un atributo denominado num_dimensiones y las reglas semánticas asociadas a cada producción, de forma que al analizar una sentencia obtengamos el número de dimensiones que tiene el array referenciado.

Ejemplo: Dada la gramática G = ({S, L, B}, {0, 1, …}, {S��L.L |

L, L��LB | B, B��0 | 1}, S), obtener una gramática con atributos que al analizar un número en binario calcule su equivalente en decimal.

5. Evaluación de atributos con analizadores sintácticos descendentes LL(1)

Las Gramáticas L-Atribuidas engloban a la mayoría de las gramáticas con atributos basadas en gramáticas LL(1).

Se define Esquema de Traducción como una gramática con atributos cuyas acciones semánticas se expresan entre llaves, y se encuentran intercaladas entre los símbolos de la parte derecha de las producciones asociadas a la gramática, o bien al final de las mismas.

Para realizar el Análisis Sintáctico Descendente de atributos con una gramática LL(1) será necesario transformar dicha gramática a un Esquema de Traducción en el que se insertarán las acciones semánticas necesarias, teniendo en cuenta que un valor de un atributo debe estar calculado antes de poder ser utilizado.

El Analizador es similar, pero ahora trabajará con producciones compuestas de los símbolos más las acciones semánticas:

a) Al aplicar una producción introduciremos en la pila los símbolos y las acciones semánticas.

b) Cuando en el tope de la pila se encuentre una acción semántica, pasaremos a ejecutarla, eliminándola a continuación del tope.

Ejemplo: Dada la gramática G = {{E, T}, {+, -, (, ), num},

{E��E+T | E-T | T, T��(E) | num}, E}:

a) Obtener un Esquema de Traducción que permita conocer el resultado de una operación válida para dicha gramática.

b) Aplicar un Análisis Sintáctico Descendente de atributos sobre el Esquema de Traducción obtenido para analizar la evaluación de la expresión 6-3+4.

Ejemplo: Dada la gramática G = {{E, T, R}, {+, -, (, ), num},

{E��TR, R��+TR | -TR | ξ, T��(E) | num}, E}:

a) Obtener un Esquema de Traducción que permita conocer el resultado de una operación válida para dicha gramática.

b) Aplicar un Análisis Sintáctico Descendente de atributos sobre el Esquema de Traducción obtenido para analizar la evaluación de la expresión 6-3+4.

6. Evaluación de atributos con analizadores sintácticos ascendentes LR(1)

En los analizadores LR no se conoce la producción aplicada hasta que no se ha analizado la parte derecha, por lo que las acciones semánticas deberán aplicarse al final de la producción.

En las Gramáticas S-Atribuidas las producciones con sus correspondientes acciones semánticas presentan la siguiente forma:

A �� A1A2...An {acciones semánticas}

En las Gramáticas L-Atribuidas pueden presentarse de esta otra forma:

A �� {0} A1 {1} A2 {2} ... An {n} donde {0}, {1}, ... , {n} son acciones semánticas.

Para poder trabajar con ellas se han de transformar de la siguiente manera:

A �� S0 A1 S1 A2 S2 … An {n}

S0 �� ξ {0}

S1 �� ξ {1}

….

Sn-1 �� ξ {n-1} donde S0, S1, …, Sn son nuevos símbolos no terminales.

Para la realización del análisis, los atributos pueden almacenarse en una pila adicional, o bien en la misma pila.

Para la gramática G = ({E, T}, {+, -, (, ), num},

{E��E+T | E-T | T, T��(E) | num}, E), podríamos realizar una implementación de las acciones semánticas asociadas de la siguiente manera, para un analizador LR:

Producción Fragmento de código

L �� E (amp. LR) Print(val[tope])

E1 �� E2 + T val[nuevoTope] = val[tope-2]+val[tope]

E �� T

T1 �� T2 * F val[nuevoTope] = val[tope-2]*val[tope]

T �� F

F �� (E) val[nuevoTope] = val[tope-1]

F �� num

Los fragmentos de código se ejecutarán justo antes de que tenga lugar una reducción por la regla asociada.

5.4 MANEJO DE ERRORES

Es una de las misiones más importantes del compilador. Se utiliza más en el análisis pero los errores pueden darse en cualquier fase. El manejo de errores es una tarea difícil por dos motivos:

1.A veces algunos errores ocultan otros

2.Un error puede provocar una avalancha de errores que se solucionan con el primero

Criterios a seguir a la hora de manejar errores

1.Pararse al detectar el primer error (conveniente para un compilador interactivo)

2.Detectar todos los errores de una pasada (conveniente para un compilador de línea)

UNIDAD IV ANALISIS SINTACTICO


4.1 INTRODUCCIÓN A LAS GRAMÁTICAS LIBRES DE CONTEXTO Y ÁRBOLES DE DERIVACIÓN.

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.

UN EJEMPLO

ORACIÓN à SUJETO PREDICADO | PREDICADO

SUJETO à ARTÍCULO NOMBRE

ARTICULO à el | la

NOMBRE à casa | niño

PREDICADOà VERBO COMPLEMENTO

VERBO àcorre | es

COMPLEMENTO àbien | obediente | bonita

ÁRBOLES DE DERIVACION

Derivaciones por la izquierda y derecha:

Existen básicamente dos formas de describir como en una cierta gramática una cadena puede ser derivada desde el símbolo inicial. La forma más simple es listar las cadenas de símbolos consecutivas, comenzando por el símbolo inicial y finalizando con la cadena y las reglas que han sido aplicadas. Si introducimos estrategias como reemplazar siempre el no terminal de más a la izquierda primero, entonces la lista de reglas aplicadas es suficiente. A esto se le llama derivación por la izquierda. Por ejemplo, si tomamos la siguiente gramática:

(1) S → S + S

(2) S → 1

4.2 DIAGRAMAS DE SINTAXIS

Es otra forma (al igual que los árboles de derivación) de especificar gramáticas del tipo

La característica de este esquema es que permite ver las derivaciones al Instante de que ocurren.

Todo diagrama de sintaxis posee un origen y un destino, que no se suelen representar explícitamente, sino que se asume que el origen se encuentra a la izquierda del diagrama y el destino a la derecha.

Cada arco con origen en " y destino en $ representa que el símbolo " puede ir seguido del $ (pudiendo ser " y $ tanto terminales como no terminales). De esta forma todos los posibles caminos desde el inicio del grafo hasta el final, representan formas sentenciales válidas.

Demostraremos que los diagramas de sintaxis permiten representar las mismas gramáticas que la notación BNF, por inducción sobre las operaciones básicas de BNF:

4.3 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.

4.4 ANALIZADOR SINTÁCTICO

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.

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ácticos a partir de una especificación de la sintaxis del lenguaje, tales y como YACC, GNU bison y javacc.

Es el proceso de determinar si una cadena dada puede ser generada por una gramática.

Los analizadores sintácticos de lenguajes de programación suele hacerse de izquierda a derecha, viendo un componente léxico a la vez

Los analizadores pueden clasificarse dependiendo de la forma en como se construyen los nodos del árbol de derivación sintáctico: ascendentes y descendentes.

• Las gramáticas se pueden expresar en forma BNF (Backus-Naur Form).

• El análisis sintáctico impone una estructura jerárquica.

id1 :=

id2 +

id3 *

60

Tipos de analizadores sintácticos

• 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

Analizador descendente (LL)

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.

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

L={anabcn | n>0}

• SàaSc|aabc

• Se requiere una anticipación de por los

menos tres símbolos LL(3)

• SàaaAc

• AàSc|abc LL(1)

4.5 ADMINISTRACIÓN DE TABLAS 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.

4.6 MANEJO DE ERRORES SINTÁCTICOS Y SU RECUPERACIÓN

• 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.

• 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.

• for(i<3,>

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.

4.7 GENERADORES DE CÓDIGO PARA ANALIZADORES SINTÁCTICOS: YACC, BISON, BYACC, ANTLR, JAVACC.

YACC (YET ANOTHER COMPILERCOMPILER): provee una herramienta general para describir la entrada de un programa de computación. El usuario de YACC especifica las estructuras de su entrada, junto con el código que será invocado en la medida en que cada una de esas estructuras es reconocida.

YACC/BISON

• YACC convierte esa especificación en una subrutina que maneja el proceso de entrada.

La subrutina de entrada producida por YACC llama a la rutina provista por el usuario para devolver el próximo ítem básico de la entrada.

• GNU Bison es un generador de parsers de propósito general que convierte una descripción gramatical desde una gramática libre de contexto LALR en un programa en C para hacer el parser.

• Es utilizado para crear parsers para muchos lenguajes, desde simples calculadoras hasta lenguajes complejos.

• GNU Bison tiene compatibilidad con Yacc: todas las gramáticas bien escritas para Yacc, funcionan en Bison sin necesidad de ser modificadas.

• Cualquier persona que esté familiarizada con Yacc podría utilizar Bison sin problemas. Es necesaria experiencia con C para utilizar Bison.