Analizador Semantico

Analizador Semantico

Análisis semántico

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

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.

Propagación de atributos Sea la expresión

    int a,b,c;
    a/(b+c^2)

El árbol sintáctico es:

          /
      ---------
      |       |
      a       +
          ---------
          |       |
          b       ^
              ---------
              |       |
              c       2

De la instrucción declarativa, la tabla de símbolos y el analizador morfológico obtenemos los atributos de los operandos:

          /
      ---------
      |       |
      a       +
     int  ---------
          |       |
          b       ^
         int  ---------
              |       |
              c       2
             int     int

Propagando los atributos obtenemos:

          / int
      ---------
      |       |
      a       + int
     int  ---------
          |       |
          b       ^ int
         int  ---------
              |       |
              c       2
             int     int

Si la expresión hubiera sido

    a/(b+c^−2)

El árbol sintáctico sería el mismo, sustituyendo 2 por −2. Sin embargo, la propagación de atributos sería diferente:

          / real
      ---------
      |       |
      a       + real
     int  ---------
          |       |
          b       ^ real
         int  ---------
              |       |
              c      −2
             int     int

En algún caso podría llegar a producirse error (p.e. si / representara sólo la división entera).

Si la expresión hubiera sido

    int a,b,c,d;
    a/(b+c^d)

El árbol sintáctico sería el mismo, sustituyendo 2 por d. Sin embargo, la propagación de atributos sería incompleta:

          / {int,real}
      ---------
      |       |
      a       + {int,real}
     int  ---------
          |       |
          b       ^ {int,real}
         int  ---------
              |       |
              c       d
             int     int

El analizador semántico podría reducir los tipos inseguros al tipo máximo (real) o utilizar un tipo interno nuevo (ej. arit={int,real}, una unión).

Lo anterior es un ejemplo de propagación bottom-up. La propagación top-down también es posible: lo que se transmite son las restricciones y los tipos de las hojas sirven de comprobación. Por ejemplo, si la división sólo puede ser entera, transmitimos hacia abajo la restricción de que sus operandos sólo pueden ser enteros. Al llegar a d, esa restricción se convierte en que d debe ser positiva. Si no lo es, error.

La implantación de todos los casos posibles de operación con tipos mixtos podría ser excesivamente cara. En su lugar, se parte de operaciones relativamente simples (ej. int+int, real+real) y no se implementan las restantes (ej. int+real, real+int), añadiendo en su lugar operaciones monádicas de cambio de tipo (ej. int→real).

Esta decisión puede introducir ambigüedades. Por ejemplo, sea el programa

    real a;
    int b,c;
    a:=b+c

El árbol sintáctico es:

          :=
      ---------
      |       |
      a       +
    real  ---------
          |       |
          b       c
         int     int

Existen dos conversiones posibles:

          := real                     := real
      ---------                   ---------
      |       |                   |       |
      a       + real              a       + int
    real  ---------             real  ---------
          |       |                   |       |
          b       c                   b       c
         int     int                 int     int

El problema es que no tenemos garantía de que los dos procedimientos sean equivalentes. El segundo puede dar overflow, el primero pérdida de precisión. La definición del lenguaje debe especificar estos casos.

Las transformaciones posibles se pueden representar mediante un grafo cuyos nodos son los tipos de datos y cada arco indica una transformación. Dado un operando de tipo A que se desea convertir al tipo B, se trata de encontrar una cadena de arcos que pase de A a B en el grafo anterior. Podría haber varios grafos, cada uno de los cuales se aplicará en diferentes condiciones, por ejemplo, uno para las asignaciones, otro para las expresiones, etc.


Mis sitios nuevos:
Emprendedores
Politica de Privacidad